Skip to content

ci(security): reusable pnpm-overrides sanity check (pnpm 10 silently ignores package.json overrides) - #9

Merged
Aurora Capital (auroracapital) merged 2 commits into
mainfrom
ci/pnpm-overrides-sanity
Aug 12, 2026
Merged

ci(security): reusable pnpm-overrides sanity check (pnpm 10 silently ignores package.json overrides)#9
Aurora Capital (auroracapital) merged 2 commits into
mainfrom
ci/pnpm-overrides-sanity

Conversation

@auroracapital

@auroracapital Aurora Capital (auroracapital) commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Why

pnpm 10 no longer reads the pnpm field in package.json. It prints
The pnpm field in package.json is no longer read by pnpm and then ignores
pnpm.overrides and pnpm.patchedDependencies.

The failure is silent and security-relevant: a security override declared there
under pnpm 10 looks committed and reviewed, resolves nothing, and the advisory
stays open.

That is exactly how critical tar GHSA-23hp-3jrh-7fpw survived repeated
dependency bumps in hypest-intelligence-hub and hypest-dashboard — both had
tar pinned ">=7.5.19" while the lockfile happily kept resolving 7.5.1.

carrier.llc is on pnpm@10.33.0 and still carries a pnpm field, so this is
not a hypest-only footgun.

How

Reusable workflow with two independent checks:

  1. Dead-config check — fails when packageManager pins pnpm ≥ 10 while
    package.json still declares overrides, patchedDependencies,
    peerDependencyRules or packageExtensions. Error names the exact dead keys
    and where to move them.
  2. Effectiveness check — parses the overrides: block in
    pnpm-workspace.yaml and asserts each entry is actually reflected in
    pnpm-lock.yaml. Catches the other half of the problem: override declared in
    the right place but lockfile never re-resolved.

Handles scoped selectors (tailwindcss>nanoid verifies nanoid), skips
non-pnpm repos, and no-ops cleanly when the files are absent.

How tested

Both directions, before commit:

  • positive — real hypest-intelligence-hub lockfile: tar7.5.22
    against override >=7.5.19, and tailwindcss>nanoid3.3.11 against
    3.3.7, both parsed correctly
  • negative control — synthetic workspace pinning >=7.5.19 against a
    lockfile holding tar@7.5.1 → checker correctly fails

The negative control matters: it proves this is not a checker that always passes.

Not validated

  • Not yet wired into consumer repos. Adoption is a follow-up per repo (start
    with carrier.llc, which is pnpm 10 with a live pnpm field).
  • Override parser is regex-based (no YAML dep). It handles the shapes in use
    here; an exotic anchored/multi-doc workspace file would need a real parser.

Summary by CodeRabbit

  • New Features

    • Added a reusable workflow for validating pnpm configuration and dependency overrides.
    • Supports running checks from a specified project directory.
  • Tests

    • Verifies resolved dependency versions against exact, minimum, caret, and tilde constraints.
    • Detects unsupported or missing overrides and configuration issues, providing remediation guidance.
  • Chores

    • Automatically skips non-pnpm repositories or projects missing required package files.

pnpm 10 no longer reads the `pnpm` field from package.json. It warns
"The pnpm field in package.json is no longer read by pnpm" and then IGNORES
pnpm.overrides / pnpm.patchedDependencies.

The failure mode is silent and security-relevant: a security override declared
in package.json under pnpm 10 looks committed and reviewed, resolves nothing,
and the advisory stays open. That is exactly how critical tar
GHSA-23hp-3jrh-7fpw survived repeated dependency bumps in hypest-intelligence-hub
and hypest-dashboard (found 2026-08-12, both had tar 7.5.1 pinned "to >=7.5.19").

This reusable workflow:
1. fails when packageManager pins pnpm >= 10 while package.json still declares
   overrides / patchedDependencies / peerDependencyRules / packageExtensions,
   naming the exact dead keys and where to move them;
2. parses the overrides block in pnpm-workspace.yaml and asserts each one is
   actually reflected in pnpm-lock.yaml, so a declared-but-unapplied override
   (stale lockfile) also fails.

Verified both directions before commit:
- positive: real hypest-intelligence-hub lockfile -> tar 7.5.22 vs override
  >=7.5.19 and tailwindcss>nanoid 3.3.11 vs 3.3.7 both read correctly
- negative control: synthetic workspace pinning >=7.5.19 against a lockfile
  holding tar@7.5.1 -> checker correctly fails (it is not a no-op that always
  passes)
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 87fb394f-ff80-4a68-b62e-be7a4f87a224

📥 Commits

Reviewing files that changed from the base of the PR and between 2d3ac90 and b536ead.

📒 Files selected for processing (1)
  • workflow-templates/pnpm-overrides-sanity.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • workflow-templates/pnpm-overrides-sanity.yml

📝 Walkthrough

Walkthrough

Adds a reusable GitHub Actions workflow with a configurable working directory. The workflow validates pnpm configuration and verifies workspace overrides against lockfile versions.

Changes

pnpm override validation

Layer / File(s) Summary
Workflow contract and repository setup
workflow-templates/pnpm-overrides-sanity.yml
Defines the reusable workflow input, read-only contents permission, checkout step, and pnpm job metadata.
pnpm configuration validation
workflow-templates/pnpm-overrides-sanity.yml
Detects pnpm repositories and pinned pnpm versions. For pnpm 10+, it fails when ignored configuration remains in package.json.
Workspace override verification
workflow-templates/pnpm-overrides-sanity.yml
Parses workspace overrides, compares exact, >=, caret, and tilde constraints with lockfile versions, and reports failures or success. Skips repositories without the required files.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant Repository
  participant OverrideValidator
  GitHubActions->>Repository: Check out repository
  GitHubActions->>Repository: Read package.json and pnpm-workspace.yaml
  GitHubActions->>OverrideValidator: Parse overrides and pnpm-lock.yaml
  OverrideValidator->>Repository: Resolve override targets
  OverrideValidator->>GitHubActions: Report validation result
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the reusable CI workflow and its main purpose: detecting ineffective pnpm overrides in pnpm 10.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/pnpm-overrides-sanity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@workflow-templates/pnpm-overrides-sanity.yml`:
- Around line 128-138: Update the override-validation logic around the visible
spec parsing and comparison so every unsupported override form—including exact,
caret, tilde, alias, catalog, and removal specifications—is recorded as an
explicit failure rather than treated as valid. Preserve the existing version
comparison for supported >= specifications, and append a clear
unsupported-specification error for all other non-evaluable values.
- Around line 119-124: The scoped override validation around target extraction
must verify the direct parent-to-child dependency edge rather than searching for
the child as a top-level key. Update the lockfile parsing used by the sanity
check to handle quoted scoped package keys, locate the specified parent’s
dependency data, and fail the check when the selected edge cannot be confirmed
instead of continuing as optional.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b510d93-6cb4-4fe3-9476-558c3e318090

📥 Commits

Reviewing files that changed from the base of the PR and between a6a1f9e and 2d3ac90.

📒 Files selected for processing (1)
  • workflow-templates/pnpm-overrides-sanity.yml

Comment thread workflow-templates/pnpm-overrides-sanity.yml Outdated
Comment thread workflow-templates/pnpm-overrides-sanity.yml Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full review at 2d3ac905. The idea is right and the org needs this gate, but as written it reports success without checking the only override that exists in the org, so I would not merge it yet.

Both open threads are real. I verified each by executing the patterns rather than reading them, and replied inline with the evidence.

The more serious one is the version comparison. re.match(r">=\s*([0-9][0-9.]*)", spec) is the only check, and a non-match falls off the end of the loop — nothing appended to failures, nothing logged. So every spec shape other than >= passes silently: bare pins, carets, tildes, npm: aliases, catalog:, file:.

That is not hypothetical. healify-web is the only repo in the org with a pnpm overrides block, and it holds exactly one entry, "tailwindcss>nanoid": 3.3.7. An exact pin. The step finds nanoid in the lockfile, prints the resolved version, skips the comparison, and prints OK: all declared overrides are reflected in pnpm-lock.yaml. Today this workflow would go green having verified nothing on its only consumer — and an exact pin that silently failed to apply is precisely what a supply-chain override guard is for. Pinned-for-security overrides are usually exact or caret, not >=, so I would treat those as the primary shape.

The second thread is narrower but the same failure philosophy. The lockfile lookup requires the two-space indent to be followed immediately by the package name, but pnpm-lock v9 quotes scoped keys ( '@babel/core@7.24.0':), so scoped targets never match, land on the continue branch, and print "may be optional". No scoped override exists in the org right now, so this is latent rather than live — but onlyBuiltDependencies in that same file is mostly scoped packages, so the first scoped override anyone adds will be silently unverified. Worth fixing while it is cheap. (One correction to the bot's analysis: parent selectors are fine — tailwindcss>nanoid reduces to the unquoted key nanoid and matches. The break is specifically quoted scoped keys, including a scoped child in a parent selector.)

Underneath both is one design decision I would change: "target not found" and "target found and verified" both produce a green, silent pass. A guard whose miss path is indistinguishable from its success path will eventually be trusted for something it never did. Printing an explicit skip line for each unverifiable entry — and considering a non-zero exit under strict when nothing could be checked — would make the log honest about coverage.

Worth noting the contrast with #10 in this same repo, which gets this right: its test harness raises AssertionError if the named workflow step is missing, so a rename fails loudly instead of quietly testing nothing. Same instinct applied here would resolve most of the above.

Everything else about the PR is fine — the overrides parser handles quoting and comments, the early exits for a missing or empty block are sensible, and the remediation hint (pnpm install --lockfile-only) is the right one.

Do not merge as it stands. Fix the spec comparison so exact pins and carets are actually compared, and the gate becomes worth having; the scoped-key fix and an explicit skip line can ride along. Happy to re-review whichever head comes out of it.

… scoped lockfile keys, fail when nothing verifiable
@auroracapital

Copy link
Copy Markdown
Contributor Author

Pushed b536ead addressing both threads.

  • Spec comparison now handles exact pins, caret and tilde in addition to >=; anything non-comparable (npm: alias, catalog:, file:, tags) is an explicit failure instead of a silent pass. tailwindcss>nanoid: 3.3.7 in healify-web is now actually compared.
  • Lockfile lookup accepts quoted scoped keys ('@babel/core@7.24.0':), including a scoped child in a parent selector.
  • Miss path is no longer indistinguishable from success: unresolvable entries print an explicit SKIP ... override NOT verified line, and if nothing at all could be verified the step exits non-zero.

Verified by extracting the step's python and running it against 13 synthetic workspace/lockfile pairs (exact/caret/tilde/gte satisfied and violated, quoted scoped key, scoped child selector, npm alias, catalog, all-unverifiable) — all 13 matched expected exit codes. Ready for re-review.

@auroracapital
Aurora Capital (auroracapital) merged commit 1c9e3b9 into main Aug 12, 2026
6 checks passed
@auroracapital
Aurora Capital (auroracapital) deleted the ci/pnpm-overrides-sanity branch August 12, 2026 17:47
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.

2 participants