Skip to content

feat(NO-TASK): Add a check-overrides action to find npm pins that went inert - #149

Open
aaronware wants to merge 1 commit into
mainfrom
feat/check-npm-overrides
Open

feat(NO-TASK): Add a check-overrides action to find npm pins that went inert#149
aaronware wants to merge 1 commit into
mainfrom
feat/check-npm-overrides

Conversation

@aaronware

Copy link
Copy Markdown
Contributor

Why

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. 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-zip override 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.

Verdict Meaning
🟢 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

The project itself is never mutated — all probing happens in a temp dir.

Two flags are load-bearing:

  • --package-lock-only — resolves the graph without downloading node_modules, keeping a 17-override sweep to seconds.
  • --ignore-scripts — subtler, and it cost me a debugging cycle. 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, doesn't enforce

fail_on_redundant exists 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's dependency-audit.yml: dev/build tooling is remediated opportunistically, never gated.

What ships

  • actions/check-overrides/ — composite action, bundled Node script resolved via github.action_path, following the update-readme pattern
  • .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 removable
  • README entries in both tables plus a caller example

Verification

Run against linchpin.com — four npm projects, 17 overrides each:

REDUNDANT     @babel/core     already resolves to 7.29.7 without the override
STILL NEEDED  adm-zip         would drop to 0.5.18 at 2 tree position(s)
DANGLING      uuid@8          not in the dependency tree at all

Root: 5 redundant, 1 dangling, 11 still needed. It correctly reports the just-added adm-zip pin as load-bearing, and will flip it to REDUNDANT on its own once @wordpress/scripts declares ^0.6 — which is the whole point.

Also verified:

  • Edge cases — no overrides block, no package.json, no package-lock.json, no args (exit 2)
  • fail_on_redundant=true exits 1; default exits 0 with findings present
  • Step summary, $GITHUB_OUTPUT counts, JSON artifact, and ::notice annotations all populate
  • Both YAML files parse; no line exceeds the 125-char yamllint limit from .ymllint.yml (the blocking CI check); no trailing whitespace or tabs

yamllint, actionlint, and zizmor aren't installed locally, so I checked the rules by hand — CI is the real gate on those.

Task: NO-TASK

🤖 Generated with Claude Code

…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>
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