Skip to content

fix: scope workspace fix commands to the declaring member - #975

Merged
sonukapoor merged 3 commits into
mainfrom
bugfix/issue-972-pnpm-workspace-filter-scoping
Aug 13, 2026
Merged

fix: scope workspace fix commands to the declaring member#975
sonukapoor merged 3 commits into
mainfrom
bugfix/issue-972-pnpm-workspace-filter-scoping

Conversation

@sonukapoor

Copy link
Copy Markdown
Collaborator

Closes #972

Parent-upgrade fix commands were emitted flat at the workspace root (e.g. pnpm add concurrently@9.2.4, npm install eslint@9.0.0) instead of scoped to the member that declares the package. In a monorepo, running the flat command from the root installs into the wrong place and doesn't fix the member where the vulnerable transitive dep actually lives. This affected all four package managers, and it surfaced from a real enterprise adopter's pnpm + Angular/NestJS workspace.

What changed

All in buildSuggestedFixCommandPlan (src/remediation/fix-commands.ts):

  • The three parent-upgrade target sites now attach the parent's workspace membership, so the command builder emits pnpm --filter ./<member> add, npm install -w <member>, yarn workspace <member> add, or bun add --filter <member>.
  • buildPnpmLockfileRefreshCommand uses pnpm's workspace selector pnpm --filter ./<member> instead of the -C <member> directory shim.
  • pnpm install commands put --filter before add (it's a CLI-level selector), fixing the flag order.
  • findSuggestedCommandForFinding (which feeds the per-finding runnableFixCommand used by JSON, the HTML per-finding view, and CycloneDX) now prefers the workspace-scoped command over a target's flat explicit command, while leaving already-scoped parent-update commands untouched.
  • The raw per-target command field (exposed in JSON and embedded in the HTML report data) is normalized to the scoped form.

SARIF is unaffected - it emits human-readable action text ("Upgrade X to Y"), not an install command.

Verification

Confirmed before/after on real runnable workspace examples for npm (examples/workspace), pnpm and yarn (two new example fixtures). bun's parent-upgrade path is not reachable through a real scan today (its parser doesn't attribute parent chains for transitive findings - separate limitation), so its scoping is covered by a unit test. Every command-bearing output (terminal, JSON, HTML report, CycloneDX) was checked to contain zero flat commands.

In-house fix. Not a contributor pickup (remediation/fix-command internals).

Parent-upgrade fix commands were emitted flat at the workspace root
(e.g. `pnpm add concurrently@9.2.4`, `npm install eslint@9.0.0`) instead
of scoped to the member that declares the package. Running the flat
command in a workspace root installs into the wrong place. Affected all
four package managers.

Root causes, all in buildSuggestedFixCommandPlan:
- The three parent-upgrade target sites (upgrade-parent-to-version,
  recommendedParentUpgrade, and the chain-resolution branch) never
  attached the parent's workspace membership, so the command builder
  fell back to a flat root install.
- buildPnpmLockfileRefreshCommand emitted `pnpm -C <member>` rather than
  pnpm's workspace selector `pnpm --filter ./<member>`.
- The pnpm install command placed --filter after `add`; the selector is
  a CLI-level flag that must precede the command (`pnpm --filter ./m add`).
- findSuggestedCommandForFinding (which feeds the per-finding
  runnableFixCommand used by JSON, the HTML per-finding view, and
  CycloneDX) returned a target's flat explicit command before checking
  workspaces, so those surfaces stayed flat even after the plan command
  was scoped.
- The raw per-target `command` field (exposed in JSON and embedded in the
  HTML report data) was left flat; it is now normalized to the scoped
  form for install targets.

Now every command-bearing output (terminal, JSON, HTML report, CycloneDX)
scopes parent-upgrade commands to the correct member. SARIF is unaffected
(it emits human-readable action text, not an install command).
Adds coverage for the parent-upgrade and within-range-refresh scoping
across npm (-w), pnpm (--filter ./), yarn (yarn workspace <name> add),
and bun (--filter). Includes the three parent-upgrade code paths, the
per-finding runnableFixCommand, and the raw per-target command field, so
no flat command survives in any command-bearing output.
Real runnable monorepo examples where a member's dev dependency
(concurrently@9.2.1) transitively pulls vulnerable shell-quote@1.8.3,
fixable only by upgrading the parent. Used to verify parent-upgrade fix
commands are scoped to the declaring workspace member end to end.
@sonukapoor
sonukapoor merged commit 4b7ccc1 into main Aug 13, 2026
6 checks passed
@sonukapoor
sonukapoor deleted the bugfix/issue-972-pnpm-workspace-filter-scoping branch August 13, 2026 12:09
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.

fix: pnpm/workspace fix commands not correctly --filter-scoped (flat-root parent-upgrade installs and -C within-range refreshes)

1 participant