Building & Testing in CI

CI/CD Pipelines — 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.