fix: pin npm past an EALLOWGIT regression that breaks node-hook installs - #210
Open
wayneminwang wants to merge 5 commits into
Open
wayneminwang wants to merge 5 commits into
wayneminwang wants to merge 5 commits into
Conversation
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>
4 tasks done
Release notes previewBelow is a preview of the release notes if your PR gets merged. 3.6.2 (2026-09-24)Miscellaneous
Bug Fixes
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
language: nodepre-commit hooks withadditional_dependencies(mirrors-prettierships
prettier@x.y.z, mirrors-eslint shipseslint@x.y.z) fail on npm11.9.0-11.12.x:
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=rootwasincorrectly 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.yamlinvalidating the cachekey.
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
Change
Setup node, symlink node/npm to a path outside$HOMEand add itto
$PATH. pre-commit only reuses an already-installed node/npm when itresolves outside
$HOME(pre_commit/lang_base.py::exe_exists); on ourself-hosted runners,
actions/setup-nodeinstalls under$HOME(
RUNNER_TOOL_CACHE), so without this pre-commit ignores it anddownloads its own via
nodeenvinstead -- whose bundled npm can land inthe same affected range.
nodeandnodejs(not justnode) to that same target.exe_existsonly gates whether pre-commit enters "system" mode, bychecking for
node/npm. Once in system mode,nodeenv's own shimsearch (
nodeenv.py::create_environment) does a separate,$HOME-blindlookup that checks the literal name
nodejsbeforenode. Ourself-hosted runner image bakes in
/usr/bin/nodejs(apt-installed,unrelated to anything
actions/setup-nodeprovisions), so without anodejsentry in our symlink dir too, that lookup walks past it (nomatch) and silently falls through to
/usr/bin/nodejs-- bypassing thenpm pin below entirely, with no error. Verified empirically on a real
runner: without this symlink, the resulting
node_env-systemresolvedto 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.2through that path. Pinned to an exact,verified version rather than an open range or
latest: bumping it lateris a deliberate action that should be re-verified first, the same way a
hook
rev:bump gets verified, not something that silently drifts backinto a broken npm the way this bug shipped in the first place.
main's existing behavior.
Net diff vs
mainis +43 lines in one new step; everything else isunchanged.
Test plan
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.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.
default_language_version: {node: system}as asymlink-free alternative: it hits the identical
nodejs-vs-nodelookup-order issue, with zero mitigation available (no file to add,
since it's config-only) -- confirmed empirically,
node_env-systemalso landed on the OS-baked node in that setup.
nodejssymlink:
node_env-system'snode --versionnow readsv24.15.0(matches the pinned setup), not the OS-baked
v22.23.2. Shim scriptcontent 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:
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.
it keeps the same "silently drifts over time" property called out
above for
latest-- just with one specific hole patched. It doesn'tchange the underlying risk shape, only patches the one instance of it
we happen to already know about.
action is consumed via the floating
@v3tag (not a pinned SHA), sobumping
PRE_COMMIT_NPM_PINis a one-time change in this repo (bump,re-verify, cut a
v3.x.yrelease, move thev3tag) -- everyconsumer 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.