Pipeline Design
Design the CI pipeline as a series of fast, independent gates that catch issues progressively from cheap to expensive.
Why
A CI pipeline is a series of quality gates — each gate catches a class of issue (lint catches style, type check catches type errors, unit tests catch logic bugs, e2e tests catch integration bugs) and blocks the build if the issue is present. The design of the pipeline determines the feedback speed (fast gates give quick feedback, slow gates block the developer) and the defect leakage (gates that are missing or too weak let bugs through to production). The key principle is progressive filtering: run the cheap, fast gates first (lint in 10 seconds) and the expensive, slow gates last (e2e in 10 minutes), so most failures are caught quickly and only the few PRs that pass all cheap gates pay the cost of the expensive ones. A pipeline that runs all gates in parallel and waits for all to finish is slower than one that runs cheap gates first and short-circuits on failure. A pipeline that has no gates lets every bug through; a pipeline with too many gates is slow and the developers bypass them. The art is finding the right set of gates — enough to catch the important bugs, fast enough to not block the developer.
When
At project start and whenever the feedback loop feels too slow or too many bugs reach production — the pipeline design is the lever for both.
How
1. Identify the gates you need - Lint (style, common bugs): 10-30 seconds - Type check (type errors): 30-60 seconds - Unit tests (logic): 1-5 minutes - Integration tests (API, DB): 5-10 minutes - E2E tests (user flows): 10-30 minutes - Security scan (CVEs, SAST): 2-5 minutes - Build (compilation, bundling): 1-5 minutes 2. Order the gates from cheap to expensive - Run lint first (10s) — if it fails, stop (no point running tests on broken code) - Then type check (30s) — if it fails, stop - Then unit tests (2m) — if they fail, stop - Then build (3m) — if it fails, stop - Then integration tests (5m) — if they fail, stop - Then e2e tests (15m) — only if all above pass - This short-circuits on the first failure, saving time 3. Run independent gates in parallel - Lint and type check can run in parallel (both are fast, independent) - Unit tests and security scan can run in parallel - But do not run e2e until unit tests pass (e2e is expensive) - Use a matrix build for parallel jobs 4. Cache aggressively - Dependencies: cache node_modules, ~/.m2, ~/.cargo - Build artifacts: cache dist/, target/, build/ - Test results: cache test results to skip unchanged tests - This reduces the gate time from minutes to seconds 5. Make the gates mandatory - Branch protection: require all gates to pass before merge - Do not allow bypass (even for admins) - A gate that can be bypassed is a gate that will be bypassed 6. Monitor and tune - Track the pipeline time (median and 90th percentile) - If the pipeline is slow, identify the slowest gate and optimize it - If bugs leak, identify the missing gate and add it - The pipeline is not set-and-forget; it evolves with the project
Example
# .github/workflows/ci.yml — progressive gates
name: CI
on: [pull_request]
jobs:
# Gate 1: Lint (10s) — cheapest, run first
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx eslint . --max-warnings 0
- run: npx prettier --check .
# Gate 2: Type check (30s) — parallel with lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx tsc --noEmit
# Gate 3: Unit tests (2m) — after lint and typecheck pass
unit-test:
needs: [lint, typecheck] # wait for cheap gates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --coverage --maxWorkers=4
# Gate 4: Build (3m) — after unit tests pass
build:
needs: [unit-test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
# Gate 5: E2E tests (15m) — expensive, only if all above pass
e2e-test:
needs: [build] # wait for build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- run: npx playwright test
# Branch protection: require all gates to pass
# Settings > Branches > Branch protection rules:
# - Require status checks: lint, typecheck, unit-test, build, e2e-test
# - Require branches to be up to date before merging
# - Do not allow bypassing (even for admins)
# Caching: speed up the gates
# .github/workflows/ci.yml (add to each job):
# - uses: actions/cache@v4
# with:
# path: node_modules
# key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}Common Problems
- ⚠Running all gates in parallel and waiting for all to finish — the lint failure is caught in 10 seconds, but the pipeline waits 15 minutes for the e2e tests to finish before reporting the failure, and the developer is blocked for 15 minutes on a 10-second issue.
- ⚠Running e2e tests on every PR regardless of what changed — a CSS-only change triggers a 15-minute e2e run, the developer waits 15 minutes for a gate that could not possibly fail, and the pipeline is perceived as slow.
- ⚠Allowing admins to bypass the gates — the senior developer pushes code that fails the type check, the team learns the gates are optional, and the gates become theater rather than a real protection.
- ⚠Having no gates at all — every PR merges directly to main, the bugs ship to production, and the team relies on hotfixes rather than prevention, which is 10x more expensive than a CI gate.
Best Practices
- ✓Order the gates from cheap to expensive (lint 10s → type check 30s → unit tests 2m → build 3m → e2e 15m) and short-circuit on the first failure — this gives fast feedback for most failures and only pays the expensive gates for PRs that pass the cheap ones.
- ✓Run independent gates in parallel (lint and type check) but use needs: to wait for cheap gates before expensive ones — parallelism speeds up the pipeline, but progressive filtering short-circuits on failure.
- ✓Cache dependencies, build artifacts, and test results aggressively — this reduces gate time from minutes to seconds, and a fast pipeline is a pipeline that developers do not bypass.
- ✓Make all gates mandatory with branch protection and do not allow bypass (even for admins) — a gate that can be bypassed is a gate that will be bypassed, and the team learns the gates are optional.