feat(NO-TASK): Add a check-overrides action to find npm pins that went inert - #149
Open
aaronware wants to merge 1 commit into
Open
feat(NO-TASK): Add a check-overrides action to find npm pins that went inert#149aaronware wants to merge 1 commit into
aaronware wants to merge 1 commit into
Conversation
…t inert Transitive advisories in build tooling get remediated with npm overrides, because the dependents -- @wordpress/scripts and friends -- often keep declaring a vulnerable range long after a fix ships upstream. That works, but the pins are write-only in practice: nothing tells you the day upstream catches up and an override stops doing anything. Projects end up carrying a dozen pins where two are load-bearing, and every one of them is a silent veto on a dependency the rest of the tree wants to move. This probes each override independently. It is removed from a scratch copy of the manifest, the graph is re-resolved with --package-lock-only, and the result is compared against the pin: - STILL NEEDED removing it would drop the package below the pinned version - REDUNDANT the tree already satisfies the pin without it - DANGLING the package has left the dependency tree entirely Two flags are load-bearing in the probe. --package-lock-only resolves the graph without downloading node_modules, which keeps a 17-override sweep to seconds. --ignore-scripts matters more subtly: the scratch copy has no node_modules, so a prepare hook (husky being the usual one) fails the install and masks every result as an error. Reports and never enforces by default. An override held as a security floor is legitimately kept even once the tree satisfies it, so removal is a judgement call for a human -- fail_on_redundant exists but is off. Ships as a composite action plus a reusable workflow. The workflow can keep a single tracking issue in sync rather than opening a fresh one every month for the same unchanged set of pins, and closes it when nothing is removable. Verified against linchpin.com, which has four npm projects and 17 overrides each: 5 redundant and 1 dangling in the root, and it correctly reports the adm-zip pin added for GHSA-xcpc-8h2w-3j85 as still load-bearing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Why
Transitive advisories in build tooling get remediated with npm
overrides, because the dependents —@wordpress/scriptsand friends — often keep declaring a vulnerable range long after a fix ships upstream.That works. The problem is the pins are write-only in practice: nothing tells you the day upstream catches up and an override stops doing anything. Projects accumulate a dozen pins where two are load-bearing, and every one of them is a silent veto on a dependency the rest of the tree wants to move.
This came out of linchpin/linchpin.com#957, where an
adm-zipoverride was added for GHSA-xcpc-8h2w-3j85 — and the obvious follow-up question was "how will we ever know when to take it out again?"How it works
Each override is probed independently: removed from a scratch copy of the manifest, graph re-resolved with
--package-lock-only, result compared against the pin.STILL NEEDEDREDUNDANTDANGLINGThe project itself is never mutated — all probing happens in a temp dir.
Two flags are load-bearing:
--package-lock-only— resolves the graph without downloadingnode_modules, keeping a 17-override sweep to seconds.--ignore-scripts— subtler, and it cost me a debugging cycle. The scratch copy has nonode_modules, so apreparehook (husky being the usual one) fails the install and masks every result as an error.Reports, doesn't enforce
fail_on_redundantexists but defaults to false. A redundant override may still be wanted as a security floor — it guards against a future downgrade even when the tree currently satisfies it. Removal is a judgement call for a human, and the report says so inline.This matches the two-tier policy already in
linchpin.com'sdependency-audit.yml: dev/build tooling is remediated opportunistically, never gated.What ships
actions/check-overrides/— composite action, bundled Node script resolved viagithub.action_path, following theupdate-readmepattern.github/workflows/check-overrides.yml— reusable workflow (workflow_call), which can keep one tracking issue in sync rather than opening a fresh one every month for the same unchanged pins, and closes it when nothing is removableVerification
Run against
linchpin.com— four npm projects, 17 overrides each:Root: 5 redundant, 1 dangling, 11 still needed. It correctly reports the just-added
adm-zippin as load-bearing, and will flip it toREDUNDANTon its own once@wordpress/scriptsdeclares^0.6— which is the whole point.Also verified:
overridesblock, nopackage.json, nopackage-lock.json, no args (exit 2)fail_on_redundant=trueexits 1; default exits 0 with findings present$GITHUB_OUTPUTcounts, JSON artifact, and::noticeannotations all populateyamllintlimit from.ymllint.yml(the blocking CI check); no trailing whitespace or tabsyamllint,actionlint, andzizmoraren't installed locally, so I checked the rules by hand — CI is the real gate on those.Task: NO-TASK
🤖 Generated with Claude Code