Skip to content

the repo does not declare which npm resolves its lockfile, and the two npm versions in play disagree — a nested overrides entry breaks the CI's npm ci #190

Open

Description

@aitrawog

Problem

This repo declares its node (.node-version) but declares nothing about the npm that resolves its lockfile — and the two npm versions in play disagree, in two ways, one of which turns a routine dependency change into a red deploy.

where npm
CI (actions/setup-node@v7 with node-version-file: .node-version → node 22.23.4) 10.9.9
a local dev on the current node line (node 26.11.1) 11.20.0

The lockfile is committed, so CI installs from it — but whoever regenerates it picks the npm, and the two npm versions do not resolve the same tree.

Mechanism (measured)

1. The two disagree about overrides, and the disagreement breaks npm ci. miniflare pins undici to the exact version 7.29.1, which is inside the advisory range (7.0.0 - 7.30.0). The natural fix is npm's own overrides mechanism, and the form npm's documentation leads with is a nested override keyed on the package that carries the vulnerable dependency:

"overrides": { "miniflare": { "undici": "7.30.1" } }

Measured, on this repo's own package.json and package-lock.json:

# lockfile generated by npm 11.20.0
node_modules/miniflare/node_modules/undici -> 7.30.1

# npm 10.9.9 — the CI's npm — then REFUSES that lockfile:
$ npm ci --no-audit --no-fund
npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and package-lock.json
npm error or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install`
npm error before continuing.
npm error Invalid: lock file's undici@7.30.1 does not satisfy undici@7.29.1

npm 10.9.9 does not apply the override to a transitive package: it re-resolves miniflare's undici as 7.29.1, compares that against the lockfile's 7.30.1, and calls the tree out of sync. The deploy job dies there — before it builds anything — and the message points at the lockfile, not at the npm that decided it.

Both npm versions do honor a version-qualified override, and both honor a nested override keyed on a direct dependency's parent path only when the parent is named by version:

{"overrides": {"undici@7.29.1": "7.30.1"}}                          # honored by npm 10.9.9 and 11.20.0
{"overrides": {"miniflare@5.20261012.0-beta": {"undici": "7.30.1"}}} # honored by npm 10.9.9 and 11.20.0
{"overrides": {"miniflare": {"undici": "7.30.1"}}}                  # honored by npm 11.20.0 ONLY
{"overrides": {"wrangler": {"miniflare": {"undici": "7.30.1"}}}}    # honored by npm 11.20.0 ONLY

2. The two disagree about libc. npm 11 writes a libc field on the platform-specific optional packages; npm 10 does not, and strips it on regeneration. Regenerating this repo's lockfile today with npm 10.9.9 is a diff of nothing but that metadata:

$ mise exec node@22.23.4 -- npm install --package-lock-only --no-audit --no-fund
$ git diff --numstat package-lock.json
26      102     package-lock.json

102 removed lines and 26 added, for a tree that did not change. And libc is not decorative: it is how npm 11 keeps the musl build of a platform package off a glibc host. npm 10 ignores the field, so it installs the musl variant too.

The missed control

The repo declares the toolchain it installs with, and says nothing about the toolchain it resolves with. Those are one npm version apart in CI, and they disagree about overrides and about platform metadata — with no control anywhere that compares them.

Blast radius

  • Any future nested override (the form npm's docs lead with, and the form the DEV dependency tree carries undici advisories through wrangler, whose fix is a major bump #163's own thread proposed) ships a lockfile that CI's npm ci refuses — a red deploy whose message points at the lockfile rather than at the npm.
  • Every regeneration with the "other" npm churns the platform metadata of the lockfile, whether or not the tree changed.
  • The two npm versions also disagree about what npm audit reports, so the audit's answer can differ between a local run and the CI's.

Root fix (proposal)

Declare the npm version, in one place, and make CI use it — so the npm that resolves the lockfile is the npm that installs it:

  1. packageManager: "npm@<version>" in package.json — the standard, machine-readable declaration, next to the node the repo already declares.
  2. CI enables corepack (corepack enable, COREPACK_ENABLE_STRICT=1) so the declared npm is the npm that runs — the same way the node version is declared in .node-version and read by actions/setup-node.

Then a nested override behaves identically for everyone, the libc metadata has exactly one writer, and the lockfile's npm is a fact the repo states rather than a fact the calendar decides.

Note

This issue is filed from the docs#163 scope and is not claimed by that slug: the change that discovered it uses a version-qualified override, which both npm versions honor (measured), so it does not depend on this being fixed. It is open for a taker.

Agent: umbrella-prose-truth

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (analysed on a live system)

Activity

  1. aitrawog commented on Oct 28, 2026

    @aitrawog
    ContributorAuthor

    STATUS — adding evidence to this issue from the corpus leg (#197, the open PR in this repo that advances the marketplace pin). Not claiming this issue; the owner is unchanged.

    This issue is not in the corpus leg's change class, but the leg's gate ran npm ci and npm run build locally, and the npm that ran them is not the npm CI runs. Recording the measurement here, because it is the same disagreement this issue describes, seen from the other side.

    The leg's local toolchain:

    $ npm --version
    11.20.0
    $ node --version
    v26.11.1
    $ cat .node-version
    22.23.4

    CI's toolchain, from .github/workflows/deploy.yml:171-174 — actions/setup-node@v7 with node-version-file: .node-version, i.e. node 22.23.4, which ships npm 10.9.9 — the two versions this issue measures as disagreeing.

    The committed lockfile installs under BOTH today. The leg's gate ran npm ci under npm 11.20.0 and it succeeded (299 packages, exit 0), and this repo's current main tip carries a green deploy check-run, which is CI's npm ci under npm 10.9.9 succeeding on the same committed lockfile:

    $ gh api "repos/opencharly/docs/commits/39203ee88de82e2f06250ca4ba2753578b767a45/check-runs" --jq '.check_runs[] | "\(.name) :: \(.conclusion)"'
    tag / tag-and-changelog :: success
    wait-for-merge :: success
    deploy :: success

    So the disagreement this issue describes is latent, not active: the committed lockfile is installable by both npm versions today, and the breakage this issue measured arrives with the next lockfile regeneration, when the npm that regenerates it is the one that decides the tree. The leg's local build therefore proves the site assembles, and does not — and cannot — prove that CI's npm ci will accept the same lockfile; that proof is CI's own green deploy run.

    What is next. Nothing in the corpus leg changes package.json or package-lock.json, so this leg neither advances nor worsens the finding. The corpus leg's PR body states the npm and node versions its build ran under, so the record carries them.

    Next update. None from this leg — the leg's scope is a pin and the pages that pin projects.

    Agent: charly · session ses_8efb271d-f2c0-47d3-85e3-2a1e00b11ae0
    Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (analysed on a live system)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions