Batch stage approval derived its dependency order from the local workspace, which could differ from the selected staged artifacts or be unavailable on a maintainer machine.
Download and inspect every selected tarball before approving the batch. Build the graph from the published manifests, key relationships by stage ID, approve dependencies first, and propagate failed approvals to their selected dependents. Bound compressed and decompressed tarball reads, preserve alias-to-tag semantics, and reject duplicate staged identities before approval. Keep the TypeScript and Rust implementations aligned and share each implementation tarball reader with stage download.
`pnpm stage approve` accepted exactly one stage id and wrapped each request in
its own `withOtpHandling`, so releasing a workspace meant one invocation and one
proof of presence per package, in an order the releaser had to work out by hand.
It now takes a list of stage ids, and with none it lists the staged versions and
offers them in a checkbox prompt (the non-interactive case keeps failing with
STAGE_ID_REQUIRED, now with a hint).
The batch runs through a new `OtpSession` in the web-auth package: it keeps the
one-time password a challenge yielded and passes it to every later request,
re-prompting only once the registry stops accepting it — a classic TOTP expires
within a minute, which is exactly the case a whole-workspace approval hits.
`withOtpHandling` is now the single-operation form of that session, so publish
and reject behave as before.
Inside a workspace the selection is sorted through the workspace dependency
graph (the same sequencer the recursive commands use), keyed on the name each
project publishes under, since that is the only name a staged version carries. A
package whose workspace dependency could not be approved is skipped rather than
published against a dependency that never reached the registry. A registry
verdict on one version leaves the rest of the batch running and ends with a
non-zero exit; an authentication failure or a broken connection aborts it.
The registry's description of a staged version decides what a maintainer
publishes, so its id and package name are validated as they came — a hidden
character can neither be stripped into a valid value nor keep a name from
matching a workspace package — and the display-only fields are stripped of the
control characters that could redraw the picker around a selection. The shared
`@pnpm/text.sanitize` package holds that character set for both the staged
picker and `update --interactive`, mirroring the `pnpm-text-sanitize` crate.
Both stacks implement this, down to the partial-batch summary and exit code —
pacquet has no output-with-exit-code channel, so it prints the summary the way
the dispatcher prints command output and exits 1.
Up to v11, `@pnpm/exe` was the build of pnpm that bundled a Node.js
runtime, so it ran where the JavaScript `pnpm` package could not. From
v12 the `pnpm` package is itself the native executable and needs no
Node.js, which left `@pnpm/exe` an equal-content copy of it, published
and dist-tagged for nothing.
generate-packages.mjs no longer emits the `pnpm-exe` wrapper dir, and
release.yml no longer packs or stages it. The per-platform
`@pnpm/exe.<target>` native packages are unchanged: they carry the
binaries the `pnpm` wrapper resolves.
update-latest.yml would have failed on the first v12 promotion, since it
unconditionally installs and dist-tags `@pnpm/exe` at the promoted
version. Both sites now pick the wrapper by major. The upgrade check
gates on the *starting* version so upgrading a v11 `@pnpm/exe` onto a
v12 release is still covered, and it gained `--allow-build=pnpm` because
from v12 the preinstall that swaps the placeholder bin for the native
one lives on the `pnpm` package -- without it that check would have
validated a placeholder.
No runtime code changes: both stacks' package-to-install selection
already converges on `pnpm` for major >= 12, the update notifier already
prints `pnpm`, and `@pnpm/exe` stays in the bin-resolver and global-alias
lists so v11-and-earlier installs keep working. Only the comments that
justified those behaviors with "published under both names" needed
correcting.
Keep staged publishing above direct OIDC publishing in the trust-policy ranking.
Use the stronger release path instead of lowering the rank.
Stage the TypeScript `@pnpm/exe` and `pnpm` packages in dependency order.
Stage Rust native packages, wrappers, and the `pnpm` gate as separate layers.
Let CI finish after creating every stage without polling for approval.
Maintainers approve the completed stages later with interactive 2FA.
Pin publication traffic to the npm registry and ignore ambient auth configuration.
Sanitize stage output before logging registry-provided content.
Closespnpm/pnpm#13693.
Version tags were created in CI with `git tag "$tag" "$SHA"` — a lightweight tag,
which has no tag object to carry a signature. Since v11.18.0 that made
`git verify-tag` fail with "cannot verify a non-tag object of type commit",
breaking downstream packagers who verify the tag before building.
Signing those tags in CI would restore verify-tag but not the property that makes
it worth verifying: the key would live in Actions secrets, inside the same trust
boundary the signature is meant to attest independently of. Cut tags from a
maintainer machine instead, and have release.yml verify the tag before any
publish job runs.
verify-release-tag imports the public keys committed under .github/release-keys
into a throwaway GNUPGHOME and runs `git verify-tag` on the pushed tag. Every
publish path descends from `plan` and `plan` descends from that job, so one edge
gates all three products. Verified against a signed tag, a lightweight tag, an
annotated unsigned tag, and a tag signed by a key outside the directory: only the
first proceeds.
The keyring is isolated so verification cannot succeed against a key already on
the runner, and checkout sets fetch-tags because a tag push checks out the commit
while the signature lives on the tag object. The non-tag verify_ts_release
dispatch exits from inside the step rather than a job-level `if`, since a skipped
job skips everything that needs it.
This guards against unsigned tags, not against an actor who can push arbitrary
ones: a tag push runs the workflow file and checks out the tree from the pushed
tag. Restricting creation of v* and pnpr@* tags is a repository ruleset concern.
RELEASING.md documents the manual procedure: tag an explicit commit, verify every
tag before pushing, and retag at the fixed commit to retry a failed release.
Related to pnpm/pnpm#13578