All Docs
Getting StartedCalmony Sanctions MonitorUpdated March 11, 2026

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?

Behaviournpm installnpm ci
Reads package-lock.jsonPartially — uses it as a hintStrictly — fails if lock file is absent or out of sync
Modifies package-lock.jsonYes, if drift is detectedNever
Installs transitive depsMay resolve newer versions silentlyExactly what the lock file records
Removes node_modules firstNoYes — always starts clean
Speed in CISlower (cache checking)Faster (no resolution step)
Failure on mismatchNo — silently updatesYes — 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:

  1. Identify why the lock file drifts (e.g. AI-assisted dependency updates that modify package.json without regenerating the lock file).
  2. Add a step to the AI agent workflow that runs npm install --package-lock-only after any package.json change to regenerate the lock file.
  3. Commit the updated package-lock.json before the CI run.
  4. Switch schema-push.yml to npm 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 install with npm ci in ci.yml and deploy.yml project dependency steps.
  • Fix the lock file drift root cause in schema-push.yml before switching it to npm ci.
  • Keep npm install -g vercel@latest in deploy.yml — global tool installs are not affected by this change.
  • The schema-push.yml workaround comment should be removed, not maintained.