Building & Testing in CI
🧪 Building & Testing in CI
Matrix Builds
Running the same job definition across multiple combinations of variables — Node 18/20/22, or multiple operating systems — automatically, in parallel. This directly extends Chapter 2's "jobs run in parallel" concept: a matrix job is really N parallel jobs generated from one definition.
This catches "works on Node 20 but breaks on Node 18" bugs before they reach users. A library published for others to use needs to actually work across whatever version range it claims to support — testing only the developer's own local version silently assumes every user matches that exact version, which is rarely true.
📦 Caching Dependencies
Without caching, every single run re-downloads every dependency from scratch — even though nothing changed most of the time. Dependencies get cached, keyed by a hash of the lockfile's contents:
Why Hash the Lockfile
A cache key must change when dependencies change — hashing the lockfile's contents guarantees the cache correctly invalidates the moment a dependency is added, removed, or updated.
The Speed Payoff
A cache hit skips re-downloading unchanged dependencies entirely — often the single biggest speed improvement available to a basic pipeline.
Running Existing Test Suites
Nothing new to learn here conceptually — it's about correctly invoking whatever test command the Go, Jest, or other course already taught (go test ./..., npm test) as a CI step, and letting that step's own exit code determine pass or fail.
🚦 Failing the Build on Test Failure
This is the actual enforcement mechanism that makes CI meaningful — Chapter 1's "blocks a merge on failure" point, made concrete. A step returning a non-zero exit code automatically fails the whole job. Most test runners already exit non-zero on any failing test by default — "failing correctly" is usually automatic, not something to manually configure.
No Caching/Matrix vs Both
Basic Workflow
Every run re-downloads everything from scratch, and only tests one Node version — slow, and blind to real compatibility bugs.
Cached + Matrix
Dependency install is nearly instant on a cache hit; three Node versions tested in parallel catch bugs a single-version run would miss entirely.
💻 Coding Challenges
Challenge 1: Add a Python Version Matrix
Write a job's strategy: matrix: block testing a Python project across versions 3.10, 3.11, and 3.12.
Goal: Practice writing a matrix block for a language other than this chapter's Node.js example.
Challenge 2: Explain What Breaks a Bad Cache Key
A workflow caches node_modules using the static key "npm-cache" (not derived from the lockfile). Explain what happens the next time a dependency is added to package.json, and why it's a problem.
Goal: Practice reasoning about the exact failure mode this chapter's tip box describes.
Challenge 3: Spot the Swallowed Exit Code
A workflow step is written as run: npm test | tee test-output.log. Explain why this can cause a pipeline to report success even when tests actually failed, and provide a fix.
Goal: Practice recognizing a subtle way the "fail on non-zero exit code" mechanism gets silently defeated.
Using a fixed cache key like "node-modules-cache" instead of a hash of the lockfile's contents silently reuses a stale cache even after dependencies have actually changed — nothing about the key itself changed to signal "this cache is now invalid." The result: a pipeline that passes using old, cached dependencies instead of the new ones a developer just added, potentially masking a real bug that only manifests with the correct, up-to-date dependencies, or missing a genuinely needed new package entirely. Always key caches off a hash of the lockfile's actual contents, never a fixed string.
🎯 What's Next
With builds and tests running fast and thoroughly, the next chapter covers what a pipeline needs to keep private: Environment Variables & Secrets — GitHub Secrets, and never committing credentials.