The Rust release built the CLI binary and the NAPI addon back to back in
one matrix leg per target. The two are compiled under different Cargo
profiles — `release` for the CLI, `napi-release` for the addon, which
must keep `panic = "unwind"` so a panic reaching the FFI boundary becomes
a JS exception instead of aborting the host node process. Different
profile means a different target directory, so the two builds shared not
one compiled dependency: every leg paid for the whole graph twice, in
series.
Split the addon into its own `build-rust-napi` matrix over the same eight
targets. Measured on the 12.0.0-rc.9 release run, per-leg build time was:
target CLI addon leg
win32-arm64 508s 369s 17m09s
win32-x64 495s 363s 15m06s
darwin-arm64 477s 241s 12m30s
darwin-x64 461s 235s 12m10s
linux-* 322-351s 217-238s ~10m
The stage is bounded by its slowest leg, so dropping the addon out of
win32-arm64 takes the critical path from ~17 to ~11 minutes and the whole
release run from ~25m30s to roughly 19 minutes. The addon legs run
alongside and gate nothing.
Downstream needs no changes: the new job uploads to
`binaries-rust-napi-<code-target>`, which the `binaries-rust-*` download
pattern the verify, publish and draft-release jobs already use picks up
and merges into the same directory. It is also free of the node-gyp
payload dependency, since only the CLI archives ship `dist/`.
The toolchain prelude the two jobs share — installing cross, the
clang-cl/ARM64 Visual Studio components, the rustup target, and the guard
that the committed PNPM_VERSION matches the tag — moves into a
`rust-release-target` composite action rather than being duplicated.
zizmor flags that action's writes to GITHUB_ENV/GITHUB_PATH because a
composite action cannot see who calls it; the values come from the
Visual Studio install and `vcvarsall`, and the only caller is a release
job running on a maintainer-signed tag, so the audit is suppressed on
that step with that justification.
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.
* fix: include sponsors in v12 release descriptions
The v11 release description comes from `pn make-release-description`, which
appends the sponsors table. v12 builds its description in the workflow by
dumping the pending changelog, so its release pages never showed sponsors.
Adds .github/release-sponsors.md — a checked-in fragment carrying the same
platinum and gold tables with release_notes attribution — and cats it onto
RELEASE.md in the Rust release job. A missing fragment warns rather than
fails; the sponsors table is not worth losing a release over.
The fragment is regenerated from pnpm.io's sponsors.json alongside the
READMEs and the v11 release text.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: read the sponsors table from the shared fragment in v11 too
make-release-description inlined its own copy of the sponsors tables — 109
lines of HTML that had to be regenerated in lockstep with the READMEs. Now
that v12 reads a fragment, v11 can read the same one.
getChangelogEntry goes back to returning just the changelog section, and
writeReleaseText appends the fragment. A missing fragment warns and writes
the description without the table, matching the v12 job: the workflow falls
back to a diagnostic description when this script fails, so throwing here
would trade a missing sponsors table for missing release notes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: assert the sponsors fragment is appended exactly once
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Rust CLI ships as `pnpm`; `pacquet` survives only as the in-repo
package identifier that keeps the npm wrapper from colliding with the
TypeScript CLI's name. Carrying that identifier into all 75 Cargo crate
names made every `use` statement and every `cargo nextest run -p ...`
spell a name users never see. This renames the crates to the product
they belong to.
Mechanical, name-only: package/lib/bin names, the root
`[workspace.dependencies]` keys, every dependency entry, and every Rust
path reference. All the crates are `publish = false`, so nothing about
the released artifacts changes and no changeset is needed.
Also updated the tooling and docs that spell crate names: the `codecov`
cargo alias, the `registry-mock` just recipe, the release workflow's
`-p` targets and `libpnpm_napi.*` artifact paths, the `libpacquet` ->
`libpnpm` cspell dictionary entry, and the crate names quoted across
`AGENTS.md`, `CONTRIBUTING.md`, `CODE_STYLE_GUIDE.md`, and the plans.
Two spots needed more than a token swap. The five insta snapshot files
carry the crate name in their filename, since insta derives it from the
module path, so they are renamed too — otherwise the tests fail with
"snapshot not found" rather than a diff. And the private module in
`pnpm-lockfile-verification` that mirrors the npm verifier's violation
codes is named after the crate it shadows, so it moved with it.
Two knock-on fixes: the shorter names let rustfmt fit three macro
invocations onto one line, where `perfectionist::macro_trailing_comma`
rejects the trailing comma that was legal while they were wrapped; and
the `libpacquet` cspell entry became `libpnpm` for the NAPI addon's
shared library.
Left alone deliberately, none of which are crate names: the product name
in prose and in the changeset package identifier, the `--pacquet`
benchmark flag and testbed IDs, test helpers and temp-dir prefixes
(`pacquet-test-*`, `../pacquet-store`), the released changesets that
quote error codes shipped versions actually emitted, the
`.github/workflows/pacquet-*.yml` filenames, and the
`@pnpm-private/pacquet-registry-mock-launcher` npm package.
Adds `gvs-linker.fresh-restore.hot-cache.hot-store` to the integrated
benchmark job, feeding both Bencher testbeds.
The harness has carried this scenario for a while, but nothing invoked
it: every scenario the workflow ran pins `enableGlobalVirtualStore:
false` in its fixture. Since pnpm/pnpm#13940 that pin is the opposite
of what users get, so the layout pnpm 12 installs into by default had
no continuous measurement at all — a regression in slot reuse could
land without any gate noticing.
It pairs with the isolated fresh-restore hot/hot scenario: same frozen
install, same warm cache and store, and the per-iteration wipe takes
`node_modules` either way, so the difference is what the layout is
worth. Measured locally over 5 runs: 187.8 ms ± 5.6 shared versus
1.047 s ± 0.119 project-local, because the materialized package
directories survive the wipe under the shared store and only the
symlinks have to be rebuilt.
Both testbeds get the scenario. `pnpr@HEAD` under the shared store had
never been exercised — the pre-warm site notes it as hypothetical — so
it was run end to end first: the server starts, both targets pre-warm,
and both installs measure clean.
Corepack installs no dependencies and runs no lifecycle scripts, so the
`@pnpm/exe.<target>` package that carries the native binary is never
installed and the preinstall that links it over the placeholder bins never
runs. `corepack use pnpm@next-12` therefore died with MODULE_NOT_FOUND
(Closespnpm/pnpm#13018).
Corepack hardcodes `./bin/pnpm.mjs` and `./bin/pnpx.mjs` as the entry
points of every pnpm >=11, so ship them. They resolve the native binary —
from the platform package when a package manager installed one, otherwise
by downloading the pinned `@pnpm/exe.<target>` tarball from the registry
and verifying it against the checksum the registry published — and hand
over to it. The binary is cached next to the wrapper, where it also finds
the `dist/` payload node-gyp ships in, so only the first run downloads.
`package.json#bin` is untouched: an ordinary install still points at the
native binary and pays no Node.js startup. This is the compatibility
wrapper Yarn 6 ships for the same reason, adapted to how pnpm distributes
its binary (https://github.com/nodejs/corepack/pull/887#issuecomment-5292891262).
Registry access mirrors Corepack's own environment (COREPACK_NPM_REGISTRY,
COREPACK_NPM_TOKEN, COREPACK_NPM_USERNAME/PASSWORD, COREPACK_ENABLE_NETWORK),
and the download has no dependencies of its own, since Corepack would not
install those either.
With a configured pnprServer, every install paid a full server exchange
even when the answer was already on disk, making pnpr strictly slower
than a direct install on up-to-date projects. Close that gap in three
layers, each deciding locally that the server has nothing to add:
- Let the pre-runtime "Already up to date" fast path run with a pnpr
server configured. The check decides purely locally that nothing
changed since the last install; asking the server cannot change that
answer. The trust/policy settings guard pnpm already records in the
workspace state (trustPolicy, trustPolicyExclude,
trustPolicyIgnoreAfter, minimumReleaseAgeStrict,
minimumReleaseAgeExclude) is now recorded and compared by pacquet
too, so a policy change still defeats the fast path.
- Gate the resolve exchange on a local satisfaction check: an
unfiltered install whose on-disk lockfile still satisfies every
manifest goes straight to the frozen materialization, exactly like
the non-pnpr preferFrozenLockfile dispatch.
- Consult the local lockfile-verified.jsonl cache before delegating
input-lockfile verification to the server, and record server-verified
and server-resolved lockfiles into that cache (pnpm's
writeWantedLockfileAndRecordVerified), so repeat verifications of an
unchanged lockfile stay local.
Add repeat-install scenarios (populated node_modules, hot and cold
cache) to the integrated benchmark, pre-warmed before hyperfine runs
and wired into the pnpr-not-slower-than-direct gate with an absolute
slack for tens-of-milliseconds runs.
Closespnpm/pnpm#13904
## Summary
- Restore the Rust CLI default that freezes an existing non-empty `pnpm-lock.yaml` in CI, so an outdated lockfile fails without being rewritten.
- Preserve explicit `frozenLockfile` and `preferFrozenLockfile` choices from CLI flags, trusted configuration, environment variables, and `.pnpmfile.cjs` hooks.
- Recognize `CI=1`, `CI=true`, and GitHub Actions; let explicit `CI=false` opt out, and prevent repository-controlled `pnpm-workspace.yaml` from overriding CI detection.
- Keep missing, byte-empty, and semantically empty lockfiles writable, and isolate CI-sensitive spawned commands in the Rust test workflow.
The TypeScript CLI already has this behavior; this brings the Rust `pnpm/` port back into parity.
Fixespnpm/pnpm#13760.
The Ecosystem E2E workflow failed on every run since it was added, for
two stacked reasons.
The matrix jobs relied on the runner image's system Node (v20), while
the built pnpm bundle needs at least v22.13 for node:sqlite, so every
scaffold died with ERR_UNKNOWN_BUILTIN_MODULE before any cell ran. The
jobs now install the devEngines.runtime pin via pnpm/setup, the same
way the workflow's build job already did.
With scaffolding unblocked, the angular and nuxt global-virtual-store
cells failed in both binaries: "@angular/build" requires tslib and
"@nuxt/vite-builder" (since v4) imports unplugin without declaring
them, and the global virtual store only exposes declared dependencies.
Since neither entry is in the "@yarnpkg/extensions" compatibility DB
yet, carry them as pnpm-specific compatibility packageExtensions in
both stacks, merged after the upstream DB and gated by the same
ignoreCompatibilityDb setting. Drop an entry once the upstream DB or
the package itself declares the dependency.
Make pnpr npm publication resumable after a partially successful run. Check each native package's publication state, skip immutable versions that already exist, and publish only missing packages.
Treat lookup failures as fatal rather than as missing versions, and continue publishing the wrapper only after every native package is confirmed available.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
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
pnpm v12 shipped no node-gyp, so any package that shells out to it during
an install script died with `spawn node-gyp ENOENT`: `node-gyp-build` with
no matching prebuild, `node-pre-gyp`, a plain `"install": "node-gyp
rebuild"`, and the `node-gyp rebuild` script pnpm synthesizes for a package
that ships a `binding.gyp` without one. Embedders worked around it by
putting their own node-gyp on the lifecycle PATH.
pnpm 11 solves this without a JS bundle: node-gyp is declared `external`,
and `pnpm deploy` resolves its whole tree from this repo's lockfile at
release time into a `dist/node_modules/node-gyp` directory that sits beside
the entry point, with `dist/node-gyp-bin` holding the wrappers that go on a
lifecycle script's PATH. Those are ordinary files next to the executable,
so the native binary can ship them the same way — nothing about the
mechanism depended on there being a bundle to hide node-gyp inside.
Resolving the tree at release time rather than at install time is the point,
not an implementation detail: the dependency tree is frozen and reviewed per
pnpm release instead of being fetched from the registry at the moment an
install script is about to execute arbitrary code.
scripts/bundle-node-gyp.mjs builds the payload, reusing the TypeScript CLI's
wrapper scripts so the two stacks cannot drift on how a script's node-gyp is
dispatched, and refusing to produce a partial dist/ — a PATH entry that
resolves nothing is worse than shipping none at all. Both wrappers ship it,
since each places the binary next to itself.
node-gyp moves to a catalog entry so both stacks pin one version; the
Node.js-floor hold-back note moves with it.
At runtime bundled_node_gyp_bin finds the wrapper dir relative to the
executable and returns None when nothing was shipped, which is what a
checkout build sees — lifecycle scripts then fall back to whatever node-gyp
the environment provides, exactly as before. The wrapper itself honors
npm_config_node_gyp and only falls back to the shipped copy when it is
unset, so a user-supplied node-gyp, a workspace one, and a package's own
dependency all keep winning.
Refs https://github.com/pnpm/pnpm/issues/13580
The peer-heavy resolver fixture has a wide run-to-run spread — a local 15-run
pass measured a ~15% coefficient of variation on an idle machine, with the slow
samples scattered through the sequence rather than clustered at the start, so
the noise is runner contention rather than a cold cache. Three CI runs against
that spread produced a 2.83s outlier on pnpm/pnpm#13551 that lifted the pacquet
mean to 2.15s while its fastest run sat at 1.54s, inside main's range.
Compare the cross-engine gate on each target's fastest run instead of its mean.
Contention only ever adds time, so the minimum is the sample least perturbed by
a noisy neighbour; this is already the statistic the workflow reports to
Bencher, for the same reason. Raise the scenario to nine runs so that minimum
has a good chance of landing on an uncontended run, and surface both mean and
min in the diagnostics table.
The gate is a correctness check, not a measurement, and it was costing us the
measurement: asserting mid-job aborted before the Bencher upload and dropped
every scenario's data point for the whole run. That is why this benchmark has
almost no history on bencher.dev — the two main pushes after it was added both
failed on the since-recalibrated 10x floor, so only one point was ever
published. Record the verdict, let the upload proceed, and re-raise the failure
afterwards.
Pacquet-only benchmark-harness and CI change; no user-visible surface, no
TypeScript counterpart, and no changeset.
Stream lazy child resolution through caches and share ancestry, missing-peer summaries, package
data, and dependency paths across repeated peer contexts. Retain only discovery nodes needed as
peer providers instead of occurrence state for every cache-served visit.
Index peer-provider children once per evolving workspace graph so cached peer lookup and lazy
provider previews avoid repeatedly scanning every dependency edge.
Recursively hoist nested conflict roots so the hoisted linker does not expand a small dependency
graph into deeply repeated node_modules paths.
Add a deterministic integrated benchmark that compares the peer-heavy resolver against main and
TypeScript pnpm while enforcing byte-identical lockfiles.
Closespnpm/pnpm#13540.
Picks up pnpm/update#4: the pin bump now happens first, so the
dependency update and the refreshed lockfile are produced by the pnpm
version the update PR moves the repository onto.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Bump pnpm/update to the release that adds the
update-pnpm-minimum-release-age input, and set it to 0. pnpm 12
defaults minimumReleaseAge to 24 hours and self-update deliberately
ignores the repo's minimumReleaseAgeExclude, so the job could never
bump the pin to a pnpm release published the same day: the next-12
dist-tag silently resolved to the newest mature version instead.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The ubuntu Node 24 smoke job previously gated the Node 22/26 test runs,
delaying them by a full test cycle. Fold the Node 24 (garnet) leg into
the test matrix and gate every test job only on compile-and-lint plus
its platform's pnpr build, so all Node.js versions and all Windows
chunks start in parallel. The trade-off is that a broken build now
surfaces on all test jobs at once instead of costing a single smoke run.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
pnpm login refused to run whenever stdin or stdout was not a TTY, even
though the registry web-auth flow only prints an authentication URL and
polls the done endpoint until the browser approval completes - neither
needs a terminal. Agent- and CI-adjacent tooling had to wrap pnpm in a
pseudo-terminal (script -q /dev/null pnpm login) to use the web flow.
Move the non-interactive guard from the top of the login command into
the classic username/password fallback, the only path that prompts on
the terminal. Without a TTY the web flow now prints the authentication
URL and polls as before; the URL is printed without the QR code (a
piped stdout cannot render the block art), and the press-ENTER browser
prompt was already skipped for a non-TTY stdin. A registry without web
login support still fails with ERR_PNPM_LOGIN_NON_INTERACTIVE.
Harden the TypeScript web-login path to match pacquet while touching
it: narrow the attacker-controlled response body at runtime (a missing,
empty, or non-string loginUrl/doneUrl is an invalid response), and
reject URLs containing Unicode control characters with pacquet's
ERR_PNPM_AUTH_COMMANDS_LOGIN_UNSAFE_URL before anything is printed or
polled. The shared error message now says "authentication URL" in both
stacks, since the check covers loginUrl and doneUrl alike.
Implemented in both stacks: the TypeScript CLI moves the guard into
classicLogin and prints a URL-only message via the new
formatAuthUrlOnlyMessage export of the web-auth package; pacquet moves
the same guard into classic_login and selects AuthUrlMessage::UrlOnly
when stdout is not a TTY. The pacquet CLI adapter unit test and the
CLI-tier integration tests now drive the guard through a 404 web-login
probe so it exercises the classic fallback, and a new integration test
covers the headless web flow end-to-end against a mock registry.
Also acknowledge a pre-existing zizmor ref-version-mismatch finding on
the winget-releaser pin in update-latest.yml with the repository's
usual inline ignore: the pinned commit is no longer reachable from any
named ref upstream, so no version comment can describe it accurately.
The first pnpm 12 beta release used pnpm 12.0.0-alpha.21 to publish a wrapper whose workspace name is `pacquet`. That version predates `publishConfig.name`, so npm trusted publishing was attempted for `pacquet` instead of `pnpm` and the root package failed after all native packages had already been published.
Use pnpm 11.18.0, the released TypeScript CLI that supports `publishConfig.name`, for release tooling. Update every `pnpm/setup` consumer to the revision that can install v11 from GitHub release archives. Make the Rust publishing loop query each effective published name and skip versions already on npm, while preserving hard failures for registry errors other than 404. This allows a moved beta tag to resume the partial release and reach the dependent GitHub release job.
The Pacquet Code Coverage job has been failing on main and on every PR
with `System.IO.IOException: No space left on device`, thrown while the
runner worker writes its own diagnostic log — so the run reports a
failure with no step output at all.
The job compiles the whole workspace with coverage instrumentation and
`--all-targets`, keeps a profraw file per test process for all 7167
tests, and then merges them, but it never dropped the preinstalled
toolchains the way `Rust CI / Test`, `Test` and `build-pnpr` do. Reclaim
the same ~25 GB before anything is compiled, and build the instrumented
artifacts with line-tables-only debug info: llvm-cov reads its regions
from the binary's `__llvm_covmap`/`__llvm_covfun` sections, so the DWARF
that dominates the debug build buys the report nothing.
Also report the free disk after the run and cap the job at 60 minutes,
so the next disk regression shows up as a number instead of a silent
worker death.
* chore: update dependencies, Node.js, pnpm, and GitHub Actions
* chore: hold typescript on 6.x for typescript-eslint compatibility
The update-lockfile job bumped the `typescript` catalog entry to 7.0.2,
which typescript-eslint refuses to load against — it hard-errors at load
with "typescript-eslint does not support TS 7.0" — failing the
Compile & Lint job.
The 7.x compiler is already used for the build via
`@typescript/native-preview` (tsgo); the `typescript` npm package only
serves the JS-API tooling (eslint, jest), which needs 6.x until
typescript-eslint adds TS >=7.1 support
(https://github.com/typescript-eslint/typescript-eslint/issues/10940).
Revert the catalog entry to 6.0.3 and add `typescript` to
`update.ignoreDeps` so the update-lockfile workflow stops re-bumping it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: drop the stray typescript@7.0.2 from the lockfile
openpgp declares an optional `typescript` peer (>=4.7). After the catalog
was reverted to 6.0.3, pnpm's incremental resolution kept openpgp latched
onto the leftover typescript@7.0.2 node, leaving it (and its native
platform packages) in the lockfile and tripping peer-mismatch review bots.
Re-point openpgp's optional peer to 6.0.3 so typescript@7.0.2 is gone
entirely; the openpgp subtree is now identical to the base branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pnpm bump ran `pnpm version -r` unfiltered, consuming the entire changeset
ledger, so every release flushed the TypeScript CLI's pending intents alongside
the Rust products' alpha prereleases. A rapid v12 alpha cadence therefore forced
an equally rapid v11 release cadence — and with it a v11 "Update available!"
notification almost every day.
Give create-release-pr.yml three checkboxes — pnpm11 (the TypeScript CLI),
pnpm (the Rust CLI, v12 alpha), and pnpr — in any combination. bump.ts
translates the selected products into `pnpm version -r --filter` arguments,
consuming only their intents and leaving the rest in the ledger. The default
lane (pnpm11) has no single positive selector, so it is expressed as the
complement of the unselected alpha products (an exclude-only filter selects
every project minus those); selecting all three yields no filter — a full
release. The embedded trust-root refresh steps run only when pnpm11 is selected,
since those keys ship in the TypeScript CLI.
Argument parsing fails closed: an unrecognized token (e.g. a `--releases` typo)
throws instead of leaving the selection empty and releasing every product; a
bare `pnpm bump` with no arguments still cuts a full release. The version
command is built as an argv array (execFileSync, no shell) so a filter value is
never interpreted by a shell.
Related to pnpm/pnpm#13264.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci(triage): time out hung Oz triage runs instead of waiting 6h
Triage runs occasionally wedge inside the `Triage with Oz agent` step
before the agent's first turn. The job then sits until GitHub's 6h
ceiling cancels it, so the issue is never labelled and never gets a
comment explaining why. Eight runs have hit this.
Successful triage runs complete in ~3 min (13 min worst case observed),
so a 20 min step timeout leaves ample headroom while turning a silent 6h
stall into a red run you can see and re-run.
Co-Authored-By: Oz <oz-agent@warp.dev>
* remove timeout comment block 1
* remove timeout comment block 2
---------
Co-authored-by: Oz <oz-agent@warp.dev>
* fix(pnpm): stop stripping the manifest on disk when bundling deps
bundle-deps.ts deleted dependencies and devDependencies from
pnpm11/pnpm/package.json on disk, a workaround kept from when the
release-pinned pnpm did not honor the beforePacking hook
(pnpm/pnpm#12955). The hook handles it now, and the on-disk strip is
what killed the v11.17.0 release: after build-artifacts ran it, the
payload-verification step tripped the verifyDepsBeforeRun gate, whose
spawned install synced node_modules to the stripped manifest and
removed the pnpm package deps that the artifact packages
prepublishOnly (verify-binary.mjs) needs during publish. The packed
manifest stays dependency-free via the hook, asserted by the release
workflow before the first publish.
* fix(release): drop target_commitish from the Rust draft release
Creating the Rust draft release fails with 403 "Resource not
accessible by integration": with immutable releases enabled, passing
target_commitish makes release creation fail for the workflow token
(the v12.0.0-alpha.19 draft failure, reproduced on rerun). The input
never did anything - GitHub ignores it when the tag exists, and the
assert step already pins the commit.
* ci: update pnpm/setup to the v12 standalone-executable revision
Since pnpm/setup#11 the action downloads the standalone pnpm v12
executable directly from the registry — no npm bootstrap, no system
Node.js dependency. Every workflow moves to the new revision except
update-latest.yml: its RELEASE_TOOL_PNPM_VERSION deliberately pins a
proven pnpm 11, which only the pre-v12 action revision can install, so
it keeps the old pin until that version moves to v12.
* ci: operate the Tag workflow with pnpm v12
RELEASE_TOOL_PNPM_VERSION moves to 12.0.0-alpha.19 and the pnpm/setup
pins to the v12 revision. The commands the workflow runs are rewritten
to forms both CLIs accept, since the Rust CLI does not (yet) expose
--registry, --allow-build on add, or a post-subcommand --dir:
- --registry becomes --config.registry (read by both stacks)
- the add build-script approval moves from `--allow-build=@pnpm/exe` to
an allowBuilds entry in the scratch dir pnpm-workspace.yaml
- --dir moves ahead of the subcommand
Each rewritten command was verified against the 12.0.0-alpha.19 binary,
including the verify-upgrade sequence end to end (add of
`@pnpm/exe@11.16.0` with the build approval, bin shim run) and the
config set/get/delete round trip for the auth token.
* revert: operate the Tag workflow with pnpm v11 again
Moving the operator to v12 required rewriting commands around three
CLI surfaces pacquet is missing (--registry, --allow-build on add,
post-subcommand --dir). The Rust CLI must gain those for parity instead
of the workflow papering over them — tracked in
https://github.com/pnpm/pnpm/issues/13242. The workflow keeps the
pre-v12 pnpm/setup pin and pnpm 11.13.1 until that lands.
* chore(release): drop the packing workarounds now that pacquet 12.0.0-alpha.19 is pinned
The pinned pacquet now carries the fs-packlist fix from
https://github.com/pnpm/pnpm/pull/13231, so a files allowlist wins over
workspace-inherited ignore rules and the release workflow no longer
needs to neutralize them: remove the empty pnpm11/pnpm/.npmignore
override, the temporary root .gitignore shadow, and the pre-publish
payload verification step (with its script) that guarded against the
now-fixed empty-tarball packing bug. The payload-verify step was also
what broke the v11.17.0 release: its filtered pn list fired a
verifyDepsBeforeRun auto-install that pruned the excluded pnpm
project's node_modules, so the darwin-arm64 artifact's prepublishOnly
could no longer resolve symlink-dir.
`@pnpm/prepare` and `@pnpm/prepare-temp-dir` were the only
publishable packages without a files allowlist; their lib/ payload
survived packing only through the .gitignore shadow. Give them files
allowlists so their tarballs no longer depend on workspace ignore
state.
* chore: record the pacquet 12.0.0-alpha.19 pin in the lockfile
* chore: drop the changeset - both files-allowlist fixes ship without a new version
The pending `@pnpm/prepare` 1100.0.22 is not on npm yet, so the rerun
of the v11.17.0 release publishes it with the files allowlist already
in place. `@pnpm/prepare-temp-dir` 1100.0.1 is already published with
correct tarball content (packed under the .gitignore shadow), so its
manifest-only change needs no republish. A changeset would only
schedule a patch release whose sole delta is the manifest field.
The verify step ran pn compile-only, whose typecheck-everything graph
includes test tsconfigs; the release runner install does not provide
the test-only devDependencies, so the v11.17.0 release failed with
TS2307 errors before verification began. Build only the publishable
projects own tsconfigs instead: the packed payloads need nothing else,
and the build-artifacts step already proved the src graph compiles in
this environment.
Add a workflow that fires on the `issues: labeled` event and, when the added
label is `regression`, posts an alert to a dedicated Discord channel via the
DISCORD_WEBHOOK_REGRESSION secret. This gives the team a real-time ping
whenever the Oz triage agent or a maintainer tags a regression.
Issue fields are passed through env vars and read with `jq --arg` rather than
inlined into the shell, so a crafted issue title cannot inject shell code.
`allowed_mentions: {parse: []}` prevents a title like `@everyone` from pinging
the server, and the job runs with empty permissions since the only outbound
call is the webhook. The step is skipped when the secret is unset.
Also fix the regression report issue template, which declared the nonexistent
`type: regression` label; the real repository label is `regression`. Aligning
the template means template-filed reports are auto-labeled and therefore
auto-announced.
Pack every publishable workspace package in dry-run mode before the
first immutable npm publish and verify each reported file list against
the manifest-declared payload (files, main, module, types, exports,
browser, bin, publishConfig.executableFiles). A packing regression like
the one that shipped nearly-empty lib tarballs for five releases
(pnpm/pnpm#13164) now fails the release job before anything publishes.
The verification is one pn compile-only (pack does not run
prepublishOnly), four concurrent `pnpm pack --dry-run --json` chunks
(recursive pack packs one project at a time and, unlike recursive
publish, does not skip private packages — so publishable projects are
selected by explicit name filters), and a single node pass over the
reported file lists. No tarballs are written or read.
Also give @pnpm/modules-mounter.daemon the lib/index.js entry point its
manifest declares; the verifier caught that it never existed.
Fixespnpm/pnpm#13179
Since pacquet started honoring workspace-root ignore files when packing,
collect_own_files fed them to the walker even when the manifest declares
a files allowlist. A workspace package whose compiled lib/ directory is
gitignored at the workspace root then published a tarball whose lib/
contained only the force-included main file: the ancestor rule excluded
lib/** before the files-field filter ever saw it. npm-packlist (and the
TypeScript `@pnpm/fs.packlist` built on it) lets the files field
override ancestor ignore rules, so every `@pnpm/*` lib package published
by pacquet since 12.0.0-alpha.13 shipped nearly empty.
Apply workspace-inherited ignore files only in tiers b/c of the
root-priority rule, i.e. when the manifest has no files allowlist.
Because the TypeScript release publishes with the packageManager-pinned
pacquet and no pacquet-only release can be cut first, also add a
TEMPORARY release-workflow step that shadows the root .gitignore with a
junk-only root .npmignore before packing, so the next release ships
complete tarballs even with a broken pinned pacquet. Remove it once the
pin carries this fix.
Broken tarballs on npm are immutable, so a changeset patch-bumps every
published lib package plus the pnpm CLI to force a full republish on
the next release.
Fixespnpm/pnpm#13164
Related to pnpm/pnpm#13179
The action reproduces this workflow's pattern (persistent chore/update-lockfile
branch off latest main, force-pushed each run, one open PR reused via a
gh-pr-list check), so the bespoke Prepare/Check/Commit/Create steps collapse into
a single `uses:` step. Input mapping:
- update-deps: latest + refresh-lockfile: true -> delete node_modules and
pnpm-lock.yaml, then `pnpm update --latest -r` for a full refresh.
- github-actions: true -> also bump the actions pinned in .github/workflows and
action.yml. UPDATE_LOCKFILE_TOKEN already carries the workflow scope (it pushes
the meta-updater's workflow changes today).
- Node.js: derived from devEngines.runtime (no hardcoded `26`), staying on the
pinned major.
- update-pnpm: next-12 -> `pnpm self-update next-12`.
- post-update: pnpm update-manifests.
- changesets left on (default): `pnpm update --changeset` records a changeset
only when a published package's production/peer deps change, and stays silent
on devDependency-only bumps.
- token: secrets.UPDATE_LOCKFILE_TOKEN, pushed via a single-use credential helper
(works with persist-credentials: false; referenced only in the action's push
step, never during install).
The action is SHA-pinned (76f98d72, tagged v0), matching how actions/checkout and
pnpm/setup are pinned here.
A task that found the per-URL tarball mem-cache slot InProgress released
the RwLock read guard and only then polled Notify::notified().
notify_waiters() stores no permit - it wakes only futures already
registered at that instant - so an owner that flipped the slot to
Available/Failed and notified inside that window left the waiter parked
forever. The window needs the owner to complete its flip on another
worker thread while the waiter is preempted between the guard drop and
its first poll, which is why it surfaced only as a rare hang on an
oversubscribed CI runner: pacquet-cli::add
save_prefix_empty_writes_exact_version (a 0.2s test) hung for over an
hour and wedged the Rust CI job at
https://github.com/pnpm/pnpm/actions/runs/29705938247.
The waiter path runs on every cold add/install: the resolve-time
prefetcher owns the fetch and the install pass waits on the same slot.
A detached prefetch task losing the same race instead pinned the
store-index writer channel open and hung the final writer_task.await -
same root cause, either direction.
Fix per the documented tokio pattern: pin the Notified future and
enable() it to register interest before re-checking the slot state,
awaiting only while the slot is still InProgress, in a loop.
The exact interleaving cannot be reproduced deterministically without
loom (it requires preempting the waiter mid-poll), so no regression
test accompanies the fix. Instead, CI gains guardrails so a recurrence
is a visible failure, not a wedged job: a workspace .config/nextest.toml
sets slow-timeout terminate-after = 5 (a test stuck past 300s is killed
and reported as TIMEOUT with output; ~3.5x headroom over the slowest
legitimate retry-ladder tests), and the Rust CI test job is capped at
timeout-minutes: 60 as a backstop for non-test hangs.