Lesson 5 of 6

Building a Real CI Pipeline

Time to compose everything into the workflow you'd actually ship: fail-fast ordering, dependency caching, tests across multiple runtime versions, and a built artifact at the end — the CI/CD track's pipeline design, expressed in the syntax from Lessons 2–4.

💡 The Bottom Line

Structure jobs by the fail-fast rule: cheap lint first, then a matrix test job across versions, then build — chained with needs:, sped up with cache:, ending in upload-artifact. This one file is the template for most projects you'll ever set up.

The complete pipeline

name: CI
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run lint

  test:
    needs: lint
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: ${{ matrix.node }}, cache: npm }
      - run: npm ci
      - run: npm test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

The four techniques inside

🙋 There Are No Dumb Questions

Q: The matrix runs npm ci twice — isn't that wasteful?
A: It's the price of honest isolation: each matrix leg is a separate machine testing a separate Node version, and dependencies can genuinely resolve differently per version — sharing them would blur exactly the difference you're testing for. The cache makes the repeat cheap (seconds, not minutes). Optimize pipeline time with caching and parallelism, not by making jobs share state they shouldn't.

⚠️ Watch Out

Make the pipeline a gate, not a suggestion: in Settings → Branches, add a protection rule for main requiring these checks to pass before merging. Without it, the red X is advisory and merge-anyway is one click — with it, "keep main green" is physics. One settings page turns your YAML into policy.

🔧 Try It Yourself

Adapt the pipeline to your real project (swap the npm commands for your stack's), push it, and then verify the two claims that matter:

1. Open the run: lint, then a fan of test (node 20) + test (node 22),
   then build — the graph view draws your needs: chain.
2. Re-run it: the "npm ci" steps drop from minutes to seconds — cache hit.
3. Download the dist artifact from the run's Summary page.

Then add the branch protection rule and open a deliberately-failing PR — watch the merge button lock.

✅ Quick Check

What does strategy.matrix with node: [20, 22] do?


Last lesson: the classic traps — workflows that never fire, secrets missing in fork PRs, and the other gotchas that cost an afternoon each.

← Back to
Next →