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
needs:— turns parallel-by-default jobs into a chain: lint → test → build. A red lint stops everything in under a minute — fail-fast made literal.strategy.matrix— one job definition, expanded per value:node: [20, 22]runs the test job twice, in parallel, on separate VMs. Matrices multiply (node×os...), and so do the minutes — expand deliberately.cache: npm—setup-node's built-in caching stores the package cache between runs, keyed on your lockfile. Typically the single biggest speedup available, for one YAML key. (Other ecosystems:setup-pythoncaches pip; the genericactions/cachehandles anything.)upload-artifact— savesdist/as the run's artifact: downloadable from the run page, and — viadownload-artifactin a later job — the "build once, promote the artifact" principle in practice.
🙋 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.