The dependency update (pnpm/pnpm#14205) broke TS CI / Compile & Lint in
two independent ways.
streamx 2.28.1 declares the 'data' event payload as unknown, so the five
tar-stream data handlers in the releasing commands that annotated the
chunk as Buffer no longer typechecked. They now take the chunk as
unknown and cast at the use site; tar-stream emits Buffers at runtime,
so the emitted JavaScript is unchanged.
The Node.js v26.8.0 binaries on nodejs.org were built with a bogus
version string and self-report as 26.8.0-alpha.0.0.0 (Node.js shipped
26.8.1 the next day to correct it). That prerelease-looking version made
cspell's engine guard reject the runtime during lint and made node-gyp
request a nonexistent v26.8.0-alpha.0.0.0 headers tarball for native
builds. The pinned runtime is bumped to 26.8.1 in the root devEngines,
the compile-only script, the CI test matrix, and the benchmark and
release workflows.
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.
* 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>
* 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.
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.
When resolving with --offline or --prefer-offline, pickPackage loaded a
package's metadata from the on-disk cache mirror but never stored it in
the in-memory metaCache, so every subsequent resolution of the same
package (once per dependent that references it) re-read and re-parsed
the full packument from disk. Store the parsed metadata in the
in-memory cache on those disk-return paths, so each package's metadata
is parsed at most once per command. On a large monorepo this collapses
23,746 metadata loads to 5,124 (66.8 GB of JSON parsed to 8.8 GB) and
takes pnpm dedupe --offline from ~140s to ~75s with a byte-identical
lockfile. The cache is the existing bounded LRU; online and network
paths are untouched.
Promotion alone would have made the top-of-function cache hit terminal:
a disk-promoted packument that predates a later-requested range would
turn a previously-recoverable network fallback into
ERR_PNPM_NO_MATCHING_VERSION. Track registry-unverified disk promotions
(a WeakSet keyed by object identity in TypeScript; a registry_verified
flag carried inside the cache value in pacquet, so meta and state are
read atomically) and let a cache hit whose pick fails on such an entry
fall through to the regular network-validating flow unless offline. The
revalidating fetch stores a verified entry, so each package revalidates
at most once. The pre-existing exact-version fast paths on both stacks
had the same latent failure mode and are covered by the same marking.
Pacquet's PackageMetaCache::set_unverified is a required trait method
so custom cache implementations cannot silently inherit the terminal
behavior, and the TypeScript PackageMetaCache documents the
object-identity contract its WeakSet relies on.
Also registry-qualify pacquet's verifier shared-meta-cache reads
(read_shared_meta queried bare name keys while the resolver stores
registry-qualified ones, so the fast path could never hit), mirroring
the TypeScript readSharedMeta lookup order.
Add an isolated-linker.fresh-resolve.hot-cache.offline benchmark
scenario (install --offline --lockfile-only against a warm mirror, with
an untimed online pre-warm pass and per-iteration node_modules +
lockfile wipes so the up-to-date short-circuit cannot skip the measured
resolution) to guard offline resolution on all Bencher testbeds. Fixing
it to measure real work also revived the pnpm benchmark pipeline,
silently dead since the pnpm11/ move: bench.sh resolved REPO_ROOT to
pnpm11/ where no Cargo.toml exists, the orchestrator probed the
pre-move pnpm/dist bundle path, and continue-on-error masked every
failure as a green run. The scenario measures this change directly:
pnpm@HEAD resolves the fixture offline in 2.41s vs 3.17s for pnpm@main
(1.31x), with metadata-mirror reads dropping from 2625 to 1297.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
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.
* 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
- Add `--start-point-clone-thresholds` to the non-main upload arms
so PR/feature branches inherit thresholds configured on main; pair
it with `--err` so a sample over the upper boundary fails the job.
- Add `checks: write` to the three workflows that call `bencher run`.
On main pushes (no `--ci-number`, not a PR event) Bencher falls back
to creating a GitHub Check on the commit; without the permission it
exits 1 with "Failed to create GitHub Check".
* ci(bencher): record benchmark results to Bencher
Tracks both stacks in one Bencher project (`pnpm`), under separate
testbeds (`pnpm`, `pacquet`).
- `benchmarks/bench.sh` emits a hyperfine-shaped `bencher-results.json`
combining the six pnpm scenarios.
- `benchmark.yml` adds `push: branches: [main]` so each merge updates
the `pnpm` baseline, then uploads via the Bencher CLI.
- `pacquet-integrated-benchmark.yml` adds the same main-push baseline
for `pacquet`, combines the four scenario JSONs into a Bencher
report, and stages it into the existing artifact.
- `pacquet-integrated-benchmark-comment.yml` uploads PR results from
the trusted `workflow_run` context so fork PRs are covered too.
Requires `BENCHER_API_TOKEN` in repo secrets; workflows no-op with a
`::notice::` if it's missing.
* ci(bencher): allow workflow_dispatch to upload pacquet results
The inline Bencher upload was gated to `event_name == 'push'`, which
meant manual dispatch from a feature branch ran the bench but skipped
the upload. Both push and workflow_dispatch execute in the base-repo
privilege context, so it's safe to upload from both — the fork-safety
gate only needs to keep `pull_request` runs out.
On dispatch from a non-main branch we record into the ref name with
`--start-point main --start-point-reset`, matching the pnpm bench's
branch policy.
* ci(bencher): treat workflow_dispatch on main as the main baseline
Two small fixes from PR review:
- `benchmark.yml`: manual dispatch from `main` was falling into the
non-main branch arm, recording `--branch main --start-point main
--start-point-reset` — equivalent to forking main from itself.
Treat it like a `push` event so a manual run on main updates the
baseline directly. Matches the pacquet workflow's branch policy.
- `bench.sh`: emit a stderr warning when `jq` is missing instead of
silently skipping `bencher-results.json`. Keeps behaviour optional
for local users but makes the skip discoverable.
* bench: rename scenarios with explicit state axes
Replaces the hyperfine-leaking names (`clean-install`, `frozen-lockfile`,
`peek`, `gvs-warm`, …) with a consistent grid that spells out every
state the benchmark depends on:
- "Fresh" — node_modules wiped at start (future variants will start
with a populated node_modules).
- "Install" vs "Restore" vs "Add new dep" — the work being measured.
- "hot/cold cache + hot/cold store" — both pnpm directories,
spelled out separately because they're distinct on disk.
- "isolated linker" — nodeLinker mode (future variants will cover
`hoisted` and `pnp`).
The slugs map directly from the clap-derived kebab-case names, so
`--scenario=fresh-restore-cold-cache-cold-store-isolated` is the new
CLI surface. Updates land across the Rust orchestrator
(`BenchmarkScenario`), `benchmarks/bench.sh`, the pacquet workflow,
`benchmarks/README.md`, and `pacquet/CONTRIBUTING.md` so the names
agree end-to-end.
Adds a justified `#[allow(clippy::enum_variant_names)]` on the enum
because every variant currently shares the `Fresh` prefix; the lint
will stop firing once `Filled*`/`Resynced*` counterparts land.
Bencher's stored history for the old benchmark names will become
orphaned and can be archived in the UI.
* bench: linker-first slug shape with dot-separated axes
Reshapes the scenario identifiers so the linker mode is the leading
group: `<linker>.<action>.<cache state>.<store state>`. Dots separate
the four axes the bench varies, and `isolated-linker.*` /
`gvs-linker.*` sort together in any dashboard that groups by prefix.
Future buckets (`hoisted-linker.*`, `pnp-linker.*`) will slot in
without disturbing the existing names.
GVS is its own top-level bucket rather than a sub-variant of
isolated — its perf profile differs enough to chart separately.
Renames:
- `clean-install` → `isolated-linker.fresh-install.cold-cache.cold-store`
- `full-resolution` → `isolated-linker.fresh-install.hot-cache.hot-store`
- `frozen-lockfile` → `isolated-linker.fresh-restore.cold-cache.cold-store`
- `frozen-lockfile-hot-cache` → `isolated-linker.fresh-restore.hot-cache.hot-store`
- `peek` → `isolated-linker.fresh-add-dep.hot-cache.hot-store`
- `gvs-warm` → `gvs-linker.fresh-restore.hot-cache.hot-store`
Each Rust variant now carries `#[value(name = "…")]` so clap accepts
the dotted CLI form (`--scenario=isolated-linker.fresh-install.cold-cache.cold-store`).
Display labels follow the slug structure: `Isolated linker: fresh
install, cold cache + cold store` and `GVS linker: fresh restore,
hot cache + hot store`.
The `#[allow(clippy::enum_variant_names)]` is renewed; 5 of 6 variants
share the `Isolated` prefix today. Once `Hoisted*` / `Pnp*` buckets
land the lint will stop firing on its own.
* style: apply rustfmt after scenario rename
The longer match-arm pattern produced by the linker-first rename
exceeded the rustfmt width budget. Auto-format breaks the
`&["install", "--frozen-lockfile"]` body onto its own line so the
arm stays within the limit.
Unify the two install-benchmark stacks (`benchmarks/bench.sh` and `pacquet/tasks/integrated-benchmark/`) into one shared Rust orchestrator. Scenario, fixture, workspace-manifest, install-script, and report generation live in one place so changes propagate to both stacks.
Scenario sets per workflow are unchanged: `benchmark.yml` keeps measuring the same 6 scenarios; `pacquet-integrated-benchmark.yml` keeps measuring the same 2. Both verdaccio and live-npm registry modes are preserved; neither is removed in favor of the other. All six scenarios accept both `pacquet@<rev>` and `pnpm@<rev>` targets.
Resolves all 30 zizmor alerts reported on main after #11607:
- template-injection (19): move `${{ ... }}` interpolations in `run:` blocks
to `env:` so untrusted-ish values (workflow_dispatch inputs, github.ref_name,
github.actor) can't break out of shell quoting.
- artipacked (8): add `persist-credentials: false` to `actions/checkout` in
audit, benchmark, ci, codeql-analysis, docker, release, test workflows.
`update-lockfile.yml` keeps the persisted token (later step pushes to a
branch) with a `zizmor: ignore[artipacked]` comment and justification.
- dependabot-cooldown (1): add a 7-day cooldown so brand-new (potentially
malicious) Actions releases don't get auto-PR'd day-of-release.
- ref-version-mismatch (1): `bluwy/release-for-reddit-action` SHA pointed at
the `v2` tag, not a non-existent `v2.0.0`. Fix the comment.
- superfluous-actions (1): mark `softprops/action-gh-release` with a
`zizmor: ignore` and justification — the release pipeline is sensitive and
the action is battle-tested; we're not swapping it for `gh release` here.
Verified locally with `zizmor --persona regular .github` (online audits on):
No findings to report. Good job! (2 ignored, 32 suppressed)
---
Written by an agent (Claude Code, claude-opus-4-7).
## Summary
Migrates CI workflows from `pnpm/action-setup` + manual `pn runtime set node …` + `pn install` to the new combined `pnpm/setup` action (see https://github.com/pnpm/setup/pull/1).
`pnpm/setup` installs pnpm and the JS runtime in one step. It also runs `pnpm install` automatically when a `package.json` is present, so per-workflow install steps are dropped. When the `runtime` input is set, the action passes `--no-runtime` to `pnpm install` so the matrix-selected Node version isn't shadowed by a different `devEngines.runtime` pin.
## What changed
| Workflow | Migration |
|---|---|
| `test.yml` | `pnpm/setup` with `runtime: node@${{ inputs.node }}`. Verify-Node step asserts the matrix version stayed active. Verify-npm step retained as canary (npm comes from the runner image, not the pnpm-installed runtime). |
| `ci.yml` | `pnpm/setup` (no `runtime` input — `devEngines.runtime` in package.json handles the Node pin). |
| `release.yml` | `pnpm/setup` with `runtime: node@26.0.0`. |
| `benchmark.yml` | `pnpm/setup` with `runtime: node@26.0.0`. |
| `audit.yml` | `pnpm/setup` with `install: false` — audit only needs pnpm itself, not `node_modules`. |
| `update-lockfile.yml` | `pnpm/setup` with `install: false` — the job deletes `pnpm-lock.yaml` and regenerates it via `--lockfile-only`, so the action's auto-install would be wasted. |
| `update-latest.yml` | Untouched — it only uses npm, no pnpm setup needed. |
## Caveats / things to watch
- **npm availability.** `pnpm runtime set node` does not extract npm. The runner image's pre-installed Node toolchain provides `npm` on PATH; if a future runner image change removes that, dlx-style git-hosted dependency tests in `test.yml` will fail. The `Verify npm` step in `test.yml` is the canary.
## Related upstream change
- [pnpm/setup#3](https://github.com/pnpm/setup/pull/3) — added the `install` input so callers like `audit.yml` and `update-lockfile.yml` can opt out of the action's auto-install.
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: update Node.js to 26.0.0
* fix(jest-config): use amaro for type stripping on Node.js 26
Node.js v26 removed the `transform` mode and `sourceMap` option from
`module.stripTypeScriptTypes`. Switch the Jest transform to call
`amaro.transformSync` directly (the same wasm transformer Node.js wraps)
so we keep inline source maps for tests.
* fix: ensure PNPM_HOME/bin is in PATH during pnpm setup
When upgrading from old pnpm (global bin = PNPM_HOME) to new pnpm
(global bin = PNPM_HOME/bin), `pnpm setup` would fail because the
spawned `pnpm add -g` checks that the global bin dir is in PATH.
Prepend PNPM_HOME/bin to PATH in the spawned process env so the
check passes during the transition.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: update pnpm to v11 beta 2
* chore: update pnpm to v11 beta 2
* chore: update pnpm to v11 beta 2
* chore: update pnpm to v11 beta 2
* fix: lint
* refactor: rename _-prefixed scripts to .-prefixed scripts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update root package.json to use .test instead of _test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ci: update action-setup
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Third-party cmd shims (e.g., npm's rimraf.cmd) call node.exe from
within IF/ELSE blocks in batch files. When node resolves to node.cmd
instead of node.exe, Windows batch file chaining breaks with
"The system cannot find the path specified."
On Windows, hardlink node.exe directly into the bin directory.
On non-Windows, symlink the node binary directly.