← All posts
Engineering
1 min read

npm ci caught what npm install let through

A peer-dependency conflict that passed locally on npm 11 and failed in CI on npm 10, and why every check now starts with npm ci.


A change passed every check on the development machine and failed in CI with a peer-dependency error. Nothing in the code was wrong. The two machines simply disagreed about what “installing” meant.

Two different installs

  • The development machine ran npm 11 and npm install.
  • CI (and Cloudflare’s build) ran npm 10 and npm ci.

npm install is forgiving. It resolves what it can, updates the lockfile if it needs to, and moves on. npm ci is strict. It installs exactly what the lockfile says, refuses if the lockfile and package.json disagree, and surfaces peer conflicts that install would paper over. Different major versions of npm also resolve peer dependencies differently.

So the local build had installed a tree that CI would not.

Run what production runs

The fix was not in the code; it was in the habit. The pre-PR check for the sites is now:

npm ci && npm run build && python3 check-html.py dist

npm ci first, because that is what Cloudflare runs when it deploys. If the check that runs locally differs from the one that runs in production, the local one is only telling you about your machine.

The lesson

Reproducibility is not only about pinning versions. It is about using the same command, in the same mode, as the environment that matters. A green local build made with a more forgiving tool is a false green.

AlzyWelzy

Building PaceStreak. GitHub

Build the habit. Keep the week.

PaceStreak is a habit and streak tracker for training and everything else worth doing regularly.