feat(build-cli): add flub check trustPolicy - audit lockfile against pnpm's no-downgrade trust policy#27191
Conversation
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, 2 Smelly Findings
|
- --lockfile-only when installing (better performance). - Linked bug in pnpm repo.
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.
- Replace the hand-rolled `node:child_process` spawn helper with `execa`, matching the rest of build-cli. Because execa resolves `pnpm.cmd` natively without a shell, we no longer need `shell: true` and can drop the workaround that double-quoted `--trust-policy-exclude name@v1||v2` values to keep cmd.exe from interpreting `||`. - Replace the local `slugify(name, version)` helper plus `usedSlugs` collision map with a single `GithubSlugger` instance, which handles filesystem-safe slugging and duplicate suffixing internally. Both deps were already in the build-cli package. - No behavior change: `--path build-tools --json` produces the same 5 violations in 6 iterations as before.
18f7bf4 to
b16fb84
Compare
| } as const; | ||
|
|
||
| public async run(): Promise<TrustPolicyAuditResult> { | ||
| const searchPath = path.resolve(this.flags.path ?? process.cwd()); |
There was a problem hiding this comment.
nit: you can give the flag a default value of process.cwd() in the flag declaration above - then this defensive code can go away.
| let workspaceDir: string | undefined; | ||
| for (const ws of buildProject.workspaces.values()) { | ||
| const dir = path.resolve(ws.directory); | ||
| if ( | ||
| (searchPath === dir || searchPath.startsWith(`${dir}${path.sep}`)) && | ||
| (workspaceDir === undefined || dir.length > workspaceDir.length) | ||
| ) { | ||
| workspaceDir = dir; | ||
| } | ||
| } | ||
| if (workspaceDir === undefined) { | ||
| this.error(`No workspace in build project ${buildProject.root} contains ${searchPath}.`); | ||
| } |
There was a problem hiding this comment.
Hmmm, I see what you mean. You could consider taking a workspace name or release group name as a flag, but I think this is fine. Easy enough to refactor later if needed, and the comment is clear.
flub check trustPolicy - audit lockfile against pnpm's no-downgrade trust policyflub check trustPolicy - audit lockfile against pnpm's no-downgrade trust policy
|
Do a full build-tools local build and commit the results - that should address the build failures. |
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).