Skip to content

Add a pin-override action for advisories Dependabot cannot fix - #29

Merged
lucas-d-ramos merged 1 commit into
mainfrom
feat/pin-override-action
Sep 9, 2026
Merged

Add a pin-override action for advisories Dependabot cannot fix#29
lucas-d-ramos merged 1 commit into
mainfrom
feat/pin-override-action

Conversation

@lucas-d-ramos

Copy link
Copy Markdown
Contributor

Why

Dependabot fixes a transitive dependency with yarn up -R <pkg>@<fixed>, which operates on a package name across the whole tree. One parent pinning that name to an exact version blocks it, even when other copies could move freely. Strapi does this everywhere (@strapi/upload requires sharp@0.35.3, not ^0.35.3).

Two failure shapes, and the quiet one is worse:

  • A dedicated security job fails with security_update_not_possible. At least visible.
  • The daily version-update job logs No update possible for qs 6.15.3 and reports success. A qs advisory sat open for a week behind a green run history.

The fix a human writes is identical every time: a range entry in resolutions. This action writes those, and nothing else.

Three rules that keep it honest

Inside the compatibility line only, mirroring the no-majors policy — and for 0.x the minor is the line. esbuild ships 0.21.5 and 0.28.1 in one tree with a fix in 0.25.0; keyed on the major alone those look like one line and the "fix" is a breaking bump.

Never a direct dependency. Dependabot edits package.json fine on its own; shadowing a declared dependency just creates a second pin to maintain.

Verified by re-running the audit, not by version arithmetic. Anything that does not actually clear its advisory is reverted. A pin that fixes nothing is worse than no pin, because someone has to explain it later.

Scoped vs unscoped is read from the lockfile, not guessed

Case Key Why
One compatibility line qs ~20 @strapi/* parents deduped onto one entry. A scoped @strapi/admin/qs forks a second entry and leaves 6.15.3 for the other nineteen.
Several lines, one single-version parent cheerio/undici undici 7.28.0 comes only from cheerio; 6.28.0 is Strapi's exact pin and is not in the advisory. No parent version in the key, so it survives a cheerio bump.
Parent itself at several versions refused, reason named brace-expansion is pulled by minimatch 3.1.5, 9.0.9 and 10.2.5 at once. The only correct key embeds a parent version that rots on the next bump. Human's call.

Measured against wemove.eu/backend

  tar: 7.5.19 -> ^7.5.22  [high] GHSA-R292-9MHP-454M
  cheerio/undici: 7.28.0 -> ^7.29.1  [high] 5 advisories
  (human) brace-expansion 1.1.16: minimatch is itself in the tree at 2 versions
  (human) vite 5.4.21: no fix inside the 5.x line
  ...
tar -> ^7.5.22 (high, 1 advisory); cheerio/undici -> ^7.29.1 (high, 5 advisories) | 11 need a human

Two resolutions covering six alerts; Strapi's undici@npm:6.28.0 left untouched in the lockfile, verified.

Verification

  • 40 unit tests, no network (published is monkeypatched). Wired into the ci gate next to python-pins.
  • Covers the 0.x line rule, every comparator shape in the audit feed, lockfile folding of multi-descriptor entries, all three descriptor cases, the resolutions round-trip, and deprecation notices being dropped rather than pinned.
  • End-to-end apply exercised on a scratch copy of wemove.eu/backend: both entries written, lockfile moved, re-audit clean, 6.x pin intact.

Not wired to anything yet

This is the action only. The scheduled workflow that calls it and opens the PR ships as a template in dependency-policy, with thin callers per repo, so scheduling stays per-repo the way yarn-bump.yml does.

🤖 Generated with Claude Code

Dependabot fixes a transitive dependency with `yarn up -R <pkg>@<fixed>`, which
operates on a package NAME across the whole tree. A single parent pinning that
name to an exact version blocks it, even when other copies of the same package
could move freely. Strapi does this everywhere: @strapi/upload requires
sharp@0.35.3, not ^0.35.3.

The failure takes two shapes, and the quiet one is worse. A dedicated security
job fails the run with security_update_not_possible, which is at least visible.
The daily version-update job instead logs "No update possible for qs 6.15.3"
and reports SUCCESS — a qs advisory sat open for a week that way behind a green
run history. Either way the fix a human writes is identical every time: a range
entry in `resolutions` that moves the shared lockfile entry. This writes those.

Three rules keep it honest:

* Inside the compatibility line only, mirroring the org's no-majors policy —
  and for 0.x the MINOR is the line. esbuild ships 0.21.5 and 0.28.1 in one
  tree with a fix in 0.25.0; keyed on the major alone those look like one line
  and the "fix" would be a breaking bump.
* Never a direct dependency. Dependabot edits package.json perfectly well on
  its own, and shadowing a declared dependency creates a second pin to maintain.
* Every entry is verified by re-running the audit rather than trusting version
  arithmetic. Anything that does not actually clear its advisory is reverted; a
  pin that fixes nothing is worse than no pin, because somebody has to explain
  it later.

Scoped vs unscoped is decided from the lockfile, not guessed. Unscoped when the
package has one compatibility line: qs was pinned exactly by ~20 @strapi/*
packages that Yarn had deduped onto ONE entry, so a scoped @strapi/admin/qs
would have forked a second entry and left the vulnerable version for the other
nineteen. Scoped to the parent NAME (never its version, so it survives a bump)
when several lines coexist and one single-version parent pulls the vulnerable
one: undici 7.28.0 comes only from cheerio, while 6.28.0 is Strapi's exact pin
and is not even in the advisory. Refused, with the reason named, when the parent
itself has several versions — brace-expansion is pulled by minimatch 3.1.5,
9.0.9 and 10.2.5 at once, and the only correct key embeds a parent version that
rots on the next minimatch bump. That is a human's call.

Measured against wemove.eu/backend: two resolutions covering six alerts
(tar -> ^7.5.22, cheerio/undici -> ^7.29.1 clearing five advisories), with
eleven named for a human and Strapi's undici 6.28.0 pin left untouched.

40 unit tests, no network. Wired into the `ci` gate alongside python-pins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lucas-d-ramos
lucas-d-ramos merged commit e8f4d22 into main Sep 9, 2026
2 checks passed
@lucas-d-ramos
lucas-d-ramos deleted the feat/pin-override-action branch September 9, 2026 13:46
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