Build Reproducibility: Why We're Moving CI to npm ci
Build Reproducibility: Why We're Moving CI to npm ci
This post accompanies the v0.1.49 release (DEP-20 audit finding). It explains the problem, the fix, and why it matters for a sanctions screening platform.
The Problem
All three CI workflow files in the repository — ci.yml, schema-push.yml, and deploy.yml — currently invoke npm install to install project dependencies before running builds, tests, linting, and deployments.
This is subtly wrong, and for a compliance-critical application it is worth explaining exactly why.
npm install vs npm ci: What's the Difference?
| Behaviour | npm install | npm ci |
|---|---|---|
Reads package-lock.json | Partially — uses it as a hint | Strictly — fails if lock file is absent or out of sync |
Modifies package-lock.json | Yes, if drift is detected | Never |
| Installs transitive deps | May resolve newer versions silently | Exactly what the lock file records |
Removes node_modules first | No | Yes — always starts clean |
| Speed in CI | Slower (cache checking) | Faster (no resolution step) |
| Failure on mismatch | No — silently updates | Yes — exits non-zero |
The key risk with npm install in CI is silent transitive drift. If package-lock.json is even slightly out of sync with package.json (which happens naturally as tools update, as PRs merge without refreshing the lock file, or — as our schema-push.yml comment acknowledges — when AI agents modify dependencies), npm install will silently resolve a different dependency tree than the one recorded. The build succeeds, tests pass, and a different set of packages ships to production.
Why This Matters for Sanctions Screening
Sanctions screening platforms sit at the intersection of financial regulation and software supply-chain integrity. Two compliance pressures make build reproducibility non-negotiable:
1. Audit Trail Integrity
Regulators and internal audit teams expect that any past deployment can be reproduced from source. If the dependency tree that ran in production differs from what the lock file records, you cannot reliably recreate the exact binary that processed a screening decision. npm ci enforces a 1:1 relationship between package-lock.json and what gets installed.
2. Supply-Chain Risk
A silently updated transitive dependency is a potential supply-chain attack vector. Malicious packages have been introduced into the npm registry as minor version bumps of popular transitive dependencies. Using npm install in CI means a compromised transitive package could enter the build without any code change in the repository.
The schema-push.yml Case
The most concerning finding is in schema-push.yml, which contains an inline comment along the lines of:
# tolerates lock file drift caused by AI agents
This comment documents a known deviation and treats it as an acceptable permanent state. It should instead be treated as a technical debt marker with a concrete remediation path:
- Identify why the lock file drifts (e.g. AI-assisted dependency updates that modify
package.jsonwithout regenerating the lock file). - Add a step to the AI agent workflow that runs
npm install --package-lock-onlyafter anypackage.jsonchange to regenerate the lock file. - Commit the updated
package-lock.jsonbefore the CI run. - Switch
schema-push.ymltonpm ci.
The workaround comment should be removed once the root cause is fixed — not left as documentation of permanent drift.
The Fix
ci.yml — Build and Lint Jobs
# Before
- run: npm install
# After
- run: npm ci
Apply this to every step in ci.yml that installs project dependencies.
schema-push.yml — After Fixing Lock File Drift
# Before
- run: npm install # tolerates lock file drift caused by AI agents
# After (once lock file is kept in sync)
- run: npm ci
deploy.yml — Project Deps vs Global Tools
# Project dependency install — change to npm ci
- run: npm ci
# Global Vercel CLI install — leave as-is, this is not a project dependency
- run: npm install -g vercel@latest
The npm install -g line installs a global CLI tool rather than a project dependency. It has no lock file equivalent and is fine to leave unchanged.
Verification
After applying the changes, a misconfigured or drifted lock file will cause the CI workflow to fail fast with an error like:
npm error `npm ci` can only install packages when your package.json
npm error and package-lock.json or npm-shrinkwrap.json are in sync.
This is the correct behaviour — a failed CI build is far preferable to a silently non-reproducible one reaching production.
Summary
- Replace
npm installwithnpm ciinci.ymlanddeploy.ymlproject dependency steps. - Fix the lock file drift root cause in
schema-push.ymlbefore switching it tonpm ci. - Keep
npm install -g vercel@latestindeploy.yml— global tool installs are not affected by this change. - The
schema-push.ymlworkaround comment should be removed, not maintained.