Now that --no-runtime fixed the VM-modules loader, tests actually run.
The next failure mode was real: other workspace packages (e.g.
@pnpm/building.commands) shell out to pnpm in their tests, and
PNPM_E2E_BIN routes those calls through pacquet, which doesn't
implement every command they exercise. Add a dedicated
test-pkgs-pnpm script so the e2e job runs jest only in the pnpm
package and PNPM_E2E_BIN affects nothing else.
Written by an agent (Claude Code, claude-opus-4-7).
test.yml's working install path uses --no-runtime so pnpm doesn't try
to materialize an executionEnv runtime that conflicts with the one
pnpm/setup pinned. The competing runtime install is the last
discoverable difference between this job and the standard e2e job.
Written by an agent (Claude Code, claude-opus-4-7).
pnpm-workspace.yaml sets verifyDepsBeforeRun: install, so every script
gets a re-install side effect before it runs. test.yml's standard e2e
job sets pnpm_config_verify_deps_before_run: false to disable this;
without that override the auto-reinstall seems to leave the loader in
the state that triggers jest's "module is already linked".
Written by an agent (Claude Code, claude-opus-4-7).
pnpm's hidden-script guard fires when \`pn -r .test\` is the top-level
shell command but not when the same call lives inside another script.
Driving the run through \`pn run test-pkgs-all\` (which then chains into
\`pn -r .test\` internally) is the call shape main CI uses successfully.
Written by an agent (Claude Code, claude-opus-4-7).
Adding --filter=pnpm re-enabled pnpm's hidden-script guard and broke the
invocation with ERR_PNPM_HIDDEN_SCRIPT. Recursive mode bypasses that
guard on its own; --if-present silently skips workspace packages that
don't define .test, so the unfiltered form does the right thing.
Written by an agent (Claude Code, claude-opus-4-7).
test.yml's standard e2e job drives the same jest config successfully via
\`pn -r .test\`, which executes the hidden \`.test\` script under pnpm's
spawn path. Running jest directly (pnpm exec, ./node_modules/.bin/jest,
etc.) hits the \`module is already linked\` VM-modules regression for
reasons that aren't clear yet but seem tied to that spawn path.
Written by an agent (Claude Code, claude-opus-4-7).
The repo root has no tsconfig.json — tsgo needs to start from
pnpm/tsconfig.json so it can walk the project references and build
all the workspace packages the e2e tests import.
Written by an agent (Claude Code, claude-opus-4-7).
Earlier attempts to drive the suite under the runner's own Node (or a
separately-installed Node from actions/setup-node) all tripped jest's
"module is already linked" VM-modules regression. The working pattern
test.yml uses for the standard e2e job is to pin a runtime through
pnpm/setup and run jest via `pn` so it inherits that Node — the suite
already passes there on 22.13.0.
PNPM_E2E_BIN points at pacquet, so `dist/pnpm.mjs` is never read; we
can skip the bundle step (and with it the `pnx node@runtime:24.6.0`
collision) and run only `tsgo --build` to produce the lib/ outputs
the tests import from.
Written by an agent (Claude Code, claude-opus-4-7).
With --experimental-vm-modules and multiple worker processes the same
module ends up linked twice and every suite errors with "module is
already linked". The base config's maxWorkers: 1 keeps file execution
sequential but still spawns worker processes; --runInBand forces the
main process to do everything, which is what the VM-modules path
expects.
Written by an agent (Claude Code, claude-opus-4-7).
pnpm hoists the jest .bin shim to the monorepo root rather than to
pnpm/, so the previous relative path 404'd.
Written by an agent (Claude Code, claude-opus-4-7).
`node $(which jest)` tried to feed Node the shell shim and crashed with
SyntaxError. Calling the shim directly lets it `exec node` against
PATH, which is the pinned 22.13.0 — the same trick that side-steps
pnpm exec's embedded Node 24.x.
Written by an agent (Claude Code, claude-opus-4-7).
`pnpm exec jest` was still hitting the "module is already linked"
regression because pnpm spawns under its own embedded Node 24.x
regardless of what `actions/setup-node` puts on PATH. Resolve the jest
script path through pnpm but invoke it directly with `node`, so the
pinned Node 22.13.0 is the one that actually runs the suite.
Written by an agent (Claude Code, claude-opus-4-7).
The runner image's Node 24.x patches trip jest's ESM VM-modules path
with "module is already linked", failing all suites before any test
runs. Bundling still needs the runner's Node so `pnx node@runtime:24.6.0`
can resolve, so the pin is added after `pn compile-only` and only
affects the test step.
Written by an agent (Claude Code, claude-opus-4-7).
Pinning the action's `runtime:` to Node 22.13.0 broke `pn bundle`'s
`pnx node@runtime:24.6.0 bundle.ts` call with
ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND — the on-demand runtime fetch
collides with the pre-installed one. ci.yml's compile job already
demonstrates the working pattern: leave runtime unset, drive
compilation through `pn compile-only`.
Written by an agent (Claude Code, claude-opus-4-7).
The pnpm CLI e2e tests already exercise the bundled `pnpm.mjs` end-to-end,
so they can serve as a parity probe for the Rust port too. A new
`PNPM_E2E_BIN` env var swaps the spawned binary in `execPnpm`/`execPnpmSync`/
`spawnPnpm`, and a `skipIfPacquet` helper gates the per-test cases that
the port doesn't yet pass. Every failing test in the existing suite was
marked from a local sweep against `target/release/pacquet`; pacquet-CI
gets a new job that builds the binary, builds the pnpm bundle, and runs
jest with the env var set so future regressions surface in CI.
Written by an agent (Claude Code, claude-opus-4-7).
The `pacquet --version` string is a hardcoded clap attribute in
`cli_args.rs`. It didn't get bumped for the 0.2.2 release, so the
published binary still reports 0.2.1. Bump the literal to 0.2.2 and
add a release-workflow step that rewrites the attribute from
`inputs.version` before `cross build`, so future releases stay
correct automatically. A trailing `grep -F` fails the job loudly if
the regex stops matching after a future refactor of the attribute.
When `configDependencies` declares pacquet (under either the unscoped `pacquet` or the scoped `@pnpm/pacquet` alias), pnpm delegates the fetch / import / link / build phases of an install to the pacquet Rust binary. Pnpm keeps owning dependency resolution — pacquet's resolver isn't ready yet — and hands pacquet a freshly-written lockfile to materialize.
Covered install shapes:
- frozen install (`tryFrozenInstall` → pacquet, no resolve needed)
- default isolated `nodeLinker` (`installInContext`: lockfileOnly resolve via JS, then pacquet)
- hoisted `nodeLinker` (same resolve-then-materialize shape)
- workspace partial install (subset of workspace projects mutated)
- agent-server install (`@pnpm/agent.client` resolves, pacquet materializes)
```yaml
# pnpm-workspace.yaml
configDependencies:
"@pnpm/pacquet": "^0.2.0" # or unscoped `pacquet`
```
## How it works
- `installing/commands/src/runPacquet.ts` resolves the platform binary via `createRequire(realpath(.pnpm-config/<name>/package.json))` — same algorithm the upstream wrapper uses, but skipping the JS shim's extra Node startup.
- Pacquet's NDJSON stderr is forwarded through `@pnpm/logger`'s global `streamParser` so `@pnpm/cli.default-reporter` renders its events the same way it renders pnpm's own. Non-JSON stderr lines pass through verbatim.
- A few pnpm-side log emits (`importing_done` placeholder, `pnpm:summary`) are suppressed when pacquet will take over so the reporter doesn't close streams or lock in empty diffs before pacquet's real events arrive. Pacquet's duplicate `pnpm:progress status:resolved` events are filtered on the resolve-then-materialize paths so the reporter doesn't double-count.
- `installing/deps-installer/src/install/index.ts` gates the delegation on a `runPacquet?: () => Promise<void>` callback in `StrictInstallOptions`. The CLI layer in `installing/commands/src/installDeps.ts` constructs the callback, threaded through both the single-project and workspace-recursive paths.
- The `pacquet` and `@pnpm/pacquet` npm packages ship the same JS shim from `pacquet/npm/pacquet/scripts/generate-packages.mjs`; per-platform binaries stay under the existing `@pacquet/<plat>-<arch>` scope and aren't duplicated.
Without an explicit shell, the step ran under PowerShell on
windows-latest, where `$TEST_SCRIPT` is not a variable (PowerShell
exposes env vars as `$env:TEST_SCRIPT`). `pn run ""` then exited 0
and just listed available scripts — the Windows test legs have been
silently no-op'ing since the env-var move in #11608.
The sibling `Verify Node version` and `Determine test scope` steps
already pin `shell: bash`; this brings `Run tests` in line.
---
Written by an agent (Claude Code, claude-opus-4-7).
Two related workflow changes:
### `pacquet-release-to-npm.yml`: switch to `workflow_dispatch`
The trigger was "push to main touching `pacquet/npm/pacquet/package.json`" — the version came from a committed bump and the workflow auto-fired on every such commit. Switch to `workflow_dispatch` only, with a `version` input (validated as semver). The workflow patches `pacquet/npm/pacquet/package.json` before `generate-packages.mjs` runs, so the version is single-sourced from the manual trigger rather than needing a separate commit to bump the manifest first.
The committed manifest now omits the `version` field entirely — it only exists at release time inside the runner.
Dropped along the way:
- The `check` job (EndBug/version-check against unpkg) — no longer needed when the operator types the version.
- The `Create GitHub Release` step — no draft release, no `v*.*.*` git tag. The pacquet `v0.x.x` tag scheme collided with pnpm's `v11.x.x`; npm is the authoritative artifact store and provenance attestations stay attached via `--provenance` on `pnpm publish`.
- `contents: write` on the publish job (no longer needs to create a tag).
### `release.yml`: add `workflow_dispatch` as a lib-only republish path
Add a `workflow_dispatch:` trigger alongside the existing tag-push trigger. Tag-push behaves exactly as before. Manual dispatch becomes a fast **lib-only republish** path — useful after a version bump to one or more lib packages that doesn't warrant a full CLI release.
On `workflow_dispatch` from any ref, the following are skipped (guarded with `if: startsWith(github.ref, 'refs/tags/')`):
- `Publish @pnpm/exe` step — also contains the multi-minute `build-artifacts` call.
- `Publish pnpm CLI` step.
- `Copy Artifacts`, `Attest build provenance` (the `dist/*` attestation), `Generate release description`, `Release` (`softprops/action-gh-release`) — these are the GitHub-Release-side ceremony. Without an explicit `tag_name`, `softprops/action-gh-release@v2.5.0` defaults to `github.ref_name`, which on a manual dispatch from main would create a junk release tagged literally `main`.
What still runs on `workflow_dispatch`:
- `actions/checkout`, garnet scan, `pnpm/setup`
- `Publish internal workspace packages (static token)` — i.e. `pn publish --filter=!pnpm --filter=!@pnpm/exe --access=public --provenance`
Compilation is handled by each lib package's own `prepublishOnly: tsgo --build` hook (which `pnpm publish` runs automatically), same as the existing tag-push flow.
The npm registry rejects any version already on it, so re-running on an already-released tree is a no-op — that's the safety net for accidental clicks.
## How to use
**pacquet release**: Actions → Release Pacquet → Run workflow → fill in `version` (e.g. `0.2.3` or `0.2.3-rc.1`) → Run. No tag, no GitHub release.
**pnpm full release**: still triggered by a `v*.*.*` tag push. Publishes @pnpm/exe + libs + CLI, attests, copies artifacts, creates a draft GitHub release.
**pnpm lib-only republish**: Actions → Release → Run workflow → choose `main` → Run. Publishes just the internal workspace packages from whatever versions are currently in each `package.json`. Skips CLI, @pnpm/exe, build-artifacts, GitHub release.
* chore(pacquet): fold registry-mock into root workspace, fix npm metadata
Two unrelated cleanups bundled because they touch the same publishing/
workspace plumbing:
1. **`pacquet/npm/pacquet/package.json` metadata** — the imported file
still pointed at the standalone repo: `repository.url` was
`pnpm/pacquet`, `repository.directory` was `npm/pacquet`, `homepage`
and `bugs` likewise. Repoint at `pnpm/pnpm`. Update
`generate-packages.mjs` so the per-platform packages it emits at
release time also point at `pnpm/pnpm`.
2. **Fold `pacquet/tasks/registry-mock` into the root pnpm workspace**.
Pacquet's standalone-repo nested-workspace setup pinned
`nodeLinker: hoisted` "for verdaccio CJS resolution," but pnpm's
own jest globalSetup (`__utils__/jest-config/with-registry/`) calls
the same `@pnpm/registry-mock.start()` API under the default
isolated linker without issue, and verified locally that
`node launch.mjs prepare` works after consolidation. The hoisted
constraint was scoped to standalone-pacquet's install pattern; in
the monorepo it's unnecessary.
Changes for (2):
- Add `pacquet/tasks/registry-mock` to `pnpm-workspace.yaml`.
- Rename the package `@pnpm-private/pacquet-registry-mock-launcher`
(private, matches the `@pnpm-private/*` convention used by other
internal workspace members) and switch `@pnpm/registry-mock` to
`catalog:` (the root catalog already pins it at 6.0.0).
- Delete `pacquet/tasks/registry-mock/pnpm-lock.yaml` and
`pnpm-workspace.yaml` — root install handles both now.
- Delete `pacquet/package.json` and `pacquet/pnpm-lock.yaml` — the
file only had a `cargo build` script + `devEngines: pnpm`, both
already covered by root, and nothing referenced it.
- `justfile install` is now just `pnpm install` (was
`cd pacquet/tasks/registry-mock && pnpm install --frozen-lockfile`).
- `pacquet-integrated-benchmark.yml` path filter and cache key
swap the deleted nested lockfile for the root `pnpm-lock.yaml` /
`pnpm-workspace.yaml`.
Verified: `pnpm install` resolves the workspace member, the lockfile
gains a `pacquet/tasks/registry-mock` importer entry, and
`pacquet/tasks/registry-mock/node_modules/@pnpm/registry-mock` is
linked correctly under the isolated layout.
* fix(pacquet): match meta-updater conventions in registry-mock launcher
The previous version of `pacquet/tasks/registry-mock/package.json`
omitted `version`, which crashed `pn lint:meta` (meta-updater hits
`manifest.version!.split('.')[0]` for every workspace package).
Backfill all the fields meta-updater would emit for an internal
`@pnpm-private/*` private package, matching `__typecheck__/package.json`
and friends:
- `version: 1100.0.0` (the pnpm 11.x convention for non-experimental
internal packages)
- self-devDep entry (`workspace:*`) that meta-updater would otherwise
inject
- `keywords: [pnpm, pnpm11]`
- `repository` pointing at this directory inside pnpm/pnpm
This is the same shape every other `@pnpm-private/*` private workspace
member uses; it lets `pn lint:meta --test` pass without modifying the
file.
* fix: update lockfile
* chore(pacquet): add build:pacquet script at root
Restore the `cargo build --release --bin pacquet` shortcut that lived in
the deleted `pacquet/package.json`. Naming it `build:pacquet` (rather
than `build`) matches the existing namespacing convention in this
file (`lint:ts`, `lint:meta`) and leaves room for a general `build`
script later. Invoke with `pnpm build:pacquet` or `pn build:pacquet`
from the repo root.
* ci(pacquet): fix all zizmor code-scanning findings
Resolves the 90 alerts opened by zizmor against the imported pacquet-*
workflows and shared composite actions:
- unpinned-uses: pin every third-party action to a SHA + version comment
(matching SHAs already used elsewhere in the repo where applicable;
taiki-e/install-action collapsed onto v2.78.0 with explicit `tool:` input).
- artipacked: add `persist-credentials: false` to every actions/checkout.
- template-injection: pass `inputs.*` and `steps.*.outputs.*` through `env:`
in binstall/rustup composite actions and pacquet-release-to-npm.yml.
- excessive-permissions: add top-level `permissions: contents: read` to
pacquet-release-to-npm.yml; move issues/pull-requests writes from the
workflow level to the benchmark-compare job in pacquet-micro-benchmark.yml.
- dangerous-triggers: keep workflow_run in pacquet-integrated-benchmark-
comment.yml but suppress with a documented zizmor: ignore — the trigger
is the recommended pattern for posting comments back to fork PRs.
- superfluous-actions: keep softprops/action-gh-release with a zizmor:
ignore (matches release.yml).
Verified by running `zizmor .github` locally with no remaining findings.
* ci(pacquet): point SHA pins at the patch-version tag
Swatinem/rust-cache and montudor/action-zip were pinned to the SHA the
major-version alias (`v2`, `v1`) resolves to, but the version comments
claimed `v2.9.1` / `v1.0.0`. zizmor's online `ref-version-mismatch`
audit flagged the inconsistency. Repoint at the SHAs the patch-version
tags actually annotate so the pin and the comment agree.
* chore(pacquet): wire pacquet workflows into monorepo
Move Cargo workspace, Rust toolchain configs, justfile, composite actions,
and 7 workflow files out of `pacquet/` and up to the repo root so:
- cargo / just / taplo run from repo root, the way the rest of the
monorepo's tooling does
- GitHub Actions actually discovers the workflows (it only reads
`.github/workflows/` at the repo root)
Workflows are prefixed with `pacquet-` and renamed to "Pacquet ..." so
they don't collide with the existing pnpm CI. Path filters are scoped
to `pacquet/**` so they don't trigger on every commit. The cargo entry
from pacquet's standalone `dependabot.yml` is folded into the root one;
pacquet's `CODEOWNERS` and `pull_request_template.md` are dropped because
the root copies supersede them.
Path rewrites:
- `Cargo.toml` workspace members → `pacquet/crates/*`, `pacquet/tasks/*`
- all path-deps in `[workspace.dependencies]` → `pacquet/...`
- `justfile` recipes (`install`, `install-hooks`) point at `pacquet/...`
- `.taplo.toml` include globs → `pacquet/crates/*/*.toml`, `pacquet/tasks/*/*.toml`
- `pacquet/npm/pacquet/scripts/generate-packages.mjs` REPO_ROOT walks one
more level up
- workflow `paths:` filters, `hashFiles(...)`, and shell paths all updated
Verified: `cargo metadata` resolves the workspace, `cargo fmt --check`
clean, `taplo format --check` picks up all 26 Cargo.tomls, `actionlint`
reports no new issues (the `type:`-on-input warnings on the rustup action
predate this move).
* chore(pacquet): drop pnpm version pin from pacquet CI workflows
The monorepo's root `package.json` declares `pnpm@11.1.1` under
`packageManager`, which conflicts with the workflows' explicit
`version: 11.0.0-rc.5` and trips `pnpm/action-setup` ERR_PNPM_BAD_PM_VERSION.
The pin was a pacquet-era workaround for the v9 lockfile while pnpm 11
was still pre-release. Stable 11.x writes v9 too, so let action-setup
read the version from `packageManager` like every other workflow in
this repo does.
* chore(pacquet): use pnpm/setup matching the rest of the monorepo
Replaces `pnpm/action-setup@v6` with the same `pnpm/setup@b1cac3...`
SHA the rest of pnpm/pnpm uses (release.yml, test.yml, ci.yml,
benchmark.yml, audit.yml). Reads pnpm version from `packageManager`
in root package.json, and skips the implicit `pnpm install` since
pacquet does its own scoped install via `just install` (which only
touches `pacquet/tasks/registry-mock/`).
The release workflow now also installs Node via the same action
(`runtime: node@22`) instead of via `pnpm runtime -g set node 22`,
since pnpm/setup handles runtimes in one step.
* chore(pacquet): tighten permissions and Dependabot cooldown
Address zizmor warnings on the pacquet CI changes:
- `dependabot.yml`: the cargo entry I added in the previous commit
inherited from pacquet's standalone repo and is missing the
`cooldown: default-days: 7` the github-actions entry uses. Add it
so cargo and github-actions debounce updates consistently.
- `pacquet-ci.yml`, `pacquet-codecov.yml`, `pacquet-cargo-unused.yml`
lacked a top-level `permissions:` block, so GITHUB_TOKEN inherited
the repo default. Declare `contents: read` — every job in these
workflows only reads the repo and runs local checks.
The other four pacquet workflows already declare permissions
explicitly (integrated-benchmark/comment, micro-benchmark, release).
* chore(pacquet): add "reimagining" to cspell dictionary
cspell at the repo root scans all `**/README.md` and was rejecting
`pacquet/README.md` and `pacquet/npm/pacquet/README.md`, which describe
pacquet as "not a reimagining of pnpm." Add the word to the existing
allow-list rather than rewording two READMEs imported from a separate
repo.
* fix(pacquet): prefix workspace-relative paths with pacquet/
Two Rust source files looked up paths off the cargo workspace root
(\`cargo locate-project --workspace\`), which now resolves to the
monorepo root rather than the pacquet directory. Add the \`pacquet/\`
prefix:
- \`tasks/registry-mock/src/dirs.rs\` — \`registry_mock()\` was
pointing the node launcher at \`<repo>/tasks/registry-mock/launch.mjs\`
instead of \`<repo>/pacquet/tasks/registry-mock/launch.mjs\`, which
failed every Pacquet CI test job ("Cannot find module ...launch.mjs").
- \`tasks/micro-benchmark/src/main.rs\` — same idea for the
fixtures folder.
Adds the Garnet network-monitoring action to the smoke test job, the
release workflow, and the npm tag workflow. The full CI test matrix is
left untouched to keep per-job overhead off the broad cross-platform
runs; the smoke test still exercises a representative install/test flow.
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).
- Adds `.github/workflows/zizmor.yml` running [zizmor](https://github.com/zizmorcore/zizmor-action) against the repo's GitHub Actions workflows.
- Triggers: push to `main` and `release/**`, and PRs targeting those branches.
- Empty workflow-level `permissions: {}` with job-level grants (`security-events: write`, `contents: read`, `actions: read`) so findings upload to the Code scanning alerts feed.
- Pins `actions/checkout` and `zizmorcore/zizmor-action` by commit SHA, matching the pinning convention used elsewhere in `.github/workflows/`.
## 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.
The previous "Publish Packages" step ran `pn release` after writing
NPM_TOKEN into pnpm's config. With a static `_authToken` configured,
`pnpm publish` bails out of OIDC entirely (see #11495 for the longer-
term fix), so every package — including `pnpm` and `@pnpm/exe` — was
silently being published with the legacy token instead of using npm's
trusted publishing. The result: published metadata showed
`_npmUser: pnpmuser` and no provenance attestation.
Until #11495 ships, work around the precedence bug by structuring the
job so the packages we *want* trusted publishing for never see a
static token at all:
1. `@pnpm/exe` — published in a step with no NPM_TOKEN. pnpm has no
token to short-circuit on, performs OIDC, gets a `trustedPublisher`
entry on npm.
2. Internal workspace packages — these don't have trusted publishing
configured on npm, so they still need the static token. The token
is written, the publish runs, then `pn config delete` removes the
token before the next step.
3. `pnpm` — published in a step with no NPM_TOKEN, same rationale as
step 1.
CI-only change; no changeset needed.
* 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.
The previous comment attributed the darwin SEA crashes to ldid producing
bad page hashes, but the upstream minimal `node --build-sea` + `codesign`
repro (nodejs/node#62893) shows codesign-signed binaries crash too. The
bug is in LIEF's Mach-O surgery during --build-sea, not in signing.
Rewrite the comment to state the actual reasons the job runs on macOS
(native codesign avoids building ldid; macos-latest is Apple Silicon so
verify-binary.mjs can smoke-test the darwin-arm64 SEA) and explicitly
note that this does NOT fix the darwin-x64 crash.
Comment-only change. No behaviour change.
Generate Sigstore-backed SLSA build provenance for the platform tarballs
and zips produced by `pn copy-artifacts` via actions/attest-build-provenance,
so users can verify with `gh attestation verify` that the binaries attached
to a GitHub release came from this repository's release workflow rather
than from a manual upload.
This complements the release attestation that GitHub auto-generates for
Releases (predicate `https://in-toto.io/attestation/release/v0.2`), which
only proves what files were attached to a tag, not how they were built.
The new attestation uses `https://slsa.dev/provenance/v1` and binds each
artifact's digest to the workflow_ref, commit SHA, and runner identity.
The `pn release` step already publishes npm tarballs with provenance, so
this closes the same gap on the GitHub Release side.
* ci(release): build artifacts on macos-latest to fix darwin-x64 signing
Cross-signing darwin Mach-O binaries on Linux with the saurik fork of
ldid produces an ad-hoc signature whose page hashes don't match the
post-postject layout for Node.js 25's chained fixups, leaving fixups
unapplied at load and crashing the binary in __cxx_global_var_init
(EXC_BAD_ACCESS at 0x3 — the unprocessed chain-entry tag).
Running the release on macos-latest lets pack-app's adHocSignMacBinary
use native codesign, which understands chained fixups. Drops the entire
ldid build step.
* ci(release): document why release runs on macos-latest
* feat: publish base docker image to GHCR
Adds a Dockerfile (debian:stable-slim + pnpm standalone binary) and a
release-triggered workflow that builds multi-arch images and pushes to
ghcr.io/pnpm/pnpm. Users who need Node.js can install it inside the
container via `pnpm runtime set node <version>`.
Refs #11300
* docs: add docker/README.md
* chore(cspell): add buildx to dictionary
* docs: mention devEngines.runtime as alternative to pnpm runtime set
* fix(docker): pin base image, verify tarball sha256, harden download
- Pin `debian:stable-slim` to a digest for reproducibility.
- Compute pnpm tarball SHA256 in the workflow and verify it inside the
build, detecting tampered artifacts regardless of what `pnpm --version`
reports.
- Download the tarball to disk with `--retry` instead of `curl | tar`
for resilience under multi-arch QEMU builds.
- README: use `--load` so the local test image is available to `docker run`.
* chore(cspell): sort dictionary additions
* fix(docker): address Copilot review feedback
- Include $PNPM_HOME/bin on PATH so pnpm-installed globals (node, etc.)
are discoverable, and make $PNPM_HOME writable for non-root users.
- Document that `pnpm runtime set node` needs `-g` to install globally.
- Pass workflow inputs via env: instead of inlining GitHub expressions
into shell, and validate the version string before use.
* fix(docker): install libatomic1 for pnpm standalone binary
The pnpm linux standalone binary dynamically links against
libatomic.so.1, which is not present in debian:stable-slim by
default. Without it, `pnpm --version` fails during the build with:
pnpm: error while loading shared libraries: libatomic.so.1:
cannot open shared object file: No such file or directory
Caught by local build testing.
* 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>