Reliability and CI

Build a CI test pipeline

Order fast checks, integration dependencies, browser tests, artifacts, and cancellation so feedback is useful and affordable.

CI has two jobs. Reject a broken change fast, and keep enough evidence to diagnose the slow failures. Everything about the pipeline layout follows from those two.

Stages of evidence

I order jobs by how quickly they answer and how much they cost:

flowchart TD
  accTitle: CI test pipeline
  accDescr: Static checks and unit tests run first, then a built artifact feeds integration and browser tests, followed by controlled performance smoke tests.
  Change["Code change"] --> Static["Static checks"]
  Change --> Unit["Unit tests"]
  Static --> Build["Build artifact"]
  Unit --> Build
  Build --> Integration["Integration tests"]
  Build --> Browser["Browser tests"]
  Integration --> Performance["Performance smoke tests"]
  Browser --> Performance
  1. Static checks: formatting, types, broken imports. No services needed, done in seconds.
  2. Unit tests: node --test. Deterministic domain regressions, also seconds.
  3. Integration tests: start the pinned PostgreSQL container, prove adapters, migrations, and HTTP contracts.
  4. Browser tests: a handful of critical user flows against the built app.
  5. Performance smoke tests: only in a controlled environment, with explicit thresholds.

A typo in normalizeBook() fails at stage 2, in under a minute. Nobody waits for Playwright to learn that a pure function is wrong.

Parallel where it’s cheap

Fast feedback doesn’t mean one long serial job. Static checks and unit tests can run side by side. Integration and browser jobs can start together once the build artifact exists, as long as each uses its own isolated services.

Make readiness explicit. “The process started” is weak. “The health endpoint answers and the database accepts a connection” is what you want before the first test runs.

Every job needs a failure story

Give every job a timeout. A hung container should fail in five minutes, not eat the six-hour runner limit.

Then ask: when this job fails, what do I need to know why? A product assertion, an npm install failure, a missing Docker daemon, and a server that never became ready are four different problems. Keep the unit test report, the service logs, and the Playwright traces on failure. Don’t upload big artifacts from every green run unless someone actually audits them.

Pin, cache, cancel

Pin the Node version and the PostgreSQL image, and bump them in their own commits. Cache node_modules keyed on the lockfile hash, not on the branch name, or a dependency change reuses a stale cache.

And cancel superseded runs on the same branch. When you push a fix two minutes after the last push, the old browser job should stop, not finish and post a stale red mark on your PR.

Try this: write the pipeline outline for the Books app. For each job, list the command, the required services, the readiness check, the timeout, and the artifacts to keep. Then state whether a failure in that job means the product is wrong or the environment is unavailable, and which piece of evidence tells you.

Lesson completed