* chore: update lockfile, Node.js, and pnpm versions
* ci: force a cold pnpm store to confirm the Windows cache corruption
The Windows Rust CI install fails with ERROR_DIRECTORY (os error 267)
creating symlinks over a store restored from actions/cache. With
enableGlobalVirtualStore on, the cached store holds the global-virtual-store
`links/` reparse points, which tar can't round-trip, so the restored store
is corrupt and a warm install fails on symlink creation. The failure moves
between symlinks run to run (parallel linking picks a different victim),
which is why the alpha.12 symlink-writer fix could not cover it.
Bump the cache key to force a cold store. If Windows goes green, the cache
is confirmed as the cause and the durable fix is to stop caching `links/`.
* fix(fs): heal a dangling junction parent when materializing a symlink
alpha.12 tried to recover from a dangling `node_modules` junction (e.g. a
store restored from a tar-based CI cache) by keying on ERROR_DIRECTORY
(os error 267), but the real path is different: `CreateSymbolicLinkW`
through a dangling junction returns NotFound, and the NotFound recovery's
`create_dir_all` then fails with AlreadyExists (os error 183) because the
junction still occupies the slot. So the 267 branch never fired and the
install aborted.
Handle it where it actually happens: when `create_dir_all` on the
symlink's parent fails and the parent is a reparse point, remove the
dangling junction (unlinking only the junction, never a live target),
rebuild a real directory, and retry. Drops the dead 267 branch.
* ci: exclude the global-virtual-store links/ tree from the pnpm store cache
The GVS `links/` tree is Windows directory junctions, which tar
(actions/cache) can't round-trip: a restored store came back with
dangling junctions and installs failed creating symlinks. Exclude
`links/` from the cached path so only the CAS content is cached; pnpm
regenerates `links/` fresh on every install. The `nolinks` key abandons
the old caches that still hold `links/`.
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* chore: update lockfile, Node.js, and pnpm versions
* chore: pin packageManagerDependencies specifier to the resolved pnpm version
The update-lockfile workflow recorded the next-12 dist-tag as the
packageManagerDependencies specifier, but the manifest pins pnpm to
12.0.0-alpha.11. pacquet validates the specifier against the manifest, so a
later --frozen-lockfile install failed with
ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE. Record the resolved pin so the
specifier matches the wanted version.
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
The Rust CLI and pnpr embed the versions their release builds report and
verify (`PNPM_VERSION` in pnpm/crates/config/src/defaults.rs, the crate
version in pnpr/crates/pnpr/Cargo.toml, and its Cargo.lock entry). These
were mirrored from the npm wrappers by `syncRustVersions` in bump.ts — a
step separate from the meta-updater, so running `pnpm version -r` without
the full bump flow bumped the wrappers (pacquet 12.0.0-alpha.10, pnpr
0.1.0-alpha.2) while the Rust sources stayed at the previous versions. The
release workflow's "Verify the committed version" step then failed, and
nothing caught the drift before the tag.
Move the sync into the meta-updater as a set of Rust-source file handlers,
so it is written by `pnpm update-manifests` and, crucially, validated by
`meta-updater --test` in pre-push and CI — a missed sync now fails locally
instead of at release time. bump.ts drops the redundant `syncRustVersions`
and runs `pnpm update-manifests` after `pnpm version -r`; the root `bump`
script no longer needs its own trailing `update-manifests`.
Regenerating brings the Rust sources up to the already-bumped wrapper
versions (alpha.10 / alpha.2), unblocking the release.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge the three release flows (TypeScript, Rust CLI + @pnpm/napi, pnpr)
into a single flow driven by pnpm's native workspace versioning
(pnpm/pnpm#12953), dropping the @changesets/cli dependency (Closes
pnpm/pnpm#12947).
The native engine keys package identity on the workspace directory, so
the Rust CLI wrapper is named pnpm (the v12 line at pnpm/npm/pnpm) and
shares the published name with the TypeScript CLI at pnpm11/pnpm.
Release configuration moves from .changeset/config.json to the
versioning key of pnpm-workspace.yaml: versioning.lanes puts the Rust
CLI, @pnpm/napi, and @pnpm/pnpr on an alpha lane (X.Y.Z-alpha.N
prereleases published under next) while the TypeScript CLI releases
stable on the main lane; versioning.fixed keeps the Rust CLI and
@pnpm/napi at one shared version; versioning.ignore freezes
@pnpm/logger, which is consumed as a catalog: peer the engine would
otherwise reject as an internal range.
Lanes replace the hand-rolled prerelease continuation, and the
committed .changeset/ledger.yaml replaces the .changeset-released
directory as the cherry-pick-safe record of consumed intents. bump.ts
drops both and is now just pnpm version -r plus syncRustVersions, which
mirrors the bumped wrapper versions into defaults.rs and the pnpr crate
version.
Because two workspace projects are named pnpm, name-based --filter=pnpm
is qualified by directory (pnpm{pnpm11/pnpm}) across the build and
release scripts, the meta-updater excludes the Rust wrappers by
directory, and changesets targeting the TypeScript CLI reference it as
./pnpm11/pnpm. release.yml's plan job gates per-product publish jobs on
which committed versions are unpublished; everything publishes via
trusted publishing. @changesets/cli, .changeset/config.json, and the
standalone pacquet/pnpr release workflows are removed; their npm
trusted-publisher bindings must be re-pointed at release.yml before the
first unified release.
pnpm-lock.yaml is regenerated from scratch: an incremental
--lockfile-only resolve after the @changesets/cli removal hit a pacquet
incremental-resolver bug (pnpm/pnpm#12958) that emptied a peer-context
snapshot the CLI depends on and broke the bundle build. A from-scratch
resolve is correct; the bug is filed separately.
* **New Features**
* The CLI build and distributed archives are now `pnpm`, with updated dev commands and release artifacts.
* Shell completions are consistently branded as `pnpm` and generate `pnpm completion-server` wiring.
* **Bug Fixes**
* Version output/user-agent reporting now aligns with `pnpm`.
* Diagnostics and retry-related error codes are updated to `ERR_PNPM_*`.
* **Documentation**
* Updated E2E and integrated-benchmark guides to reference `pnpm` build outputs.
* SBOM metadata now identifies the tool as `pnpm`.
* **Chores**
* Updated CI E2E/benchmark workflows and test suites to run the `pnpm` binary.
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.
## 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.
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.
* chore: update pnpm-lock.yaml
* chore: sync node.js runtime version in scripts with devEngines via meta-updater
* chore: sync node.js version in CI workflows with devEngines via meta-updater
The TS test harness (`__utils__/jest-config/with-registry/globalSetup.js`)
already launches `pnpm-registry`, and pacquet's `RegistryMode::Verdaccio`
spawns `pnpm-registry` too (the enum variant is a misnamed leftover). The
verdaccio dependency was only there to satisfy `@pnpm/registry-mock`'s
peerDependency declaration — nothing in this repo invokes verdaccio at
runtime.
Remove the catalog entry, the stranded `verdaccio.yaml` config, and the
`@verdaccio/auth` packageExtensions block. Mark `@pnpm/registry-mock`'s
verdaccio peer optional so pnpm doesn't auto-install it (and the entire
`@verdaccio/*` tree) across the workspace. Lockfile drops ~1100 lines.
Written by an agent (Claude Code, claude-opus-4-7).
* chore(pacquet): fold registry-mock into root workspace, fix npm metadata
Two unrelated cleanups bundled because they touch the same publishing/
workspace plumbing:
1. **`pacquet/npm/pacquet/package.json` metadata** — the imported file
still pointed at the standalone repo: `repository.url` was
`pnpm/pacquet`, `repository.directory` was `npm/pacquet`, `homepage`
and `bugs` likewise. Repoint at `pnpm/pnpm`. Update
`generate-packages.mjs` so the per-platform packages it emits at
release time also point at `pnpm/pnpm`.
2. **Fold `pacquet/tasks/registry-mock` into the root pnpm workspace**.
Pacquet's standalone-repo nested-workspace setup pinned
`nodeLinker: hoisted` "for verdaccio CJS resolution," but pnpm's
own jest globalSetup (`__utils__/jest-config/with-registry/`) calls
the same `@pnpm/registry-mock.start()` API under the default
isolated linker without issue, and verified locally that
`node launch.mjs prepare` works after consolidation. The hoisted
constraint was scoped to standalone-pacquet's install pattern; in
the monorepo it's unnecessary.
Changes for (2):
- Add `pacquet/tasks/registry-mock` to `pnpm-workspace.yaml`.
- Rename the package `@pnpm-private/pacquet-registry-mock-launcher`
(private, matches the `@pnpm-private/*` convention used by other
internal workspace members) and switch `@pnpm/registry-mock` to
`catalog:` (the root catalog already pins it at 6.0.0).
- Delete `pacquet/tasks/registry-mock/pnpm-lock.yaml` and
`pnpm-workspace.yaml` — root install handles both now.
- Delete `pacquet/package.json` and `pacquet/pnpm-lock.yaml` — the
file only had a `cargo build` script + `devEngines: pnpm`, both
already covered by root, and nothing referenced it.
- `justfile install` is now just `pnpm install` (was
`cd pacquet/tasks/registry-mock && pnpm install --frozen-lockfile`).
- `pacquet-integrated-benchmark.yml` path filter and cache key
swap the deleted nested lockfile for the root `pnpm-lock.yaml` /
`pnpm-workspace.yaml`.
Verified: `pnpm install` resolves the workspace member, the lockfile
gains a `pacquet/tasks/registry-mock` importer entry, and
`pacquet/tasks/registry-mock/node_modules/@pnpm/registry-mock` is
linked correctly under the isolated layout.
* fix(pacquet): match meta-updater conventions in registry-mock launcher
The previous version of `pacquet/tasks/registry-mock/package.json`
omitted `version`, which crashed `pn lint:meta` (meta-updater hits
`manifest.version!.split('.')[0]` for every workspace package).
Backfill all the fields meta-updater would emit for an internal
`@pnpm-private/*` private package, matching `__typecheck__/package.json`
and friends:
- `version: 1100.0.0` (the pnpm 11.x convention for non-experimental
internal packages)
- self-devDep entry (`workspace:*`) that meta-updater would otherwise
inject
- `keywords: [pnpm, pnpm11]`
- `repository` pointing at this directory inside pnpm/pnpm
This is the same shape every other `@pnpm-private/*` private workspace
member uses; it lets `pn lint:meta --test` pass without modifying the
file.
* fix: update lockfile
* chore(pacquet): add build:pacquet script at root
Restore the `cargo build --release --bin pacquet` shortcut that lived in
the deleted `pacquet/package.json`. Naming it `build:pacquet` (rather
than `build`) matches the existing namespacing convention in this
file (`lint:ts`, `lint:meta`) and leaves room for a general `build`
script later. Invoke with `pnpm build:pacquet` or `pn build:pacquet`
from the repo root.
Adds `devEngines.runtime` to pin the Node.js version (24.6.0, `onFail: download`) the project uses for development, so contributors don't have to manage Node versions manually.
CI changes that come with it:
- Bumps pnpm to **11.1.1** and `pnpm/action-setup` to a bootstrap that ships `@zkochan/cmd-shim` 9.0.3. The cmd-shim update is required because the previous shim's `exec cmd /C` got mangled by Git Bash's MSYS path conversion (`/C` → Windows path), which broke any `pn …` invocation from `shell: bash` on Windows.
- Switches the install step to `pn install --no-runtime` so the per-test-matrix Node version chosen by `pn runtime -g set node …` isn't overridden by the project-pinned 24.6.0.
- Adds a `Verify Node version` step that asserts `pn node -v` matches the matrix's Node.
* chore(release): wrap changeset version with cross-branch consumed-id ledger
When a fix is cherry-picked from main to a release branch (or vice
versa), the changeset file ends up on both branches. The release
branch's release consumes and deletes its copy, but the cherry-picked
copy on main survives the merge back and would be re-applied on the
next main release.
Introduce a small wrapper around `changeset version` that maintains a
per-branch ledger at .changeset/.released/<branch>.txt. Each entry is a
consumed changeset id; the file is written only by the branch it is
named after, so the records merge across branches without conflicts.
Before running `changeset version` the wrapper reads the union of every
ledger file, hides matching .changeset/<id>.md files (rename to
.md.released), then runs `changeset version` against the remaining set.
Newly consumed ids are appended to the current branch's ledger; hidden
files are removed afterward (their consumption is already on record
elsewhere). On failure the hidden files are restored to keep the
working tree clean.
* docs: move release-ledger explanation out of AGENTS.md
AGENTS.md is for instructions to AI agents working on the codebase, but
the cross-branch ledger is release machinery that the maintainer running
`pnpm bump` interacts with — agents authoring changesets do not need to
know about it. Move the explanation to where someone runs into it:
- .changeset/.released/README.md — discovered by anyone exploring the
directory.
- A short doc-comment header at the top of __utils__/scripts/src/bump.ts
pointing readers there.
* fix(scripts): harden bump wrapper edge cases from PR review
- Use url.pathToFileURL(realpathSync(...)) to compare against
import.meta.url so the direct-invocation guard works on Windows
paths and through symlinks (Copilot review).
- hideReleased() now iterates the changeset directory and filters by
the released set instead of iterating the (potentially long) ledger
and probing existsSync per entry (Copilot review).
- hideReleased() restores already-renamed files if a later rename
throws, so a partial failure leaves the .changeset directory in its
original state (CodeRabbit review).
- Move deleteHidden() into a finally so the .md.released files are
cleaned up even if appendReleased() throws after a successful
changeset version run (CodeRabbit review).
- Add a unit test that forces hideReleased() to fail mid-loop and
asserts the rollback.
* chore: use @zkochan/git-wt package for worktree creation
Replace the in-repo `worktree:new` script and `shell/wt.*` helpers with
the published `@zkochan/git-wt` package. Contributors now install it
globally (`pnpm add -g @zkochan/git-wt`) and enable the `wt` shell
function via `git-wt init <shell>`, which also makes `git wt <branch>`
available as a native git subcommand.
* chore: remove shell/cleanup-worktrees.sh
Its functionality is now available as `git-wt cleanup` in the
@zkochan/git-wt package, which contributors are already being directed
to install in CONTRIBUTING.md.
* docs: give copy-paste install commands for the wt shell function
Previously CONTRIBUTING.md said "add this line to your config" and showed
the snippet, making contributors open the rc file themselves. Replace with
a one-liner per shell that appends to the rc file and activates `wt` in the
current session in one go.
* test: update registry-mock to 6.0.0 stable and use pnpm view in tests
Update @pnpm/registry-mock from 6.0.0-6 to 6.0.0 stable release.
Replace npm view with pnpm view in test helpers now that pnpm has
native view/dist-tag commands. Unskip the nodeRuntime test that was
blocked on the registry-mock republish.
* chore: update pnpm to beta 8
* feat: support versions, dist-tags, and time field selectors in pnpm view
The view command now exposes versions (as an array of version strings),
dist-tags, and time from registry metadata. Single-field --json output
returns the raw value instead of wrapping it in an object, matching npm
behavior. This allows tests to use pnpm view instead of npm view.