Building & Testing in CI

CI/CD Pipelines
Chapter 4 ยท Building & Testing in CI

๐Ÿงช Building & Testing in CI

Chapter 3 built one working workflow. This chapter goes deeper on the build/test stages specifically โ€” making them faster with caching and more thorough with matrix builds, the two most common practical upgrades teams make to a basic pipeline.

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.

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

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:

- uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' # keyed internally off package-lock.json's hash

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.

โ†’ Solution

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.

โ†’ Solution

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.

โ†’ Solution

โš ๏ธ Gotcha: Caching With a Static Key

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.