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>
Strengthen the duplicate-PR guidance to point at the issue's
automatically linked PRs, add an AI-assisted contributions section
(agents are welcome, but contributors own the output — check linked
PRs, understand the change, run the tests, keep the diff focused,
disclose agent authorship with the standard footer), and document that
human review starts only after CodeRabbit approves and CI is green.
CodeRabbit is the single required AI approval — Qodo ignores
Dependabot/Renovate PRs, so a two-reviewer gate could never be
satisfied for dependency updates; the PR template is aligned
accordingly.
Multiple contributors keep submitting parallel PRs for the same issue
(e.g. pnpm/pnpm#13114 was superseded by pnpm/pnpm#13124 for issue
pnpm/pnpm#13108). GitHub already cross-links every PR that references
an issue on the issue's timeline, so add a checklist item prompting
authors to check those links before submitting.
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.
The plan job released every product whose committed version was missing
from npm, regardless of which tag triggered the run. Combined with the
shared release concurrency group - GitHub keeps at most one pending run
per group and cancels the rest - a multi-product release PR could get a
product's run evicted from the queue and its release performed under a
sibling product's tag. This published the v11.15.0 artifacts and notes
on the v12.0.0-alpha.15 release page
(https://github.com/pnpm/pnpm/actions/runs/29644133887).
Now the tag is the dispatch key: a run releases exactly the product
whose committed version the tag names, and a tag matching no product
fails the plan. The npm check remains as an idempotency guard, so
rerunning a tag resumes a partial release and a completed tag skips
every job. The concurrency group is scoped per ref so simultaneous tags
run in parallel instead of sharing a queue that evicts pending runs;
per-product runs publish disjoint packages, making parallelism safe.
The TypeScript GitHub-release step additionally pins tag_name and name
to the planned version, and both GitHub-release jobs assert that the
version tag points at the commit the run built, since the dispatch
matches version strings and cannot prove tag-commit identity. The Rust
job's target_commitish cannot enforce this - GitHub ignores it whenever
the tag already exists.
Report each scenario's minimum latency to Bencher instead of the mean, so the
threshold tracks the min. On shared CI runners the timed work is CPU/disk-bound
(the simulated latency only adds a deterministic floor, and the noisiest
scenario runs --offline with no network), so the mean drifts run-to-run with
host contention — byte-identical binaries differed ~30% on the offline scenario
and tripped a spurious alert.
Contention only ever adds time, so the minimum is the run least perturbed by a
noisy neighbour and the truest regression signal. Emit Bencher Metric Format
and upload the integrated benchmark with --adapter json, reporting the min
honestly as the value (nanoseconds, min seconds x 1e9, to match the latency
measure's unit) rather than relabelling hyperfine's mean field. The TS
test-duration and micro-benchmark uploads keep shell_hyperfine (single-sample,
so min equals mean there). Because the min only needs one uncontended run,
hyperfine's default sampling suffices. The human comment still shows mean +/- σ.
Note: the workflow_run-triggered comment workflow runs from the default branch,
so this PR's own benchmark-comment upload fails (main still uses
--adapter shell_hyperfine); it self-heals once this lands on main.
Resolve package selectors concurrently during pacquet add instead of waiting for
each selector's registry request before starting the next one.
Share packument metadata state across the concurrent resolutions. Buffer catalog
warnings and process the completed results in selector order to keep warnings and
error selection deterministic.
Add regression coverage that proves registry requests overlap while preserving
selector-ordered warnings and errors.
Restrict the Rust CI pnpm cache to CAS files so Windows runners do not restore
invalid global-virtual-store junctions.
Closespnpm/pnpm#13089.
Two published v11 releases were broken in ways nothing in the pipeline
checked, and both only surfaced once users upgraded onto them.
11.12.0 was packed by a pnpm that ignores the .pnpmfile.cjs beforePacking
hook, so the bundled dependency fields survived into the published
manifest. Resolving node-gyp then pulled a peer-suffixed snapshot into the
env lockfile, and every upgrade onto 11.12.0 died in
buildLockfileFromEnvLockfile (pnpm/pnpm#12955, pnpm/pnpm#12959).
11.12.0 and 11.13.0 also shipped `@pnpm/exe` platform packages with no
native binary; setup.js exits 0 when the binary is missing, so the
placeholder bin survived and only a real invocation caught it.
Assert on the packed tarball that the published pnpm manifest declares no
dependency fields, rather than trusting either stripper, since npm
publishes are immutable. Then, in the Tag workflow, upgrade from the
release line's current version onto the new one for both the `pnpm` and
`@pnpm/exe` wrappers and run the resulting binary. The release workflow has
already published under next-<major> at that point, so this reads the real
registry artifact, and it runs before the dist-tags move — the last gate
before a version reaches everyone.
Verified against the registry: the assert rejects pnpm@11.12.0 and accepts
11.11.0/11.13.1; the upgrade gate passes 11.13.0 -> 11.13.1 and catches
both `pnpm@11.12.0` and `@pnpm/exe@11.12.0`/11.13.0.
Related to pnpm/pnpm#12959.
The native release flow parks registry-mode changelog sections under
`.changeset/changelogs`, so use that as the only source for generated release
notes instead of the stale committed `CHANGELOG.md`.
Release-note metadata must not block publishing valid artifacts. If generation
fails, write a diagnostic release body with a link to the workflow run. Create
the GitHub release as a draft, compare its persisted body exactly with the
generated or diagnostic file, and publish it without resending the body.
The TypeScript release preflight rejected valid empty files and allowed the
workspace root gitignore to remove the CLI dist directory from the pnpm
tarball. Its executable installation test also ran npm from inside a checkout
whose devEngines requires pnpm.
Accept empty regular files while retaining byte-for-byte hash verification,
isolate the pnpm package from workspace ignore rules before building, and run
the npm lifecycle smoke test from its temporary install directory. Add a
manual verification-only dispatch that exercises the complete artifact build
and smoke test while keeping every publish job disabled.
Release jobs could package and publish generated native or executable payloads
without a release-time signal if a packlist regression omitted a required file.
Add read-only verification jobs before each trusted-publishing job. They pack every
generated package, verify its declared literal payload files against the source
artifacts, and smoke-test the v11 plain-JS CLI, the v11 executable wrapper
with its native package, and the pnpr wrapper with its Linux native package.
Keeping the smoke tests outside OIDC-enabled publishing jobs prevents an
artifact executed during verification from obtaining a trusted-publishing token.
The verification tarballs live outside package directories so they cannot become
part of subsequently published archives.
Move non-npm-trusted-publishing GitHub Actions jobs to Blacksmith runner labels.
Use larger Blacksmith runners for heavier build, Docker, benchmark, Windows, and macOS jobs, while leaving the npm OIDC trusted-publishing publish jobs on GitHub-hosted runners. npm trusted publishing currently does not support self-hosted runner providers, so the TypeScript pnpm publish job, Rust pnpm publish job, and pnpr publish job keep their GitHub-hosted labels.
Keep benchmark artifact names stable so follow-up workflow_run jobs on the default branch keep matching the producing benchmark jobs.
* 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>
* ci: auto-tag released versions when the release PR merges
The release tag was pushed by hand after merging a release PR, which let
the tag name drift from the version committed on the merge commit. When
they disagree, release.yml's plan job publishes the committed version but
github-release-rust drafts a GitHub release for `v<committed-version>` — a
tag that doesn't exist as a git ref — and the create-release call returns
403 "Resource not accessible by integration" even with contents: write.
Add tag-release.yml: on a merged `release-pr/*` PR it derives each
product's tag from the committed manifest (the same manifests plan reads),
skips versions already tagged on the remote, and pushes the new tags with
UPDATE_LOCKFILE_TOKEN so the push triggers release.yml (a tag pushed with
the default GITHUB_TOKEN would not). Deriving the tag from the committed
version makes the off-by-one impossible, and the tag exists as a real ref
before release.yml runs, so the draft-release step no longer 403s.
A release PR can bump several products at once, landing several tags and
starting a release run apiece; add a `release` concurrency group so they
serialize and plan's already-published gate makes the later runs no-ops.
Drop the now-obsolete "push the version tag after merging" line from the
release PR body.
* ci: harden the auto-tag workflow per review
- Gate on create-release-pr.yml's exact contract (same-repo head branch
named `release-pr/<base>`) instead of a `release-pr/` prefix, so a fork
PR or an unrelated same-prefixed branch can't reach the PAT tag push.
- `set -euo pipefail` so a failed `git ls-remote` aborts instead of
yielding an empty tag set that makes every version look new.
- `git ls-remote --refs` to drop annotated tags' peeled `^{}` entries.
- Reword the header comment to state the current contract rather than
what it replaces.
* ci: tag pnpr in its own namespace to avoid pnpm tag collisions
pnpr's version starts at 0.x, and pnpm's tag history already holds 166
v0.x tags (v0.1.0, v0.2.0, …) plus the v1.x line. Tagging pnpr as
`v<version>` shares that namespace: once pnpr cuts a stable 0.x that
matches an old pnpm tag, the existence check would treat the ancient
pnpm tag as pnpr's and silently skip it, so pnpr would never get its
trigger tag or release.
Give pnpr its own `pnpr-v<version>` tag (it has no GitHub release and no
external consumer of the tag — it ships via npm and GHCR — so it's free
to move) and trigger/validate release.yml on `pnpr-v*.*.*` alongside
`v*.*.*`. plan reads committed manifests regardless of the triggering
tag, so a pnpr-only release still publishes correctly.
* ci: adopt the Changesets-style pnpr tag scheme
Use the widely-recognized `pnpr@<version>` form (the Changesets/Lerna
convention) for pnpr's release tag instead of the ad hoc `pnpr-v<version>`
from the previous commit. release.yml triggers and validates
`pnpr@*.*.*` alongside `v*.*.*`; pnpm (v11.x) and pacquet (v12.x) keep
`v<version>`, which get.pnpm.io's install scripts depend on.