Overview

A CI/CD pipeline is a chain of automated gates on every push and pull request. Order them cheapest first so a failure surfaces in seconds, and never skip a gate to save time: a green build with a suppressed lint step is false confidence.

Run gates in this order: lint, typecheck, test, build, deploy

  • Lint: static analysis and style; seconds.
  • Typecheck: compile-time errors; seconds to tens of seconds.
  • Test: unit and integration (testing).
  • Build: bundling and static generation; the slowest gate before deploy.
  • Deploy: publish the artifact, only from main, never from a PR.

Chain jobs with needs and deploy only from main

Put the cheap gates in one job so you install dependencies once and fail fast; separate jobs for each gate run in sequence but repeat setup, so split only when you need parallelism or per-gate status.

name: CI
on:
  push: { branches: [main] }
  pull_request: { branches: [main] }
  merge_group:        # required if you use a merge queue
 
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
 
  build:
    needs: check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v7
        with: { name: build-output, path: dist/ }
 
  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v8
        with: { name: build-output, path: dist/ }
      - run: npm run deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Action majors checked October 2026: checkout v7, setup-node v7, upload-artifact v7, download-artifact v8, cache v6. Store deploy credentials as secrets, scoped per github-secrets.

Cache dependencies

setup-node with cache: npm keys on package-lock.json and restores the npm cache. For monorepos add cache-dependency-path: "**/package-lock.json". For build output (compiled assets, generated types) use actions/cache@v6 with a key that hashes the source files; a stale cache that yields wrong output is harder to debug than a slow build, so prefer narrow keys. More: set-up-github-actions-cache.

Require status checks by exact job name

In Settings > Branches, protect main, enable “Require status checks to pass before merging”, and add the job names from the workflow (check, build). The required name is the job name, not the workflow name, and a mismatch silently skips the requirement. Do not require deploy; it never runs on PRs. Enable “Require branches to be up to date” so two PRs that pass alone cannot break together, and use the merge queue on busy repositories, which needs the merge_group trigger shown above.

Preview on PRs, deploy on merge

Hosts such as Vercel and Cloudflare post a preview URL as a PR check. For a static artifact you can upload to a staging bucket per PR and comment the URL with actions/github-script. Verify the preview before promoting (pre-launch-checklist).

Mirror the gates locally with hooks

Wire lint and typecheck to a Husky pre-commit hook, and run only staged files with lint-staged so the hook stays around two seconds. git commit --no-verify bypasses hooks, so CI is the enforcing gate and hooks are fast feedback. Use the same commands in both. After release, watch regressions in error-tracking; version-control discipline is in git.