Add flub check trustPolicy - audit lockfile against pnpm's no-downgrade trust policy#27191
Add flub check trustPolicy - audit lockfile against pnpm's no-downgrade trust policy#27191TommyBrosman wants to merge 11 commits intomicrosoft:mainfrom
flub check trustPolicy - audit lockfile against pnpm's no-downgrade trust policy#27191Conversation
the pnpm trust policy. Details: - Treats trust as binary (has-provenance vs no-provenance). pnpm has finer tiers (trusted publisher > provenance > none) but the registry's public abbreviated metadata only reliably exposes provenance. So this script may miss trusted-publisher -> provenance regressions, but won't produce false positives. - Uses publish time from the npm registry to determine "earlier" versions, matching pnpm's documented "based solely on publish date" wording. - Ignores prereleases when evaluating stable versions (matches pnpm v10.24+ behavior). - Respects trustPolicyExclude from pnpm-workspace.yaml. - Doesn't model trustPolicyIgnoreAfter (yet) This script is a little verbose as it manually parses the lockfile (see parseLockfilePackages) instead of calling a pnpm API. It also parses it as text instead of YAML.
|
Hi! Thank you for opening this PR. Want me to review it? Based on the diff (460 lines, 3 files), I've queued these reviewers:
How this works
|
…ions). - Added brackets around if statement bodies.
flub check trustPolicy - audit lockfile against pnpm's no-downgrade trust policy
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new flub check trustPolicy command to audit the repo's pnpm-lock.yaml against pnpm 10's trustPolicy: no-downgrade behavior and report the full set of violations.
Changes:
- Adds a new oclif command that enumerates lockfile entries, builds a temporary audit workspace, and iteratively reruns
pnpm installwhile excluding discovered trust-policy offenders. - Adds command documentation for
flub check trustPolicyand its flags/output modes. - Ignores the default scratch directory used by the new audit command.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| build-tools/packages/build-cli/src/commands/check/trustPolicy.ts | Implements the new trust-policy audit command, lockfile parsing, temp workspace generation, pnpm execution, and NDJSON parsing. |
| build-tools/packages/build-cli/docs/check.md | Documents the new flub check trustPolicy command and its CLI flags. |
| .gitignore | Ignores the default .trust-audit-temp/ scratch directory created by the command. |
|
On it! Starting review with: Correctness, Security, API Compatibility, Testing |
🔭 PR Review Fleet ReportNote This report is generated by an experimental AI review fleet and is provided as a beta feature. Findings are a starting point for discussion, not a gate. Use your own judgement. Verdict: ❌ Request Changes 0 Spicy, 3 Pungent, 4 Smelly Findings
|
- --lockfile-only when installing (better performance). - Linked bug in pnpm repo.
| */ | ||
|
|
||
| import { spawn } from "node:child_process"; | ||
| import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; |
There was a problem hiding this comment.
Prefer the /promises imports and async for file i/o.
| const child = spawn("pnpm", args, { | ||
| cwd, | ||
| shell: true, | ||
| stdio: ["ignore", "pipe", "pipe"], | ||
| env: { ...process.env, CI: "1" }, | ||
| }); |
There was a problem hiding this comment.
We use a wrapper called execa that I would recommend using here. If you're especially forward thinking you could adopt tiny-exec now, which is what I would like to replace execa with eventually. This code works, but it's really low level and a higher level API would be easier to read and maintain (IMO).
| default: false, | ||
| }), | ||
| tempDir: Flags.directory({ | ||
| description: "Scratch workspace directory (default: <repo-root>/.trust-audit-temp).", |
There was a problem hiding this comment.
Is there value to putting this in the repo boundary vs. a system-wide temp directory? E.g. /tmp on linux?
There was a problem hiding this comment.
I've been going back and forth on the OS temp dir approach. On one hand, it doesn't clutter up the repo. On the other hand, it's harder to find. I think using the OS temp dir makes sense if you never have to inspect the output. The script exposes a "keep" flag for the case where you might want to examine the output. I think I want to keep it in the repo unless it is significantly simpler not to.
| function slugify(name: string, version: string): string { | ||
| const safeName = name.replace(/[^\dA-Za-z]+/g, "-").replace(/^-+|-+$/g, ""); | ||
| const safeVersion = version.replace(/[^\dA-Za-z]+/g, "-").replace(/^-+|-+$/g, ""); | ||
| return `${safeName}-${safeVersion}`.toLowerCase(); | ||
| } |
There was a problem hiding this comment.
Honestly might be worth a tiny dep, otherwise just make sure it's got unit tests.
Co-authored-by: Copilot <copilot@github.com>
…rkspace root now comes from this.getBuildProject(this.flags.path).root instead of getContext().
- Added path flag — Flags.directory({ exists: true }) so the command can target a build project outside the current working directory.
- Adopted oclif's built-in JSON mode — static readonly enableJsonFlag = true;, removed the hand-rolled --json flag, and run() now returns a typed TrustPolicyAuditResult. Exit-on-failure gated on !this.jsonEnabled() so JSON consumers still get the structured payload.
- Restructured violation tracking as Map<name, Set<version>> — replaced the flat Set<string> of name@version tokens. The exclude-flag builder now reads versions directly from the map (no lastIndexOf("@") parsing); the || union construction itself was already in place from the previous commit.
- Violations returned as objects, not strings — TrustPolicyAuditResult.violations and extractTrustViolations both now return PinnedVersion[] ({name, version}). Text output formats ${name}@${version} inline.
- Removed the gross sort helper — extractTrustViolations returns events in pnpm's emission order (caller dedupes via the map). The single sort that matters (final output stability) is inlined where the result array is built.
- Misc cleanup — removed backticks from verbose log strings; added countViolations helper.
- .log -> .info
…red under any workspace, not just the repo root (since the audit now picks the most specific workspace and writes its temp dir there). Per-workspace audit. --path now selects the most specific workspace (longest-prefix match) inside the build project containing the path, rather than always auditing the build-project root. This lets the command target a single release group (e.g. build-tools, routerlicious) instead of the whole repo. Updated --path and --tempDir flag descriptions accordingly. pnpm list -r and the scratch dir are now anchored to the chosen workspace, not the repo root. Removed the lockfile existence check. getBuildProject() already validates the workspace exists; the manual pnpm-lock.yaml check was redundant. info() instead of log() for the human-readable summary block, so --quiet suppresses it. --json mode already suppresses both, so the INFO: prefix doesn't leak into structured output. output-determinism doc comment added at the violations-flatten block, explaining that the sort exists so printed/JSON output is stable across runs (Map/Set preserve insertion order, which depends on pnpm's emission order). Best-effort post-run cleanup. The finally block's rmSync(tempDir, ...) is now wrapped in try/catch — failures (Windows file locks, AV holds, etc.) become a this.warning instead of masking the audit's exit code or violations list. Doc comment explains the rationale. Pre-run cleanup hardened. Removed the existsSync TOCTOU check in writeAuditWorkspace (since rmSync with force: true already no-ops on missing paths). Wrapped the rmSync itself in try/catch with a clear actionable error: "Cannot prepare scratch workspace: ... Delete it manually or pass a different --tempDir." Dropped the now-unused existsSync import.
pnpm 10's trustPolicy: no-downgrade rejects installs where a package's publisher or signing key has changed since it was first trusted. Running it against our existing workspace only surfaces one violation at a time and isn't usable as a CI signal because we can't pre-populate the trusted-publishers store from a single run. This new command audits every
name@versionand reports the full violation set in one shot by generating a temporary workspace with a single large package.json containing every single { package, version } entry from the repo.Tracking bug in the pnpm repo: pnpm/pnpm#10622
Description
flub check trustPolicyin the build-cli package, with--json,--keep,--tempDir, and--verboseflags.name@versionfrom both thepackagesandsnapshotssections, skipping URL, tarball, and git refs (which trust policy doesn't apply to).pnpm install --trust-policy no-downgrade --reporter ndjson, parses pnpm's NDJSON stdout forERR_PNPM_TRUST_DOWNGRADEevents, adds each offender to--trust-policy-exclude, and re-runs until pnpm exits cleanly or stops surfacing new violations.package.{name,version}so any pnpm contract change surfaces loudly instead of silently skipping violations.tempDir,exitCode,iterations,elapsedSec,totalUniqueDependencies, and the sortedviolationslist under--json.Sample run on this branch: 8 iterations / 2606 unique dependencies / ~82s, surfacing 7 violations (
@langchain/core@0.3.80,@octokit/endpoint@9.0.6,axios@0.30.3,chokidar@4.0.3,detect-port@1.6.1,semver@5.7.2,semver@6.3.1).