Skip to content

fix: pin npm past an EALLOWGIT regression that breaks node-hook installs - #210

Open
wayneminwang wants to merge 5 commits into
mainfrom
f/weimin_fix_stale_precommit_version
Open

wayneminwang wants to merge 5 commits into
mainfrom
f/weimin_fix_stale_precommit_version

Conversation

@wayneminwang

Copy link
Copy Markdown

Problem

language: node pre-commit hooks with additional_dependencies (mirrors-prettier
ships prettier@x.y.z, mirrors-eslint ships eslint@x.y.z) fail on npm
11.9.0-11.12.x:

npm error code EALLOWGIT
npm error Fetching non-root packages of type "git" have been disabled

This is a real npm bug, not a pre-commit problem: npm/cli#9189, fixed in
11.13.0 via npm/cli#9206 (backported as #9237). --allow-git=root was
incorrectly rejecting legitimate root-level git installs in that range.
Verified by bisecting exact npm versions against pre-commit's actual
install command -- 11.8.x and below pass, 11.9.0-11.12.x fail, 11.13.0+
passes.

Any repo forced to rebuild its hook environments hits this, which also
blocks release pipelines gated on lint. Hook environments already present
in the S3 cache mask the failure, so repos break unpredictably: the
trigger is any change to .pre-commit-config.yaml invalidating the cache
key.

An earlier version of this PR pinned pre-commit below 4.6.1 instead, on
the (wrong) theory that pre-commit's node-hook installer could never
satisfy npm's root-package check. That's disproved by this fix: with npm

= 11.13.0, pre-commit's current (4.6.2, unpinned) installer works fine.

Change

  • After Setup node, symlink node/npm to a path outside $HOME and add it
    to $PATH. pre-commit only reuses an already-installed node/npm when it
    resolves outside $HOME (pre_commit/lang_base.py::exe_exists); on our
    self-hosted runners, actions/setup-node installs under $HOME
    (RUNNER_TOOL_CACHE), so without this pre-commit ignores it and
    downloads its own via nodeenv instead -- whose bundled npm can land in
    the same affected range.
  • Symlink both node and nodejs (not just node) to that same target.
    exe_exists only gates whether pre-commit enters "system" mode, by
    checking for node/npm. Once in system mode, nodeenv's own shim
    search (nodeenv.py::create_environment) does a separate, $HOME-blind
    lookup that checks the literal name nodejs before node. Our
    self-hosted runner image bakes in /usr/bin/nodejs (apt-installed,
    unrelated to anything actions/setup-node provisions), so without a
    nodejs entry in our symlink dir too, that lookup walks past it (no
    match) and silently falls through to /usr/bin/nodejs -- bypassing the
    npm pin below entirely, with no error. Verified empirically on a real
    runner: without this symlink, the resulting node_env-system resolved
    to the image's baked-in v22.23.2 (bundled npm 10.9.8) instead of the
    v24.15.0/npm 12.0.2 set up by this action. The only reason earlier test
    runs still showed "no EALLOWGIT" is that this image's baked npm (10.9.8)
    happens to be outside the affected range -- coincidence, not this fix
    actually taking effect. A different runner image (or this one after an
    OS package bump) could easily have exposed the exact bug this PR is
    meant to close, silently unprotected.
  • npm install -g npm@12.0.2 through that path. Pinned to an exact,
    verified version rather than an open range or latest: bumping it later
    is a deliberate action that should be re-verified first, the same way a
    hook rev: bump gets verified, not something that silently drifts back
    into a broken npm the way this bug shipped in the first place.
  • No changes to how pre-commit itself is detected/installed -- reverted to
    main's existing behavior.

Net diff vs main is +43 lines in one new step; everything else is
unchanged.

Test plan

  • Bisected the affected npm range directly against pre-commit's install
    command (npm install --allow-git=root --install-links -g git+file://<hook> <dep>): 11.8.x OK, 11.9.0-11.12.x fail, 11.13.0+ OK.
  • Verified in a consumer repo on a real runner with the hook cache
    cleared (unpinned pre-commit 4.6.2 on PATH, untouched): after this
    fix, effective npm is 12.0.2, mirrors-prettier installs, prettier
    passes, no EALLOWGIT.
  • Ruled out default_language_version: {node: system} as a
    symlink-free alternative: it hits the identical nodejs-vs-node
    lookup-order issue, with zero mitigation available (no file to add,
    since it's config-only) -- confirmed empirically, node_env-system
    also landed on the OS-baked node in that setup.
  • Decisive check on the real shipped action after adding the nodejs
    symlink: node_env-system's node --version now reads v24.15.0
    (matches the pinned setup), not the OS-baked v22.23.2. Shim script
    content confirms it resolves through our symlink dir, not
    /usr/bin.

Design tradeoff: pin an exact version vs. only block the known-bad range

Considered an alternative: instead of pinning to one exact npm version,
detect the specific known-bad range (11.9.0-11.12.x) at runtime and only
force an upgrade when the installed npm falls inside it, otherwise leave
whatever npm is already present untouched. That would need near-zero
maintenance and let npm keep auto-advancing on its own, same as today.

Went with the exact pin instead:

  • A range check only protects against this bug. It's a denylist, so
    it's reactive by construction -- it does nothing against a different,
    future npm regression we don't know about yet, which is exactly the
    failure mode that produced this incident (npm shipped a bug nobody
    anticipated). An exact pin is proactive: an unknown future regression
    in some other npm version simply never reaches us, because we never
    move off the pin until a human decides to.
  • A range check is still an open range with an exception carved out, so
    it keeps the same "silently drifts over time" property called out
    above for latest -- just with one specific hole patched. It doesn't
    change the underlying risk shape, only patches the one instance of it
    we happen to already know about.
  • The maintenance cost of the exact pin is lower than it looks: this
    action is consumed via the floating @v3 tag (not a pinned SHA), so
    bumping PRE_COMMIT_NPM_PIN is a one-time change in this repo (bump,
    re-verify, cut a v3.x.y release, move the v3 tag) -- every
    consumer repo picks it up on its next CI run with zero action on their
    end. That's the main cost this alternative was trying to avoid, and it
    turns out to already be close to zero.

wayneminwang and others added 5 commits September 17, 2026 16:29
Runner images can ship a pre-baked `pre-commit` on PATH that predates
4.6.2, which previously short-circuited the pip-install step entirely
(it only ran when PATH had no pre-commit at all). A pre-commit older
than 4.6.2 hits a known upstream bug installing `language: node` hooks
(e.g. mirrors-prettier/mirrors-eslint) on npm 11.x/12.x runners:
https://github.com/pre-commit/pre-commit/releases/tag/v4.6.2

Now checks the version of whatever pre-commit is found on PATH and
upgrades it via pip when it's below the minimum, instead of trusting
PATH unconditionally.
pre-commit 4.6.1 rewrote `language: node` hook installation to run
`npm install --allow-git=root -g git+file://<hook checkout> <deps>`,
replacing the previous `npm pack` tarball approach. When a node hook also
declares additional_dependencies (mirrors-prettier ships prettier@x.y.z,
mirrors-eslint ships eslint@x.y.z) the git+file:// package is no longer
the sole root package, so npm refuses to fetch it:

  npm error code EALLOWGIT
  npm error Fetching non-root packages of type "git" have been disabled

Any repo forced to rebuild its hook environments then fails lint, which
also blocks release pipelines gated on lint. Hook environments already
present in the S3 cache mask the failure, so repos break unpredictably:
the trigger is any change to .pre-commit-config.yaml (a renovate hook-rev
bump is enough) invalidating the cache key.

Verified against a cold cache with identical npm: 4.6.0 installs
mirrors-prettier successfully, 4.6.2 fails with EALLOWGIT.

Runner images can pre-bake an affected pre-commit on PATH, so this checks
the version of whatever is found rather than only whether one exists, and
installs the pinned version into a dedicated venv placed first on PATH
(plain `pip install` cannot be relied on to shadow /usr/local/bin).
- Fall back instead of aborting: composite steps run with `-e -o pipefail`,
  so an unreadable `pre-commit --version` previously killed the whole step
  rather than letting us install a known-good pre-commit.
- Default to installing the pin when the version cannot be parsed. An empty
  version string previously compared as "older than 4.6.1" and left an
  unknown pre-commit in place.
- Resolve the interpreter as python3 or python rather than assuming python3,
  matching the detection done in "Version files".
- Drop the redundant pip self-upgrade and trim the comment now that the
  forensics live in the PR description.
npm 11.9.0-11.12.x has a real upstream bug (npm/cli#9189, fixed in 11.13.0
via npm/cli#9206/#9237): --allow-git=root incorrectly rejects legitimate
root-level git installs. pre-commit's `language: node` installer
(rewritten in 4.6.1) hits exactly this path for any hook with
additional_dependencies, e.g. mirrors-prettier/mirrors-eslint, failing
with:

  npm error code EALLOWGIT
  npm error Fetching non-root packages of type "git" have been disabled

This replaces the earlier approach (pinning pre-commit below 4.6.1) with
a fix at the actual root cause -- npm, not pre-commit -- verified against
a real npm issue/fix rather than a hypothesis about pre-commit's installer.
pre-commit itself no longer needs any special handling.

Also ensures pre-commit treats our node/npm as a usable "system" install
(pre_commit/lang_base.py::exe_exists rejects executables resolved from
inside $HOME, which is where our self-hosted runners install node) --
without this, pre-commit ignores the npm we just pinned and downloads its
own via nodeenv instead.

The npm version is pinned to an exact, verified value rather than an open
range or "latest": bumping it is a deliberate action that should be
re-verified first, not something that silently drifts the way this bug
shipped in the first place.
nodeenv's system-mode shim search (create_environment()) checks the
literal name "nodejs" before "node", independent of pre-commit's own
$HOME gate. Our self-hosted runner image bakes in /usr/bin/nodejs, so
without a "nodejs" entry in this directory too, that lookup walks past
it (no match) and falls through to /usr/bin/nodejs -- bypassing the
npm pin entirely. Verified empirically: node_env-system resolved to
the image's v22.23.2 instead of the v24.15.0 set up above, before this
fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Release notes preview

Below is a preview of the release notes if your PR gets merged.


3.6.2 (2026-09-24)

Miscellaneous

  • deps: update actions/checkout action to v7 (cf9c6ed)
  • deps: update actions/setup-node action to v7 (7a32af4)
  • deps: update actions/setup-python action to v7 (2d520d0)
  • deps: update all non-major dependencies (7d1989b)
  • deps: update dependency open-turo/renovate-config to v1.21.1 (e3fa877)
  • deps: update dependency open-turo/renovate-config to v1.21.2 (1965fae)
  • deps: update dependency open-turo/renovate-config to v1.21.3 (3b6f1cd)
  • deps: update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.26.0 (d13582e)
  • deps: update pre-commit hook pre-commit/mirrors-eslint to v10.10.0 (1e1b46e)
  • deps: update pre-commit hook pre-commit/mirrors-eslint to v10.6.0 (dd9d887)
  • deps: update pre-commit hook pre-commit/mirrors-eslint to v10.7.0 (c880ee0)
  • deps: update pre-commit hook pre-commit/mirrors-eslint to v10.8.0 (f108cbe)
  • deps: update pre-commit hook pre-commit/mirrors-eslint to v10.8.1 (f86e0b0)
  • deps: update pre-commit hook pre-commit/mirrors-eslint to v10.9.0 (b54237b)
  • deps: update pre-commit hook pre-commit/mirrors-eslint to v10.9.1 (fc81178)
  • deps: update python docker tag to v3.14.7 (c983a97)

Bug Fixes

  • also symlink "nodejs", not just "node"/"npm" (3407154)
  • harden pre-commit version detection (3a9e473)
  • pin npm past the EALLOWGIT regression instead of pinning pre-commit (6b8ef1c), closes 9206/#9237
  • pin pre-commit below the broken node-hook installer (b91b0fd)
  • upgrade stale pre-installed pre-commit below v4.6.2 (79f1d09)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant