CI/CD Pipelines

Patterns for continuous integration and deployment with GitHub Actions.

Overview#

CI/CD automates testing and deployment for reliable releases. This pattern covers:

  • GitHub Actions workflows
  • Testing and linting
  • Preview deployments
  • Production deployments
  • Database migrations

Prerequisites#

# GitHub repository with Actions enabled # Vercel, AWS, or other deployment target

Code Example#

Complete CI Workflow#

1# .github/workflows/ci.yml 2name: CI 3 4on: 5 push: 6 branches: [main] 7 pull_request: 8 branches: [main] 9 10env: 11 NODE_VERSION: '20' 12 13jobs: 14 lint: 15 runs-on: ubuntu-latest 16 steps: 17 - uses: actions/checkout@v4 18 - uses: actions/setup-node@v4 19 with: 20 node-version: ${{ env.NODE_VERSION }} 21 cache: 'npm' 22 23 - run: npm ci 24 - run: npm run lint 25 26 typecheck: 27 runs-on: ubuntu-latest 28 steps: 29 - uses: actions/checkout@v4 30 - uses: actions/setup-node@v4 31 with: 32 node-version: ${{ env.NODE_VERSION }} 33 cache: 'npm' 34 35 - run: npm ci 36 - run: npm run typecheck 37 38 test: 39 runs-on: ubuntu-latest 40 steps: 41 - uses: actions/checkout@v4 42 - uses: actions/setup-node@v4 43 with: 44 node-version: ${{ env.NODE_VERSION }} 45 cache: 'npm' 46 47 - run: npm ci 48 - run: npm test -- --coverage 49 50 - name: Upload coverage 51 uses: codecov/codecov-action@v3 52 with: 53 token: ${{ secrets.CODECOV_TOKEN }} 54 55 build: 56 runs-on: ubuntu-latest 57 needs: [lint, typecheck, test] 58 steps: 59 - uses: actions/checkout@v4 60 - uses: actions/setup-node@v4 61 with: 62 node-version: ${{ env.NODE_VERSION }} 63 cache: 'npm' 64 65 - run: npm ci 66 - run: npm run build 67 68 - name: Upload build 69 uses: actions/upload-artifact@v4 70 with: 71 name: build 72 path: .next 73 retention-days: 1 74 75 e2e: 76 runs-on: ubuntu-latest 77 needs: build 78 steps: 79 - uses: actions/checkout@v4 80 - uses: actions/setup-node@v4 81 with: 82 node-version: ${{ env.NODE_VERSION }} 83 cache: 'npm' 84 85 - name: Install Playwright 86 run: npx playwright install --with-deps 87 88 - name: Download build 89 uses: actions/download-artifact@v4 90 with: 91 name: build 92 path: .next 93 94 - run: npm ci 95 - run: npm run e2e 96 97 - name: Upload test results 98 if: failure() 99 uses: actions/upload-artifact@v4 100 with: 101 name: playwright-report 102 path: playwright-report

Production Deployment#

1# .github/workflows/deploy.yml 2name: Deploy 3 4on: 5 push: 6 branches: [main] 7 8jobs: 9 deploy: 10 runs-on: ubuntu-latest 11 environment: production 12 steps: 13 - uses: actions/checkout@v4 14 - uses: actions/setup-node@v4 15 with: 16 node-version: '20' 17 cache: 'npm' 18 19 - run: npm ci 20 21 - name: Run migrations 22 run: npx prisma migrate deploy 23 env: 24 DATABASE_URL: ${{ secrets.DATABASE_URL }} 25 26 - name: Deploy to Vercel 27 run: vercel --prod --token=${{ secrets.VERCEL_TOKEN }} 28 env: 29 VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 30 VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

Preview Deployments#

1# .github/workflows/preview.yml 2name: Preview 3 4on: 5 pull_request: 6 types: [opened, synchronize] 7 8jobs: 9 deploy-preview: 10 runs-on: ubuntu-latest 11 steps: 12 - uses: actions/checkout@v4 13 14 - name: Deploy to Vercel 15 id: deploy 16 uses: amondnet/vercel-action@v25 17 with: 18 vercel-token: ${{ secrets.VERCEL_TOKEN }} 19 vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} 20 vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} 21 22 - name: Comment PR 23 uses: actions/github-script@v7 24 with: 25 script: | 26 github.rest.issues.createComment({ 27 issue_number: context.issue.number, 28 owner: context.repo.owner, 29 repo: context.repo.repo, 30 body: `## Preview Deployment 31 32 Your changes have been deployed to: 33 ${{ steps.deploy.outputs.preview-url }} 34 35 Commit: \`${{ github.sha }}\`` 36 })

Matrix Testing#

1# Test across multiple Node versions and OS 2jobs: 3 test: 4 runs-on: ${{ matrix.os }} 5 strategy: 6 matrix: 7 os: [ubuntu-latest, macos-latest] 8 node-version: [18, 20, 22] 9 fail-fast: false 10 11 steps: 12 - uses: actions/checkout@v4 13 - uses: actions/setup-node@v4 14 with: 15 node-version: ${{ matrix.node-version }} 16 17 - run: npm ci 18 - run: npm test

Database Migration Job#

1# .github/workflows/migrate.yml 2name: Database Migration 3 4on: 5 workflow_dispatch: 6 inputs: 7 environment: 8 description: 'Environment to migrate' 9 required: true 10 type: choice 11 options: 12 - staging 13 - production 14 15jobs: 16 migrate: 17 runs-on: ubuntu-latest 18 environment: ${{ inputs.environment }} 19 steps: 20 - uses: actions/checkout@v4 21 - uses: actions/setup-node@v4 22 with: 23 node-version: '20' 24 25 - run: npm ci 26 27 - name: Run migrations 28 run: npx prisma migrate deploy 29 env: 30 DATABASE_URL: ${{ secrets.DATABASE_URL }} 31 32 - name: Notify on failure 33 if: failure() 34 uses: actions/github-script@v7 35 with: 36 script: | 37 github.rest.issues.create({ 38 owner: context.repo.owner, 39 repo: context.repo.repo, 40 title: 'Migration failed in ${{ inputs.environment }}', 41 body: 'Check workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}', 42 labels: ['bug', 'urgent'] 43 })

Release Workflow#

1# .github/workflows/release.yml 2name: Release 3 4on: 5 push: 6 tags: 7 - 'v*' 8 9jobs: 10 release: 11 runs-on: ubuntu-latest 12 steps: 13 - uses: actions/checkout@v4 14 with: 15 fetch-depth: 0 16 17 - uses: actions/setup-node@v4 18 with: 19 node-version: '20' 20 21 - run: npm ci 22 - run: npm run build 23 24 - name: Generate changelog 25 id: changelog 26 uses: requarks/changelog-action@v1 27 with: 28 token: ${{ secrets.GITHUB_TOKEN }} 29 tag: ${{ github.ref_name }} 30 31 - name: Create Release 32 uses: softprops/action-gh-release@v1 33 with: 34 body: ${{ steps.changelog.outputs.changelog }} 35 generate_release_notes: true 36 files: | 37 dist/*

Caching Strategies#

1# Efficient caching 2- uses: actions/setup-node@v4 3 with: 4 node-version: '20' 5 cache: 'npm' 6 7# Custom caching for Next.js 8- uses: actions/cache@v4 9 with: 10 path: | 11 ~/.npm 12 .next/cache 13 key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }} 14 restore-keys: | 15 ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}- 16 ${{ runner.os }}-nextjs- 17 18# Playwright browser caching 19- uses: actions/cache@v4 20 with: 21 path: ~/.cache/ms-playwright 22 key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}

Conditional Jobs#

1jobs: 2 changes: 3 runs-on: ubuntu-latest 4 outputs: 5 frontend: ${{ steps.filter.outputs.frontend }} 6 backend: ${{ steps.filter.outputs.backend }} 7 steps: 8 - uses: actions/checkout@v4 9 - uses: dorny/paths-filter@v2 10 id: filter 11 with: 12 filters: | 13 frontend: 14 - 'app/**' 15 - 'components/**' 16 backend: 17 - 'lib/**' 18 - 'prisma/**' 19 20 frontend-tests: 21 needs: changes 22 if: ${{ needs.changes.outputs.frontend == 'true' }} 23 runs-on: ubuntu-latest 24 steps: 25 - run: npm run test:frontend 26 27 backend-tests: 28 needs: changes 29 if: ${{ needs.changes.outputs.backend == 'true' }} 30 runs-on: ubuntu-latest 31 steps: 32 - run: npm run test:backend

Usage Instructions#

  1. Create workflow files in .github/workflows/
  2. Set up required secrets in repository settings
  3. Configure branch protection rules
  4. Enable required status checks
  5. Set up deployment environments

Best Practices#

  • Run jobs in parallel - Speed up CI with parallel execution
  • Use caching - Cache dependencies and build artifacts
  • Fail fast - Stop on first failure in matrix builds
  • Environment protection - Require approvals for production
  • Status checks - Block merges on failing checks
  • Artifact retention - Set appropriate retention periods
  • Secrets management - Never log secrets, use GitHub Secrets