Port the `pnpm bugs` command to pacquet, following the structure of the TypeScript handler at `pnpm11/deps/inspection/commands/src/bugs/index.ts` and matching its error codes (`ERR_PNPM_NO_BUGS_URL`, `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND`). The command supports local manifest lookup and registry lookup by package name, with repository URL normalization for GitHub/GitLab/Bitbucket shorthand, hosted git URLs, and self-hosted git servers. Unit tests cover all URL derivation branches (36 tests). Integration tests cover the CLI entry points with local manifests and a mocked registry (9 tests).
Related to pnpm/pnpm#11633.
On a cache miss the proxy buffered the whole tarball into the cache, verified
its SRI, and only then streamed the finished file — a head-of-line stall on
every cold fetch: the client waited for the full upstream fetch plus a verify
pass before the first byte, with no overlap. Buffer-then-verify is the wrong
model for a streaming proxy.
Stream the upstream response to the client while teeing it into the cache and
hashing it (stream_verified_to_cache); promote the cache entry only once the
full body matches dist.integrity. A mismatched/truncated/oversize body is never
cached and the client re-verifies what it receives, so a bad upstream tarball
can't poison a later client — it just can't be turned into a BAD_GATEWAY, since
SRI is only known after the body is already streamed.
Converge the namespaced /~<uplink>/ route onto the same model (the integrity is
known from the packument either way), removing the now-dead uplink integrity
sidecar machinery. Stop forwarding the upstream Content-Length on streamed
responses (attacker-controlled, unverifiable before streaming; kept only for
the internal early oversize check). Add a cold-pnpr-cache benchmark scenario
that actually exercises the cold download/serve path (every other scenario
warms the mock); it runs frozen so resolution variance doesn't mask the
serve-path delta.
The measured speedup on that scenario is modest and within noise (this fixture
is small tarballs over a latency-bound link); the change is a correctness fix,
and the benchmark confirms no regression.
Out of scope: packuments with no usable dist.integrity (pnpm/pnpm#12727).
Make the pacquet Rust port installable from npm as pnpm v12, published with
equal content under two names — `pnpm` and `@pnpm/exe` — on the `next-12`
dist-tag (never `latest`, which keeps serving the production TypeScript pnpm
v11). First release is 12.0.0-alpha.0.
CLI presents as pnpm: clap name/bin_name are `pnpm`; PACQUET_VERSION holds the
pnpm 12.x version; the default User-Agent is the plain `pnpm/<version>` form;
the reporter footer reads `Done in ... using pnpm v<version>`; the `pnpm link`
/ `pnpm patch` / `pnpm patch-commit` hints now name pnpm (parity fixes); and
launched as `pnpx`/`pnx`, the binary injects the `dlx` subcommand itself by
detecting its current_exe name (porting pnpm's buildArgv).
npm packaging mirrors how `@pnpm/exe` ships pnpm: both the `pnpm` and `@pnpm/exe`
wrappers ship root-level placeholder bins (pnpm/pn/pnpx/pnx) plus an install.js
preinstall that hardlinks the host's native binary over the placeholder (no Node
launcher). Native binaries publish as `@pnpm/exe.<platform>-<arch>[-musl]` (file
`pnpm` inside). `pacquet`/`@pnpm/pacquet` are no longer published.
installPnpm initializes v12 exactly like `@pnpm/exe`: linkExePlatformBinary takes
the wrapper name and resolves the native binary via require, and PNPM_ALLOW_BUILDS
includes both names so the GVS hash is per-platform. resolvePackageManagerIntegrities
already resolves both `pnpm` and `@pnpm/exe`, so publishing `@pnpm/exe@12` makes
self-update / version-switch work with no v12-specific logic. From v12 the
install always uses the `pnpm` package (even from a `@pnpm/exe` build), via
pnpmPackageNameToInstall().
The configDependencies install-engine path still installs the published
`pacquet`/`@pnpm/pacquet` releases (which ship `@pacquet/<target>` binaries) and
is left untouched.
Add `pacquet ping`, ported from pnpm's ping command.
`ping` GETs `<registry>-/ping?write=true` and prints pnpm's PING/PONG
report, plus a pretty-printed `PONG <details>` line when the registry
returns a non-empty JSON body. It resolves the registry from `--registry`
or the configured default, adds a trailing slash before joining so a
registry path prefix is preserved (matching pnpm's
`new URL('./-/ping', ...)`), sends the optional `Authorization` header when
one is configured (unlike whoami it does not require auth), and issues a
single attempt with no retries (pnpm's `retry: { retries: 0 }`). Timing
wraps the request and body read, matching pnpm's `Date.now()` measurement.
Both a transport failure and a non-success status map to the single
`ERR_PNPM_PING_ERROR` code with pnpm's "Failed to reach registry" message;
the transport error message is credential-redacted before it reaches
stderr / CI logs, the same precaution as whoami. Unlike whoami's username
(printed raw, hence sanitized), ping's JSON details are re-serialized
through serde_json, which escapes control characters, so no raw registry
bytes reach the terminal.
Extract the registry-client builder duplicated from whoami into a shared
`cli_args::registry_client::build_registry_client` and switch whoami to it.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* ci: add Oz cloud triage workflow for new issues
Wire Warp's Oz "cloud factory" issue-triage stage onto pnpm/pnpm as a
GitHub-triggered cloud agent (part of the pnpm x Warp OSS partnership).
- .github/workflows/triage-issues.yml runs on issues:[opened], skips
Bot-authored issues, and invokes warpdotdev/oz-agent-action@v1 with the
triage skill. Minimal permissions: contents:read, issues:write.
- .agents/skills/triage/SKILL.md adapts the canonical triage rubric to pnpm:
it maps the four readiness states onto existing pnpm "state:" labels
(accepted / needs design / needs steps to repro / blocked / rejected)
instead of creating new ones, is aware of the TypeScript/pacquet/pnpr split
and the install/add/update/remove parity rule, and is conservative about
removing maintainer-applied labels.
Triage-only for now; the implementation/auto-PR stage is intentionally not
included. Activation also requires the WARP_API_KEY secret and the Oz by Warp
GitHub App with the org enabled in Warp's Admin Panel.
* ci: address CodeRabbit review on Oz triage PR
- Pin actions to commit SHAs (actions/checkout to the repo's v7.0.0 pin,
warpdotdev/oz-agent-action to the v1 tag commit) with version comments,
matching the repo convention and satisfying zizmor.
- Harden the triage skill against prompt injection: treat all fetched issue
content (title, body, comments, attachments, linked docs) as untrusted data,
never as instructions, and forbid actions beyond the defined read/label/comment
steps.
- Make the non-bug "Needs info" path consistent across the decision rule, the
label-application step, and the result template: apply no state label and post
clarifying questions instead of forcing an ill-fitting label.
The Rust CI test job's `cargo nextest` build holds a full set of
`--all-targets` debug binaries for the whole workspace (~60 crates plus
heavy C deps such as aws-lc-sys, ring, libsql-ffi, and tree-sitter) in
target/. That had grown close to the hosted ubuntu runner's free-disk
ceiling even after the fixed `Free up runner disk space` step, and a run
crossed it: the linker died with "No space left on device" while building
an unrelated crate's test binary (macOS and Windows were unaffected).
Build the test artifacts with line-tables-only debug info via job-level
CARGO_PROFILE_DEV_DEBUG / CARGO_PROFILE_TEST_DEBUG env. This drops most of
the debuginfo bytes that fill target/ while keeping panic backtraces
file:line-accurate. Setting it in the workflow rather than Cargo.toml
leaves local `just test` builds with full debug info.
pnpr-release-to-npm.yml can be triggered via workflow_dispatch. Its publish job uses npm OIDC
trusted publishing to publish the `@pnpm/pnpr` package, and its docker job pushes the mutable
ghcr.io/pnpm/pnpr:latest tag. Neither had an environment gate, so any collaborator with write
access could publish to npm and overwrite the mutable Docker tag without reviewer approval.
Add an environment: release gate to the publish and docker jobs (the build job produces no
externally visible artifacts and is intentionally left ungated), matching the protection used
by release.yml.
Co-authored-by: Claude <noreply@anthropic.com>
The Docker workflow can be triggered via workflow_dispatch, and its build job pushes mutable
tags (:latest and :<major>) to ghcr.io/pnpm/pnpm. Unlike release.yml and update-latest.yml,
the build job had no environment gate, so any collaborator with write access could publish an
image to the public mutable tags without reviewer approval.
Add an environment: release gate to the build job so workflow_dispatch runs require approval
from the release environment reviewers, matching the protection already used by the other
publishing workflows. The release (published) event path is unaffected.
Co-authored-by: Claude <noreply@anthropic.com>
A branch that lives in this repo fires both a push and a pull_request
TS CI run for the same commit, and both report the TS CI / Success
required check. Branch protection then sees two results for that
context, so a cancellation or flake on the push side (e.g. two pushes
racing on the test.yml concurrency group) blocks the PR even when the
pull_request run passed.
Restrict push CI to main so PR branches carry TS CI / Success only via
their pull_request run (and the merge queue via merge_group). Since
pull_request now covers every PR, run the build/test jobs on every
pull_request (same-repo and fork) instead of relying on the push run,
and drop the same-repo/fork/chore-lockfile gating that existed only to
avoid duplicate push+pull_request builds. main stays covered pre-merge
by merge_group and post-merge by the push run.
* ci(pnpr): publish a Docker image on release
Add a `docker` job to the pnpr release workflow that builds and pushes a
multi-arch (linux/amd64, linux/arm64) image to ghcr.io/pnpm/pnpr after the
npm packages are published.
The image is built from the static musl binaries the release job already
produces, so no Rust toolchain runs in the Docker build and the image
matches exactly what ships to npm. It is based on debian:stable-slim with
ca-certificates (pnpr reaches upstream registries over HTTPS via rustls'
platform cert verifier), runs as a non-root user, declares storage and
cache volumes, exposes 7677, and defaults to binding 0.0.0.0 so the server
is reachable from outside the container. The build self-checks that
`pnpr --version` matches the PNPR_VERSION build-arg.
Stable releases also move the mutable `latest` tag; prereleases (versions
containing a hyphen) only get the exact-version tag.
* ci(pnpr): drop unused id-token permission from docker job
The docker job uses only docker/build-push-action's built-in provenance
and SBOM attestations, which are pushed to GHCR via GITHUB_TOKEN. It does
not call actions/attest-build-provenance, so the OIDC id-token: write
permission was unused. Drop it to follow least privilege.
* ci(pnpr): verify staged binary checksum in the docker image build
The build job now pins each binary's SHA256 at build time and uploads it
with the artifact. The docker job verifies the staged binaries against
those checksums and passes them as build-args, and the Dockerfile
re-verifies the binary it copies before trusting it. A 'pnpr --version'
check only confirms reported metadata, so it cannot stand in for an
integrity check on the binary itself. Mirrors the existing pnpm image.
* ci(pnpr): extract release artifacts into an isolated directory
Extract the downloaded musl tarballs into a throwaway directory and move
only the expected, checksum-verified regular files into the Docker build
context. A malformed archive (path traversal or symlink entries) can no
longer escape into the workspace and overwrite the Dockerfile or staged
binaries before the image is pushed to GHCR.
* docs(pnpr): add a language to the docker README code fence
The image-name fenced block lacked a language identifier, tripping
markdownlint MD040. Tag it as text.
GitHub's merge queue runs each entry on a temporary
`gh-readonly-queue/**` branch, whose creation also fires a `push`
event. For `ci.yml` (`on: [push, pull_request, merge_group]`) that
produced two TS CI runs for the identical commit: one `push`, one
`merge_group`. Both reach the reusable `test.yml`, whose concurrency
group keys on `github.ref` with `cancel-in-progress: true`. Since the
two runs share `github.ref`, their per-(platform, node, chunk) test
jobs land in the same group and cancel each other.
The Windows shards are gated only on compile-and-lint + build-pnpr, so
both runs' Windows jobs start near-simultaneously and reliably collide.
The `merge_group` run — the only one the queue's required
`TS CI / Success` check reads — lost the race, so its cancelled test
jobs failed `Success` and ejected the PR for "failed status checks".
Restrict the `push` trigger to ignore `gh-readonly-queue/**` so only
the `merge_group` run executes for queued commits. `pacquet-ci.yml`
already scopes push to `main` and was unaffected. Apply the same
`branches-ignore` to `audit.yml` to drop its redundant push run on
those branches.
* ci: build pnpr per-OS so Linux tests don't wait on the Windows build
The pnpr binaries were built by a single `build-pnpr` matrix job spanning
`[ubuntu-latest, windows-latest]`. A `needs` edge can only target a whole
job, never an individual matrix leg, so every consumer of `build-pnpr`
(the ubuntu smoke/test jobs and the windows test shards) blocked until both
legs finished. The Linux tests therefore waited for the slower Windows pnpr
build even though they only consume the Linux binary artifact.
Extract the build into a per-OS reusable workflow (`build-pnpr.yml`) and call
it twice from `ci.yml` as `build-pnpr-linux` and `build-pnpr-windows`. The
ubuntu test jobs now gate only on `build-pnpr-linux` and the windows shards
only on `build-pnpr-windows`, so neither platform waits on the other's build.
* ci: trigger TS CI when build-pnpr.yml changes
Add the new reusable .github/workflows/build-pnpr.yml to the dorny/paths-filter
ts: list so a PR touching only that workflow still runs the pnpr build and TS
test jobs it gates, instead of being skipped.
The benchmark report comment identifies which revision was benchmarked
only via the workflow run, leaving the PR comment detached from the
exact SHA when read later. Add a clickable short-SHA line directly
under the report header so the comment is self-describing.
SHA source is the PR head SHA on pull_request events (stable commit
that survives merge), falling back to github.sha for push and
workflow_dispatch. Display-only — the comment workflow continues to
resolve the PR number from the trusted workflow_run payload, so the
SHA in the fork-controlled artifact cannot redirect where the comment
lands.
* ci: run required checks on merge_group so the merge queue works
The merge queue dispatches a merge_group event against a temporary
gh-readonly-queue/main ref, but neither TS CI (ci.yml) nor Rust CI
(pacquet-ci.yml) listened for it. Their required status checks therefore
never ran in the queue, so every queued PR waited forever on the missing
contexts (e.g. Rust CI / Success never starting).
Add merge_group to both workflows' triggers and force the change
detection true for that event. Forcing matters because TS CI / Compile &
Lint is itself a required context: a skipped job never reports its
context, which would keep the queue waiting, so it has to actually run.
It also makes the queue test the fully merged result, which is the point
of a merge queue. The Rust deny job's nested path filter, which has no
push/PR base to diff against in the queue, runs unconditionally on
merge_group instead.
* ci: gate compile-and-lint and build-pnpr on merge_group explicitly
The merge queue tests the merged commit, so the gating jobs must run on
merge_group. build-pnpr (added by the Windows-sharding work) carries the
same event guard as compile-and-lint and feeds test-smoke/test/test-windows,
so it needs the merge_group clause too — otherwise the queue would skip the
whole test suite.
Run Windows TypeScript test jobs only on Node.js 22 and split them into three parallel chunks.
Windows TS tests are significantly slower than Linux, and running the full suite on three Node versions spends the extra capacity on duplicate runtime coverage instead of reducing wall clock time. The reusable test workflow now accepts chunk inputs, includes chunk identity in concurrency and artifact names, and keeps Bencher uploads with chunk-specific benchmark names.
Add a CI helper that discovers the selected workspace packages for the same full or affected test scope, shards Jest test files into balanced chunks, runs selected package files with the same Jest Node options and package pretest setup, and writes pnpm-style execution summaries so the pnpm CLI e2e duration extraction still works.
The Release workflow allowed workflow_dispatch from branch refs but only built `@pnpm/exe` artifacts on tag refs. A branch dispatch on main after the 11.9.0 release bump selected the platform artifact packages in the internal publish step, then failed in their prepublishOnly binary verification because the binaries had not been built.
Add a preflight job that accepts only refs/tags/v*.*.*. The real release job depends on that preflight, so non-tag dispatches fail before entering the protected release environment or configuring npm auth.
With non-tag refs rejected up front, the release job can run the full tag release path without per-step tag guards.
The TypeScript pnpm CLI was relocated from `pnpm/` to `pnpm11/pnpm/` in
pnpm/pnpm#12537, but the "Extract pnpm CLI e2e test duration" step still
passed `--package-dir pnpm`. That path no longer exists, so the
exec-summary lookup found no entry and the step exited 1, failing the
full TS CI test job on main.
Point `--package-dir` at the package's new location, `pnpm11/pnpm`.
The TypeScript pnpm CLI freezes at v11; pnpm 12 will be the Rust pacquet
port. To make that split legible, all TypeScript source, test, and build
directories move under a new top-level pnpm11/ directory. The name states
the version boundary rather than implying a behavioral fork, since the two
stacks are meant to behave identically.
Scope is source-only: the shared workspace root stays at the repo root.
pnpm-workspace.yaml, package.json, pnpm-lock.yaml, .pnpmfile.cjs,
.meta-updater, __patches__, .changeset, .husky, and the lint/spell configs
remain in place, so one pnpm workspace and one Cargo workspace still span
all three products. pnpr/client and pacquet/tasks/registry-mock stay as
cross-product workspace members.
Rewiring the move required:
- pnpm-workspace.yaml globs prefixed with pnpm11/
- root package.json script paths, eslint.config.mjs, tsconfig.lint.json,
.gitignore, and CODEOWNERS updated
- .meta-updater/src/index.ts literals repointed (pnpm11/pnpm/package.json,
pnpm11/__utils__, pnpm11/__typings__, and the main package directory)
- regenerated every moved package's repository/homepage URL via meta-updater
- pnpm11/pnpm/bundle-deps.ts and __utils__/scripts/src/typecheck-only.ts
climb one more level to reach the repo root
.meta-updater stays at the repo root because @pnpm/meta-updater resolves
its config at <cwd>/.meta-updater/main.mjs.
TS CI (.github/workflows/ci.yml) now only runs when pnpm11/-relevant paths
change, via a dorny/paths-filter changes job plus a TS CI / Success
aggregate gate; branch protection should require only that gate.
Approvals on PRs from forks never got the `reviewed: coderabbit` label or a
Discord announcement. A pull_request_review run triggered by a forked PR is
granted a read-only token and no secrets, so the label step failed with HTTP
403 and the Discord step had no webhook.
Split the automation in two: the pull_request_review workflow now only records
the approval as an artifact (no token, no secrets), and a new workflow_run
companion runs from the default branch in base-repo context — where it has the
write-scoped token and secrets — to add the label and post to Discord.
The privileged half never checks out or executes PR content: it reads inert
data files, validates the PR number is an integer, maps the reviewer to a fixed
label, requests only actions:read + issues:write, surfaces a failed (vs absent)
artifact download instead of passing as a silent no-op, and scopes the Discord
webhook secret to the announce step.
Also documents that agents must open PRs using .github/pull_request_template.md,
since gh pr create does not apply it automatically.
* chore: stop configuring CodeRabbit pre-merge checks
Defining any pre-merge check turned on the whole pre-merge-checks
subsystem, which under Request Changes Workflow replaces the
auto-approve flow: CodeRabbit emits a pre-merge-checks status block
instead of approving once review threads are resolved, leaving PRs at
REVIEW_REQUIRED even with all threads cleared and all checks green.
Remove the title check (added in pnpm/pnpm#12460) to restore
auto-approval and document why the block is intentionally absent.
Conventional Commits on the squash title stay covered by the commit-msg
hook and the squash-title convention.
* fix: declare Qodo auto-approval under [config]
enable_auto_approval and auto_approve_for_no_suggestions were declared
under [pr_reviewer], where Qodo never reads them, so auto-approval never
ran. Per the Qodo docs both keys belong under [config]. Move them and
drop the now-empty [pr_reviewer] section.
* chore: stop applying labels from Qodo
Product labels are applied by CodeRabbit (auto_apply_labels +
labeling_instructions in .coderabbit.yaml). Having Qodo also publish
labels via enable_custom_labels/publish_labels/custom_labels meant two
tools managing the same labels. Drop the Qodo label config and leave
labeling to CodeRabbit.
* ci: tag Qodo-approved PRs and fix the label step
Add an on-qodo-approval job that applies the 'reviewed: qodo' label when
qodo-free-for-open-source-projects[bot] approves, mirroring the existing
CodeRabbit job.
The label step used 'gh pr edit --add-label', which is broken (it errors
on the deprecated Projects-v1 GraphQL field), so adding the label failed
on approval. Switch all three jobs to the issues REST API
(POST /repos/{owner}/{repo}/issues/{number}/labels) instead.
The pr-review-automation workflow merged in pnpm/pnpm#12462 fails at the
label step with 'Resource not accessible by integration
(addLabelsToLabelable)'. gh pr edit --add-label uses the GraphQL
addLabelsToLabelable mutation, which requires issues: write even when
labeling a PR; pull-requests: write alone is insufficient. Grant it per
job, matching pacquet-integrated-benchmark-comment.yml.
* ci: label and notify on CodeRabbit approval
Add a workflow that fires on pull_request_review and, when CodeRabbit
submits an approving review, applies the informational "reviewed:
coderabbit" label and (if a DISCORD_WEBHOOK secret is configured) posts
a notice to Discord.
PR fields reach the steps through the environment rather than script
interpolation, so an attacker-controlled PR title cannot inject shell
commands. The Discord step is skipped when the webhook secret is unset.
* ci: flag maintainer-approved PRs for automerge
Rename the approval workflow to cover both review automations and add a
second job: when zkochan submits an approving review, apply the existing
"state: automerge" label.
Like the CodeRabbit job, the PR number is passed through the environment
rather than interpolated into the run script.
* ci: harden PR review automation per review feedback
- Scope permissions to each job (top-level permissions: {}) instead of
granting pull-requests: write workflow-wide (zizmor).
- Set allowed_mentions to {parse: []} on the Discord payload so a PR
title containing `@everyone`/`@here` cannot ping the server.
- Add connect/overall timeouts and retries to the Discord curl call.
Branch protection required the individual Rust CI and TS CI jobs as
status checks. That is brittle: most Rust jobs skip on PRs that touch no
Rust files, and a matrix job skipped at the job level never expands its
'${{ matrix.* }}' name — GitHub reports one check under the literal
unexpanded name, so the required per-OS contexts never appear and any
non-Rust PR blocks forever waiting for status that is never reported.
Add one static-named aggregate gate per workflow ('Rust CI / Success'
and 'TS CI / Success') that runs with 'if: always()' and fails only when
a dependency actually failed or was cancelled; skipped dependencies
count as a pass. Branch protection can then require just these two
contexts, decoupling them from the matrix shapes.
TS CI runs on both push and pull_request, and on same-repo PRs the
pull_request run skips every job for deduplication. A naive gate would
report a green 'TS CI / Success' on that skipped run and let a PR merge
before the real push run finished. The gate's name is therefore
'TS CI / Success' only on runs that do real work, and a different name
on the skipped duplicate run — using 'compile-and-lint''s 'if:' verbatim,
which the name MUST stay in sync with. Rust CI needs no such guard: its
push trigger is restricted to main, so a PR produces a single run.
Requires a branch-protection update: replace the per-job Rust CI / TS CI
contexts with 'Rust CI / Success' and 'TS CI / Success'.
* fix(ecosystem-e2e): run the build script via absolute /bin/sh
The build stage sets the child PATH to prepend the project's
node_modules/.bin, and Rust resolves a bare program name against that child
PATH. Spawning `sh` therefore let a dependency-installed `.bin/sh` run in
place of the system shell — running unintended code and masking build
pass/fail. Use the absolute `/bin/sh` so the orchestrating shell is always
the system one; the script it runs still finds framework bins through the
prepended PATH.
* ci(ecosystem-e2e): build pnpm and pacquet once, share via artifacts
The stack matrix rebuilt the pacquet release binary and the pnpm bundle in
every one of its seven jobs. Add a single build job that compiles pacquet,
the harness, and the pnpm bundle once and uploads them as an artifact; the
per-stack jobs download it and run, so the multi-minute Rust and bundle
builds happen once per run instead of seven times.
* feat(ecosystem-e2e): add harness installing real JS stacks across pnpm/pacquet and layouts
Adds `pacquet/tasks/ecosystem-e2e`, a Yarn-PnP-style ecosystem test that
installs, builds, and serves real framework scaffolds (Next.js, Vite,
Angular, Astro, SvelteKit, Nuxt, React Router 7) across the cross product
of {pnpm, pacquet} x {isolated, global virtual store}.
The binary axis catches pnpm/pacquet parity gaps; the layout axis catches
breakage introduced by the global virtual store. Each cell scaffolds the
project once, installs with the binary under test, runs the build, then
boots the production server and probes it over HTTP — so a green cell means
the produced node_modules works at runtime, not just at bundle time.
Runs on a daily cron (one job per stack), not per-PR: the installs are slow
and track upstream framework releases, so a red cell is investigated rather
than treated as a merge blocker.
* style(ecosystem-e2e): use ASCII ellipsis in comments for dylint
* fix(ecosystem-e2e): address PR review findings
- workflow: wrap `pnpm/bin/pnpm.cjs` for the `--pnpm` shim; the built bundle
is `dist/pnpm.mjs`, so the previous `dist/pnpm.cjs` path never existed and
the shim would have failed to launch the repo-built pnpm.
- keep: `--keep` now reuses an already-scaffolded template instead of
re-running the generator into a non-empty directory (which fails).
- serve: retry HTTP 4xx/5xx until the deadline rather than failing on the
first one — dev servers can briefly answer error statuses while warming up.
- serve: the probe reads only the status line, not the whole response body.
- stacks: pin the create-astro, sv, nuxi, and create-react-router generators
to a major version instead of tracking the latest tag, matching the stated
reproducibility intent for the scheduled run.
* fix(ecosystem-e2e): scrub subprocess env, bound CI job, fix README fence
- security: scaffold/install/build/serve run third-party lifecycle code, so
they now launch via `sandboxed_command`, which clears the inherited
environment down to a small allowlist (PATH, HOME, proxy/cert/locale vars,
plus PORT/HOST for servers). An unattended dependency build script can no
longer read ambient CI secrets such as the workflow token.
- workflow: add a 60-minute job timeout so a hung build or serve subprocess
can't pin a runner indefinitely.
- docs: give the README's grid code block a language identifier (MD040).
* fix(ecosystem-e2e): spawn serve argv directly and reset cell log per run
- serve: run the tokenized serve command directly off the project's
node_modules/.bin instead of joining tokens into a `sh -c` string, so
argument boundaries survive and a token that needs quoting can't break the
launch. The spawned PID is the server itself, so teardown still works.
- log: truncate cell.log at the start of each run so reruns (notably under
--keep) don't interleave with stale output.
## Problem
On Windows, **any failed `pnpm` command hangs 20–46 seconds before exiting.** The error handler (`pnpm/src/errorHandler.ts`) enumerates descendant processes via `pidtree` to terminate them on every error exit. On Windows `pidtree` shells out to `wmic` and, where wmic has been removed, a PowerShell `Get-CimInstance Win32_Process` fallback — a process listing that takes tens of seconds on busy CI runners.
This also broke Windows CI: the `verifyDepsBeforeRun/*` e2e suites are full of intentional-failure assertions (e.g. `pnpm start` with `--config.verify-deps-before-run=error` when deps aren't installed). Each failure paid the ~23 s error-handler tax, so the suite blew past the 70-minute cap. `pnpm install` and success paths never hit the error handler, which is why only failures were slow.
Diagnosed by sampling `process.getActiveResourcesInfo()` during the hang: it showed a lingering `ProcessWrap` (a spawned child), and hooking `child_process.spawn` named it (`wmic` → `powershell … Get-CimInstance Win32_Process`, exiting after ~23–46 s).
## Fix
Race the descendant-process lookup against a 2 s timeout. If it doesn't return in time, skip the kill and exit — `exit()` calls `process.exit`, which abandons the still-running (harmless, read-only) process query instead of blocking on it. The fast path (Unix, fast Windows) is unchanged.
Confirmed on Windows CI: the failing `start` invocations dropped from **~23 s to ~2.7 s**, and `multiProjectWorkspace.ts` went from **716 s to 124 s**.
## Also included
The CI pnpr-binary cache is split into `restore` + an explicit `save` step that runs right after the build, so a failing test step no longer discards the ~20-minute Rust build (the combined `actions/cache` only saved in a post-job step that gets skipped on failure).
## Summary
Adds CI duration tracking for the `pnpm-ci-performance` Bencher project.
Tracked Rust testbeds and benchmarks:
- `pacquet.ubuntu`, `pacquet.windows`, `pacquet.macos` -> `tests.all`
- `pnpr.ubuntu`, `pnpr.windows`, `pnpr.macos` -> `tests.all`
Tracked pnpm testbeds and benchmarks for full test runs:
- `pnpm.ubuntu.node22`, `pnpm.ubuntu.node24`, `pnpm.ubuntu.node26` -> `tests.all`, `tests.cli`
- `pnpm.windows.node22`, `pnpm.windows.node24`, `pnpm.windows.node26` -> `tests.all`, `tests.cli`
The test workflows produce Bencher-compatible JSON artifacts without receiving `BENCHER_API_TOKEN`. A separate `workflow_run` workflow downloads those artifacts only for same-repository runs, validates their metadata, and uploads from trusted workflow code using the existing `BENCHER_API_TOKEN` secret. The pnpm CLI e2e duration is extracted from `pnpm run --report-summary` output during the same full-test execution, so the CLI e2e suite is not run a second time.
* fix(pnpr): pass batch_publish test request bodies by reference
The put_json/put_json_with_token test helpers took the JSON body by
value but only borrowed it for serde_json::to_vec, tripping clippy's
needless_pass_by_value under --all-targets. Take &Value instead, which
also drops an unnecessary body.clone() at one call site.
* ci: run clippy as a single-OS job and add it to the pre-push hook
Clippy was a step inside the three-OS Lint-and-Test matrix, so it ran
once per OS even though it lints the same platform-independent source
each time. Move it to its own ubuntu-only job, mirroring the existing
single-OS doc, format, and dylint jobs (platform-gated cfg blocks are
still type-checked per-OS by the test build).
It was also missing from pacquet/scripts/pre-push-rust.sh, so a clippy
lint that only fires under --all-targets — like the one that just
reached main — slipped past local pushes and surfaced only in CI. Add
the same --all-targets workspace clippy gate to the hook.
## Motivation
The [vlt.sh benchmarks](https://benchmarks.vlt.sh/) (2026-06-11 run, pacquet 0.11.3) show pacquet several times slower than the fastest package managers in the warm-metadata fresh-resolve cells (`cache`: 3.9–8.1x), the cold-cache frozen-install cells (`lockfile`: up to 10x on vue), and `clean`. Profiling the babylon and vue fixtures locally (macOS time profiles of the warm fresh resolve and the install tail) surfaced three independent causes, fixed here.
## Changes
### 1. Deprecation probing without manifest hydration (pacquet)
With `minimumReleaseAge` active (the default), every range pick runs `filter_pkg_metadata_by_publish_date`, and any dist-tag pointing outside the maturity cutoff (`next`, `beta`, `canary`, a too-fresh `latest`) repopulates by scanning all candidates and reading each candidate's `deprecated` flag. Each read hydrated the full version manifest — a complete `serde_json` parse including the flattened catch-all map. On babylon's warm fresh resolve this was the single largest CPU consumer (~10 thread-seconds, all on the resolve task's critical path).
`PackageVersions::is_deprecated` now answers from the raw fragment (substring pre-check, then a single-field deserialize with the same normalization as `PackageVersion::deprecated`), the tag-repopulation loop parses candidate versions once per filter call (mirroring the `parsedSemverCache` in pnpm's `filterPkgMetadataByPublishDate`), and the deprecated-pick fallback uses the probe instead of hydrating every version.
**babylon warm fresh resolve: `resolve_workspace` 7.5s → 2.6s.**
### 2. Relative-symlink up-to-date check (pacquet)
`force_symlink_dir` joined an existing link's relative contents onto the link parent and compared the result *verbatim* against the wanted target. Virtual-store links contain `..` segments (`../../<pkg>/node_modules/<name>`), so the joined path never compared equal and every up-to-date symlink was unlinked and recreated. Node's `path.relative` — which upstream `symlink-dir`'s `isExistingSymlinkUpToDate` builds on — resolves its arguments, so pnpm treats those links as current. Both sides now pass through `lexical_normalize`. The babylon install tail was dominated by exactly this unlink+symlink churn.
**babylon warm install: 6.8s → 4.7s; warm frozen install: 4.2s → 2.3s.**
### 3. Default network concurrency floor 16 → 64 (pnpm + pacquet)
The default was `min(64, max(workers * 3, 16))`. Downloads are I/O-bound, not CPU-bound: on a 4-vCPU CI runner the formula yields 16 concurrent requests, so a low-latency registry drains 600–1300-tarball installs 16 at a time while staying unsaturated — a large share of the cold-cell (`lockfile`/`clean`) gap on the benchmark runners. The default is now `min(96, max(workers * 3, 64))`; the `networkConcurrency` setting still overrides it. Applied to `@pnpm/installing.package-requester`, the lockfile-resolution verifier fan-out that mirrors its floor, and the same two spots in pacquet. Changeset included (minor). **This is a user-visible defaults change on both stacks — flagging it explicitly for review.**
## Local results (M-series macOS, vlt fixtures, isolated store/cache)
| cell | before | after |
|---|---|---|
| vue `cache` | 1159 ms | **479 ms** |
| vue `cache+lockfile` | 621 ms | **392 ms** |
| vue no-op install | 48 ms | **41 ms** |
| babylon `cache` | ~8.8 s | **4.75 s** |
| babylon `cache+lockfile` | ~4.2 s | **2.27 s** |
vue's warm cells are now ahead of every competitor measured locally; babylon's `cache` cell closed from ~2.5x behind the leader to ~1.35x (the remainder is the per-file store-integrity verify and per-file linking that the pnpm store contract requires).
## Validation
- `cargo nextest`: registry, resolving-npm-resolver, resolving-deps-resolver, lockfile-verification, network, fs, tarball, package-manager, cli — 1300+ tests, all green; new unit tests cover the deprecation probe (string/bool/empty/corrupt shapes, nested-key false positives) and cross-parent relative-symlink reuse (fails without the fix).
- Lockfile stability: `--lockfile-only` output is byte-identical before/after on vue; on babylon the resolved **package-version sets are identical across 6 runs (3 per binary)**. The babylon lockfile does flap between runs in the peer-suffix shape of `webpack-dev-server@5.2.2` (`(bufferutil@4.1.0)(utf-8-validate@5.0.10)` appearing/disappearing) — this is **pre-existing nondeterminism** reproducible with the unmodified binary against itself, in the optional-peer area; worth a separate issue.
- Pre-push checks (fmt, taplo, `cargo doc -D warnings`, dylint) pass; eslint (root config) and `tsgo --build` pass for the two touched TS packages.
Summary:
- add Linux musl binary package selection to the pacquet and pnpr npm shims
- generate linux-x64-musl and linux-arm64-musl native npm packages with libc metadata
- build musl Rust release targets for both pacquet and pnpr
- update package docs and cspell entries for the touched workflow files
Follow-up to #12292 (which verifies the **package-manager** binary). This closes the same class of gap for the **Node.js runtime**.
When a repository requests a Node.js runtime — `devEngines.runtime: node@X` (with `onFail: download`, the default) or `useNodeVersion` — pnpm downloads and then executes a Node binary (it's used to run lifecycle / `run` / `exec` scripts). The download **mirror is repository-configurable** via `node-mirror:<channel>` (`nodeDownloadMirrors`) in project `.npmrc`, and the integrity comes from `SHASUMS256.txt` fetched **from that same mirror**.
That's a circular check: a malicious mirror serves a tampered `node` tarball **and** a matching `SHASUMS256.txt`, the sha256 check passes, and pnpm runs the binary. Drive-by on a normal command in a cloned repo.
## Fix
pnpm now fetches `SHASUMS256.txt.sig` and verifies its **detached OpenPGP signature** against the **Node.js release team's public keys, embedded in the pnpm CLI**, before trusting the hashes. A mirror that serves a tampered binary cannot also produce a valid signature, so verification fails. Any faithful mirror (one that proxies the real signed SHASUMS) keeps working.
- `@pnpm/crypto.shasums-file`: new `fetchVerifiedNodeShasums` / `fetchVerifiedNodeShasumsFile` verify the signature via `openpgp` against the embedded keys.
- The keys live in a generated file (`src/nodeReleaseKeys.ts`, 28 keys) mirrored from the canonical `nodejs/release-keys` list. `crypto/shasums-file/scripts/update-node-release-keys.mjs` keeps them current (`pnpm check:node-release-keys` / `--update`), and the **create-release-pr** workflow runs the check as a gate so a new release signer can't silently break verification.
- `@pnpm/engine.runtime.node-resolver` verifies the **configurable-mirror** SHASUMS. The hardcoded `unofficial-builds.nodejs.org` musl mirror is **not** repo-configurable and is signed by a different key, so it stays trusted over TLS.
## Scope
- **Pre-release channels (rc, nightly, …) are not verified** — Node only signs the `release` channel (no `SHASUMS256.txt.sig` exists for them, even on nodejs.org), so they remain unverifiable. Verification is gated on the `release` channel.
- **Bun / Deno are unaffected** — their download/SHASUMS URLs are hardcoded to canonical GitHub (`github.com/oven-sh/bun`, `api.github.com/repos/denoland/deno`), not mirror-configurable, so a repo can't redirect them.
- **Pacquet parity:** `pacquet/crates/engine-runtime-node-resolver` has the same mirror-configurable SHASUMS logic and needs the equivalent Rust port — tracked as a follow-up (per the repo's parity rule, opening the TS side first).
pnpm can be made to download and execute a native binary through two **repository-controlled** inputs, neither of which was authenticated before this change:
1. **pacquet install engine** — declaring `pacquet` (or `@pnpm/pacquet`) in `configDependencies` (in `pnpm-workspace.yaml`) opts in to pnpm's Rust install engine, and pnpm spawns the platform binary `@pacquet/<platform>-<arch>` during `pnpm install`.
2. **package-manager version switch** — the `packageManager` / `devEngines.packageManager` field makes pnpm download and run a specific pnpm version. This is **on by default** (`onFail` defaults to `download`) and also covers `pnpm self-update` and `pnpm with`.
In both cases the repository also controls the lockfile integrity and the registry the bytes are fetched from (via `.npmrc`), so matching the lockfile integrity proves nothing — it matches the hash the attacker wrote. A cloned, untrusted repository could therefore execute an arbitrary native binary just by running a normal pnpm command.
## Fix (corepack-style registry-signature verification)
pnpm now verifies the **npm registry signature** of the bytes it is about to spawn, **over the installed integrity**, against npm's public signing keys that **ship embedded in the pnpm CLI** (exactly as corepack does). If the bytes on disk were substituted or tampered with, npm's real signature does not validate over them.
- New reusable `verifyInstalledPackageSignatures()` in `@pnpm/deps.security.signatures` verifies `name@version:integrity` against `dist.signatures` using the embedded keys.
- Because the keys are **embedded** (not fetched), a registry the user did not vouch for cannot supply its own keypair to forge a signature. The signed packument is fetched from the **configured** registry, so an **npm mirror works transparently** — it proxies the same signed packument, with no configuration. There is intentionally **no runtime override or off-switch** for the keys.
- **pacquet** (`installing/commands`): verifies the `pacquet` shim and the host platform binary. It **fails the command** if the signature does not verify or cannot be checked (e.g. registry unreachable); the only graceful fallback to pnpm's own engine is when pacquet has no binary for the current platform.
- **pnpm engine** (`engine/pm/commands`): verifies `pnpm`, `@pnpm/exe`, and the host platform binary, **only on a store cache miss** (an actual download), so it adds no network round trip to every command. It **fails closed** — any verification failure, including an unreachable registry, refuses the version switch rather than running an unverified binary.
## Keeping the embedded keys fresh
The embedded keys live in a generated file. `deps/security/signatures/scripts/update-npm-signing-keys.mjs` keeps them in sync with npm's keys endpoint (`pnpm check:npm-signing-keys` / `--update`), and the **create-release-pr** workflow runs the check as a gate, so a key rotation cannot silently break verification — a stale key set blocks the release until refreshed.
## Pacquet parity
pacquet gained `configDependencies` support on `main` (#12285), but it has **no install-engine-spawn sink** — pacquet *is* the engine, and it does not select/spawn an alternate engine from `configDependencies` (its only config-dependency code-execution path is `updateConfig` plugin pnpmfiles, which it shares with pnpm and which this advisory does not cover). So CAND-PNPM-097 has no pacquet-side analog; no pacquet code change is needed.