GitHub Actions Complete Guide 2026: CI/CD Pipelines, Workflows, and Automation
Advertisement
GitHub Actions 2026: Automate Everything
GitHub Actions is the default CI/CD for most teams. It's free for public repos and has the best integration with GitHub. This guide covers production pipelines.
- Core Concepts
- Full CI/CD Pipeline for Next.js
- Docker Build and Push
- Deploy to AWS ECS
- Reusable Workflows
- Matrix Strategy: Test Multiple Versions
- Caching for Speed
- Auto-Merge Dependabot PRs
- Scheduled Jobs
Core Concepts
Workflow → YAML file in .github/workflows/
Event → What triggers the workflow (push, PR, schedule)
Job → A set of steps that run on one runner
Step → Individual task (run command or use action)
Runner → VM where job runs (ubuntu-latest, etc.)
Action → Reusable step from Marketplace
Full CI/CD Pipeline for Next.js
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
# Job 1: Run tests
test:
name: Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run type check
run: npm run typecheck
- name: Run unit tests with coverage
run: npm run test:coverage
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
# Job 2: E2E tests
e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
# Job 3: Deploy to production
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: [test, e2e]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Docker Build and Push
# .github/workflows/docker.yml
name: Build and Push Docker Image
on:
push:
branches: [main]
tags: ['v*']
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp }}
GIT_SHA=${{ github.sha }}
Deploy to AWS ECS
deploy-aws:
runs-on: ubuntu-latest
needs: docker
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition my-app \
--query taskDefinition > task-definition.json
- name: Update ECS container image
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: my-app
image: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
- name: Deploy to ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: my-app-service
cluster: my-cluster
wait-for-service-stability: true
Reusable Workflows
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
DEPLOY_TOKEN:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Deploy to ${{ inputs.environment }}
run: echo "Deploying to ${{ inputs.environment }}"
env:
TOKEN: ${{ secrets.DEPLOY_TOKEN }}
# Use in another workflow:
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
secrets:
DEPLOY_TOKEN: ${{ secrets.STAGING_TOKEN }}
Matrix Strategy: Test Multiple Versions
jobs:
test:
strategy:
fail-fast: false
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
Caching for Speed
- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: npm-${{ runner.os }}-
- name: Cache Next.js build
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
Auto-Merge Dependabot PRs
# .github/workflows/dependabot-auto-merge.yml
name: Dependabot Auto-merge
on: pull_request
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1
- name: Enable auto-merge for patch/minor updates
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Scheduled Jobs
# .github/workflows/scheduled.yml
name: Scheduled Tasks
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight UTC
- cron: '0 */6 * * *' # Every 6 hours
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Clean up old data
run: curl -X POST ${{ secrets.API_URL }}/admin/cleanup
env:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
sitemap-ping:
runs-on: ubuntu-latest
steps:
- name: Ping Google Sitemap
run: |
curl "https://www.google.com/ping?sitemap=https://webcoderspeed.com/sitemap.xml"
GitHub Actions is the backbone of modern software delivery. Every commit should trigger tests. Every merge to main should deploy. Automate the boring parts so you can focus on building.
Advertisement