* fix(lockfile): keep non-reconstructable tarball URLs when lockfileIncludeTarballUrl is false
`lockfile-include-tarball-url` defaults to `false`, so for the vast
majority of users the early return added by #10621 silently dropped
tarball URLs that cannot be reconstructed from registry+name+version —
breaking `pnpm install --frozen-lockfile` from an empty store on
GitHub Packages (`https://npm.pkg.github.com/download/<scope>/<name>/<version>/<hash>`),
JSR, and similar registries.
`false` now matches the historical (v10) heuristic: tarball URLs are
written when they are non-reconstructable, otherwise omitted.
`true` continues to force every tarball URL into the lockfile.
Refs #11276, #11407.
* chore: appease cspell
Replace "reconstructable" with "derivable" and avoid the cspell-flagged
"mypkg" placeholder in the new test fixture.
* docs(changeset): use camelCase setting name
* fix(lockfile): guard against missing tarball field in toLockfileResolution
`TarballResolution.tarball` is typed as required, but callers that
deserialize resolutions from external state can violate that. Return
early with just `integrity` if the tarball URL is missing instead of
asserting non-null at the use site (which previously paired a
`as string | undefined` cast with `tarball!.replaceAll(...)` —
contradictory signals that confused both readers and review tools).
* fix: skip Content-Length check when response is content-encoded
Per the HTTP spec, Content-Length refers to the encoded payload when
Content-Encoding is set, but undici fetch yields decoded bytes — so the
strict size check rejected legitimate downloads with
ERR_PNPM_BAD_TARBALL_SIZE. Closes#11506.
* fix(tarball-fetcher): match v10's no-compression behavior
Verified the user's report against v10 source: v10 worked because it
called node-fetch with `compress: false` (network/fetch/src/fetchFromRegistry.ts
on the v10 branch), which suppressed Accept-Encoding and prevented
auto-decompression. v11's switch to undici fetch lost that opt-out — undici
sends Accept-Encoding: gzip, deflate, br by default and transparently
decodes the body, while keeping Content-Length pointed at the encoded
payload (confirmed empirically). The strict size check then rejects the
download.
Restore v10's effective behavior by sending Accept-Encoding: identity for
tarball requests, and as defense in depth against misbehaving servers
that stamp Content-Encoding regardless, skip the strict size check when
the response declares a non-identity Content-Encoding.
* fix(tarball-fetcher): parse Content-Encoding as a coding list
Per RFC 9110 §8.4 the header is a comma-separated, case-insensitive list
that may include whitespace and mixed codings (e.g. `gzip, identity`).
The previous string-equality check misclassified those — the response is
now treated as encoded iff any coding is non-`identity`.
@changesets/read treats every directory inside .changeset/ as a legacy
v1 changeset and tries to read changes.md from it, which made
`changeset version` fail with ENOENT on .changeset/.released/changes.md.
Move the per-branch ledger to .changeset-released/ at the repo root.
* test(exe): add Windows-only repro for #11486 (pn/pnpx/pnx aliases)
Captures the user-reported failure on a fresh Windows CI: when the
@pnpm/exe install rewrites bin entries to point at .cmd files,
@zkochan/cmd-shim's Bash shim does `exec cmd /C ...target.cmd`, MSYS2
mangles the lone `/C` into a Windows path, and cmd.exe falls into
interactive mode (printing its banner instead of running the alias).
These tests will fail on `windows-latest` until the follow-up commit
points the bin entries at .exe hardlinks of the SEA binary.
* fix(exe): route pn/pnpx/pnx through .exe hardlinks on Windows (#11486)
The @pnpm/exe install rewrote bin to point pn/pnpx/pnx at .cmd files,
which cmd-shim wraps as `exec cmd /C ...target.cmd "$@"` in its Bash
shim. MSYS2 / Git Bash mangles the lone `/C` into a Windows path
before cmd.exe sees it, so cmd.exe finds no /C or /K and falls into
interactive mode — the user sees its banner instead of `pnpm dlx`.
Hardlink pn.exe / pnpx.exe / pnx.exe to the SEA pnpm.exe (in setup.js
preinstall and in self-update's linkExePlatformBinary) and rewrite
those bin entries to the .exe names. cmd-shim emits a direct exec for
.exe sources, taking cmd.exe out of the chain entirely. The SEA reads
process.execPath's basename and prepends `dlx` when launched as
pnpx / pnx.
* test(exe): make Windows alias tests robust to local-dev environments
Two follow-ups from Copilot review on #11501:
* Use `'junction'` instead of `'dir'` for the detect-libc symlink on
Windows. Non-junction directory symlinks need Developer Mode or
admin, which the existing failure-path tests already skip on Windows
for; junctions don't.
* Probe \`bash --version\` before running the Git Bash / MSYS2 alias
test, and skip cleanly if it isn't on PATH (local Windows dev
machines often lack it; CI windows-latest ships it). Fold the status
check into the assertion so a non-zero exit surfaces in the diff.
* test(exe): wire @pnpm/exe into the recursive test runner
The setup.test.ts in this package wasn't running in CI — `@pnpm/exe`
had no `.test` script, so `pn -r .test` (what `test-pkgs-all` runs)
silently skipped it. The existing tests there have apparently been
dead since they were added; the Windows alias repro added in 1e93a1d
inherited the same gap.
Add `.test` (jest invocation, matching every other workspace
package's shape) and a `test` alias so it's picked up by the
recursive runner. meta-updater's @pnpm/exe / artifacts branch
short-circuits before adding test scripts; preserve that behavior by
hand-writing them rather than restructuring the rule.
## Summary
`pnpm publish` will now let an OIDC-derived token from npm's trusted publishing flow take precedence over a statically configured `_authToken` (e.g. one written from an `NPM_TOKEN` CI secret), mirroring the [npm CLI's behavior](https://github.com/npm/cli/blob/7d900c46/lib/utils/oidc.js).
### Background
We noticed that `pnpm@11.0.0-alpha.5` was published with trusted publishing on npm (its registry metadata has `_npmUser.trustedPublisher` and a SLSA `attestations.provenance` block) — but `pnpm@11.0.6` was not (`_npmUser: pnpmuser`, no attestations). The two were published by different clients: alpha.5 went out via `npm publish` (its metadata carries `_npmVersion: 11.6.2`, which `pnpm publish` never sets), while 11.0.6 went out via `pn release` from `.github/workflows/release.yml`.
Both paths run in CI with `id-token: write` granted, and both have the same `pn config set //registry.npmjs.org/:_authToken "\${NPM_TOKEN}"` step before publish. The difference is purely in how each client orders the auth precedence:
- **npm CLI**: tries the OIDC exchange first; on success **overwrites** the configured `_authToken`. The static token only acts as a fallback when OIDC isn't applicable (no trusted publisher configured, exchange fails, etc.).
- **pnpm publish (before this PR)**: bailed out of OIDC entirely as soon as a token was already configured (`releasing/commands/src/publish/publishPackedPkg.ts`, the old `fetchTokenAndProvenanceByOidcIfApplicable`). Worse, the call site used `??=` so OIDC could never overwrite the static token even if the bail-out had been removed.
So even though pnpm has a complete, working OIDC implementation, any release workflow that still wired in `NPM_TOKEN` silently downgraded to legacy token-based publishing — no `trustedPublisher` on the metadata, no provenance attestation. That's the fix here.
## Changes
- \`fetchTokenAndProvenanceByOidcIfApplicable\` → \`fetchTokenAndProvenanceByOidc\`. Dropped the now-unused \`targetPublishOptions\` parameter and removed the early bail-out — OIDC is always attempted when running in a supported CI environment with \`id-token: write\`.
- At the call site in \`createPublishOptions\`: when OIDC returns an authToken, it now overwrites \`publishOptions.token\` instead of nullish-assigning. The static token still wins when OIDC isn't applicable (no CI env, no trusted publisher configured, exchange fails).
- \`appendAuthOptionsForRegistry\` propagates the (possibly OIDC-overridden) token to the registry-scoped \`\${configKey}:_authToken\`, so libnpmpublish picks it up correctly.
- A \`ProvenanceError\` from \`determineProvenance\` no longer discards the freshly-fetched OIDC authToken — the publish itself can still go through with the OIDC token, again matching npm CLI behavior.
- Exported \`fetchTokenAndProvenanceByOidc\` (marked \`@internal\`) so the precedence rules are unit-testable.
### Recursive publish
The recursive publish loop in \`recursivePublish.ts\` calls \`publishPackedPkg\` once per workspace package, and OIDC token exchange is package-scoped on the npm side (\`/-/npm/v1/oidc/token/exchange/package/\${name}\`). With this fix, every workspace package independently attempts trusted publishing and only falls back to the static token if its own exchange fails. No structural change needed there.
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.
Fixes [#11492](https://github.com/pnpm/pnpm/issues/11492).
In pnpm v11 a scoped registry resolved to different URLs depending on which command read it:
- `pnpm config get @<scope>:registry` returned the value from `.npmrc`
- `pnpm publish` used the value from `pnpm-workspace.yaml`'s `registries` block
When the two sources disagreed, `pnpm publish` silently targeted the URL from `pnpm-workspace.yaml`, even though `pnpm config get` reported a different (and seemingly authoritative) URL — so users could publish to the wrong registry without any indication.
This PR makes `pnpm config get @<scope>:registry` read from the merged `Config.registries` map (the same map `publish` and the resolvers use) before falling back to `authConfig`. Both commands now report and use the same URL.
Closes#11488.
`pnpm fetch` writes forced-empty `hoistPattern: []` and `publicHoistPattern: []` into `.modules.yaml` (because its `virtualStoreOnly` install path skips hoisting). In v10 the follow-up `pnpm install` ignored these unless the user had explicitly set a hoist-pattern in their config. v11's [#11199](https://github.com/pnpm/pnpm/pull/11199) removed that explicit-config gate, so `validateModules` now always sees the empty patterns as a hoist-pattern change and purges `node_modules` — slow on every CI run, and per the bug report sometimes leaves the modules dir in an `ERR_MODULE_NOT_FOUND` state on subsequent runs.
The fix marks `.modules.yaml` with a new `virtualStoreOnly: true` field after a fetch. `validateModules` recognizes this flag as "incomplete install state" and skips the `PUBLIC_HOIST_PATTERN_DIFF` / `HOIST_PATTERN_DIFF` comparisons. The next install then completes the missing post-import linking in place rather than purging. The flag is dropped from `.modules.yaml` once a normal install runs.
A genuine hoist-pattern change (without a fetch in between) still triggers the purge as before — verified manually with `publicHoistPattern` in `pnpm-workspace.yaml`.
The CLI argument parser short-circuits `--help` and `--version` and was discarding every other parsed option in the process — including universal flags like `--pm-on-fail`. So `pnpm audit --pm-on-fail=ignore --help` and `pnpm --pm-on-fail=ignore --version` failed with the strict `packageManager` mismatch error instead of doing what was asked. Users had no documented way out: the suggested escape hatch in the error message itself didn't work.
The fix plucks universal options back out of the exploratory `nopt` parse and surfaces them through both short-circuits. They were already typed correctly there; only the regular per-command parse adds command-specific options. Command-specific options (e.g. `--frozen-lockfile`) stay dropped, since the matching command isn't being executed.
Closes [#11487](https://github.com/pnpm/pnpm/issues/11487).
For git-hosted tarballs (`codeload.github.com` / `gitlab.com` / `bitbucket.org`) the fetcher dropped the integrity it computed while downloading, so the lockfile only ever stored the URL. A compromised git host or man-in-the-middle could serve a substituted tarball on subsequent installs and pnpm would install it — the lockfile had no hash to compare against.
This pins the SHA-512 SRI of the raw tarball in the lockfile, in the same `sha512-<base64>` form npm-registry tarballs use. The only difference is the source: for npm we pass through `dist.integrity`, for git we compute it locally from the downloaded buffer. Subsequent installs validate the download against that integrity in the worker (`addTarballToStore` → `parseIntegrity` → hash compare), so a tampered tarball fails with `TarballIntegrityError`.
## Why git-hosted stays on `gitHostedStoreIndexKey`
The lockfile pins integrity for security, but the *store key* for git-hosted resolutions stays on `gitHostedStoreIndexKey(pkgId, { built })` rather than collapsing under the integrity-based key. Reason: git-hosted tarballs are post-processed (`preparePackage` / `packlist`), so the cached file set depends on whether build scripts ran during fetch. The integrity-only key would fold the built and not-built variants into a single slot, letting one overwrite the other and serving the wrong content if `ignoreScripts` was toggled between runs. Keeping git-hosted on the existing key shape preserves that dimension; the integrity is still validated on every fresh download.
## How the routing stays clean
The naive way to express "use gitHostedStoreIndexKey for git-hosted, integrity key for npm" is to call `isGitHostedPkgUrl(resolution.tarball)` everywhere a store key is computed — fragile, scattered, and easy to forget when adding new readers (Copilot caught two of those during review). Instead, a typed annotation: `TarballResolution` gets an optional `gitHosted: boolean` field. The git resolver sets it; the lockfile loader (`convertToLockfileObject`) backfills it for entries written by older pnpm versions; `toLockfileResolution` carries it through on serialize. Every consumer reads `resolution.gitHosted` directly. URL detection lives in exactly two places — the resolver and the loader — instead of seven.
## Changes
### Security fix
- `fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts` — return the `integrity` that the inner remote-tarball fetch already computed (was being silently dropped by the destructure).
### Lockfile schema (additive)
- `@pnpm/lockfile.types` and `@pnpm/resolving.resolver-base` — `TarballResolution` gains optional `gitHosted: boolean`.
- `@pnpm/resolving.git-resolver` — sets `gitHosted: true` on every git-hosted tarball it produces.
- `@pnpm/lockfile.fs` (`convertToLockfileObject`) — backfills the field on load for older lockfiles via inlined URL detection.
- `@pnpm/lockfile.utils` (`toLockfileResolution`, `pkgSnapshotToResolution`) — preserve / read the field.
### Store-key consumers (now one-line typed reads, dropped the URL-sniffing dep)
- `installing/package-requester` (`getFilesIndexFilePath`)
- `store/pkg-finder` (`readPackageFileMap`)
- `modules-mounter/daemon` (`createFuseHandlers`)
- `building/after-install` (side-effects-cache lookup + write)
- `store/commands/storeStatus`
- `installing/deps-installer` (agent-mode store-controller wrapper)
### Fetcher routing
- `fetching/pick-fetcher` — `pickFetcher` prefers `resolution.gitHosted`; URL fallback retained for ad-hoc resolutions.
### Tests
- New integrity-validation test in `tarball-fetcher` (mismatched `integrity` on the resolution must throw `TarballIntegrityError`).
- New git-hosted lookup test in `pkg-finder` asserting routing through `gitHostedStoreIndexKey` even when integrity is present.
- New `toLockfileResolution` test asserting `gitHosted: true` flows through serialization.
- `fromRepo.ts` lockfile snapshot updated for the now-pinned integrity + `gitHosted: true`.
- `git-resolver` tests updated to assert `gitHosted: true` in produced resolutions.
- Closes#11483.
- `@pnpm/exe@11.x` was packing `dist/node-gyp-bin/node-gyp`, `dist/node-gyp-bin/node-gyp.cmd`, and `dist/node_modules/node-gyp/bin/node-gyp.js` with mode `0644` instead of `0755`. Any install whose lifecycle script invokes `node-gyp rebuild` while pnpm has prepended `dist/node-gyp-bin/` to `PATH` failed with `sh: 1: node-gyp: Permission denied` — most visibly via `pnpm/action-setup@v6`'s standalone path on GitHub Actions runners with system Node < 22.13.
- The regular `pnpm` package's `package.json` already declares `publishConfig.executableFiles` for these three shims, which `pnpm publish` honors by packing matching tar entries with mode `0755` (see `releasing/commands/src/publish/pack.ts:273` and the `bins`-driven mode selection at `pack.ts:360-361`). `@pnpm/exe`'s `package.json` was missing the same field, so the shims it copies from `pnpm/dist/` shipped with `0644`. This PR mirrors the field into `@pnpm/exe`.
- To prevent the two manifests from drifting again, meta-updater is now the single source of truth for the executable-files list and writes it into both `pnpm/package.json` and `pnpm/artifacts/exe/package.json`.
* fix: include workspace root only when --filter is positive
PR #10465 stopped the implicit workspace-root exclusion for `pnpm -r
run/exec/test/add` whenever any --filter was provided. That gate was
too broad: with a negative-only filter set (e.g. `--filter '!a'`) the
workspace root started showing up in the matched projects, contradicting
the documented default behavior.
Only suppress the implicit root exclusion when the user provided at
least one positive (non-`!`) filter that could have been intended to
select the root. Negative-only filter sets keep the documented default,
and `--include-workspace-root` continues to opt the root in explicitly.
Close#11341.
* test: fix root-exclusion assertion to match --stream prefix
The --stream reporter prefixes output lines with the project's relative
directory, so the workspace root appears as `. which$`, not
`root which$`. The original assertion would have passed even if the
regression were still present. Verified the corrected assertion fails
against the buggy code and passes against the fix.
Moves 20 user-level preference settings from the workspace-only exclusion list into the global config allowlist (`config/reader/src/configFileKey.ts`):
- Shell / scripts: `scriptShell`, `shellEmulator`
- Notifications & UI: `updateNotifier`, `useStderr`
- Trust policy (already DLX-inherited as user-level posture): `trustPolicy`, `trustPolicyExclude`, `trustPolicyIgnoreAfter`
- Store / virtual store: `globalVirtualStoreDir`, `virtualStoreDir`, `virtualStoreDirMaxLength`, `verifyStoreIntegrity`, `sideEffectsCache`, `sideEffectsCacheReadonly`
- Build / dep verification: `strictDepBuilds`, `verifyDepsBeforeRun`
- Misc personal/system prefs: `stateDir`, `registrySupportsTimeField`, `initPackageManager`, `initType`, `agent`
These are personal/system preferences rather than workspace structure. In v10 they could be set in `~/.npmrc`. v11 silently dropped them from both `~/.npmrc` and the new global `config.yaml`, leaving `pnpm-workspace.yaml` as the only working location — which the issue author rightly points out is impractical for system-level defaults like `scriptShell`.
After this change:
- Settings in `~/.config/pnpm/config.yaml` are applied instead of being filtered out by `isConfigFileKey` (`config/reader/src/index.ts:296`).
- `pnpm config set --location global scriptShell <path>` succeeds instead of throwing `ConfigSetUnsupportedYamlConfigKeyError` (same predicate used in `config/commands/src/configSet.ts:237`).
`pmOnFail` and `runtimeOnFail` are intentionally left workspace-only because they would cause lockfile divergence between contributors when set globally. `~/.npmrc` support for non-auth/non-network keys is also intentionally not restored — the team has moved those settings to YAML config.
Closes#11474.
* chore(release): wrap changeset version with cross-branch consumed-id ledger
When a fix is cherry-picked from main to a release branch (or vice
versa), the changeset file ends up on both branches. The release
branch's release consumes and deletes its copy, but the cherry-picked
copy on main survives the merge back and would be re-applied on the
next main release.
Introduce a small wrapper around `changeset version` that maintains a
per-branch ledger at .changeset/.released/<branch>.txt. Each entry is a
consumed changeset id; the file is written only by the branch it is
named after, so the records merge across branches without conflicts.
Before running `changeset version` the wrapper reads the union of every
ledger file, hides matching .changeset/<id>.md files (rename to
.md.released), then runs `changeset version` against the remaining set.
Newly consumed ids are appended to the current branch's ledger; hidden
files are removed afterward (their consumption is already on record
elsewhere). On failure the hidden files are restored to keep the
working tree clean.
* docs: move release-ledger explanation out of AGENTS.md
AGENTS.md is for instructions to AI agents working on the codebase, but
the cross-branch ledger is release machinery that the maintainer running
`pnpm bump` interacts with — agents authoring changesets do not need to
know about it. Move the explanation to where someone runs into it:
- .changeset/.released/README.md — discovered by anyone exploring the
directory.
- A short doc-comment header at the top of __utils__/scripts/src/bump.ts
pointing readers there.
* fix(scripts): harden bump wrapper edge cases from PR review
- Use url.pathToFileURL(realpathSync(...)) to compare against
import.meta.url so the direct-invocation guard works on Windows
paths and through symlinks (Copilot review).
- hideReleased() now iterates the changeset directory and filters by
the released set instead of iterating the (potentially long) ledger
and probing existsSync per entry (Copilot review).
- hideReleased() restores already-renamed files if a later rename
throws, so a partial failure leaves the .changeset directory in its
original state (CodeRabbit review).
- Move deleteHidden() into a finally so the .md.released files are
cleaned up even if appendReleased() throws after a successful
changeset version run (CodeRabbit review).
- Add a unit test that forces hideReleased() to fail mid-loop and
asserts the rollback.
pnpm 10 transitively emitted an npm-CLI-shaped per-package JSON object on stdout for
`pnpm publish --json` because the publish command shelled out to npm. pnpm 11 (#10591)
reimplemented publish natively and the new handler returned undefined, so `--json` now
produces 0 bytes on stdout on success — silently breaking downstream tooling that grepped
that contract. The most impactful regression is `nx release publish`, which parses
stdout JSON to confirm success; under `--nx-bail=true` it aborts the rest of a release
run when JSON is missing (see nrwl/nx#35575).
This restores the contract for both single-package and recursive publish, and aligns
`--report-summary` to use the same per-package shape:
- `pnpm publish --json` → single object `{ id, name, version, size,
unpackedSize, shasum, integrity, filename, files,
entryCount, bundled }`, mirroring `npm publish --json`.
- `pnpm publish -r --json` → array of those objects, mirroring `pnpm pack --json`'s
shape choice for single vs multi.
- `pnpm publish -r --report-summary` → existing `pnpm-publish-summary.json` envelope
`{ publishedPackages: [...] }` is preserved, but
each entry is upgraded to the same per-package shape
(additive — `name` and `version` are still present).
Implementation:
- `pack.api()` computes `unpackedSize` while it still has the filesMap.
- `publishPackedPkg` returns a `PublishSummary` (SHA-1 `shasum` + SHA-512 `integrity`
via `node:crypto`, no new deps) instead of `void`.
- `bundled` normalizes `bundledDependencies` / `bundleDependencies` per npm's semantics
(including `true` → all dep names).
- `recursivePublish` collects per-package `PublishSummary` objects and returns them; the
existing manifest-pick fallback is kept for paths that don't produce a summary.
- `publish.handler` returns `{ output, exitCode }` with the serialized summary (object
or array) when `opts.json` is set; main.ts already writes `output` to stdout.
Refs #11476
Refs nrwl/nx#35575
Amp-Thread-ID: https://ampcode.com/threads/T-019df90f-8f75-763f-b528-4602e870a972
Co-authored-by: Charlie Croom <charlie+amp@noreply.com>
Co-authored-by: Amp <amp@ampcode.com>
* 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.
Add a "Working with GitHub PRs, Issues, and Comments" section to AGENTS.md covering three rules:
Keep PR title and description current when pushing new changes.
Reply to resolved review comments with the resolution + commit hash and close the conversation.
Sign all agent-authored comments, issues, and PRs with a footer that names the agent and model.
Expose the helpers that build the `configByUri` registry-config map
from a flat rawConfig-style auth dict so that downstream consumers
(e.g. third-party clients of `@pnpm/installing.client` /
`@pnpm/store.connection-manager`) can produce the same configByUri
shape pnpm itself uses, without re-implementing the parsing logic.
- pnpm v11 silently drops local-only settings (e.g. `nodeLinker`, `hoistPattern`, `linkWorkspacePackages`) when they appear in the global `config.yaml`. Users had no way to tell their global configuration was being ignored.
- The reader now emits a warning that names the ignored keys and the path of the global config file, and directs users to either move the settings to a project-level `pnpm-workspace.yaml` or share them across projects via [config dependencies](https://pnpm.io/11.x/config-dependencies).
- Allowed keys (e.g. `dangerouslyAllowAllBuilds`, proxy settings) continue to be accepted with no warning.
close#11429
- When updating `pnpm-workspace.yaml`, existing keys keep their positions instead of being globally re-sorted alphabetically.
- New keys are inserted in alphabetical position when the existing layout is sorted; otherwise they are appended at the end. A leading `packages` key is treated as a sorted-first slot, matching the conventional pnpm layout.
- Blank lines between top-level fields are propagated to newly inserted entries so they match the surrounding style — including the case where a new key sorts to the front and demotes the previously-first pair.
* fix(config): accept uppercase PNPM_CONFIG_* env vars
Env vars are case-sensitive on macOS/Linux, so PNPM_CONFIG_USERCONFIG —
the rename suggested by the v11 migration guide — was silently ignored
because parseEnvVars only matched lowercase pnpm_config_*. Also wire the
env var into the early npmrcAuthFile lookup so it actually decides which
user-level .npmrc gets read.
Closes#11465
* chore: add changeset for npmrc auth file env-var load order
* test: cover lowercase pnpm_config_npmrc_auth_file env var
Matches the exact env var name reported in #11465. Without the early
env-var lookup before loadNpmrcConfig, this case is parsed too late
to actually load the custom .npmrc.
* test: lock precedence when both lowercase and uppercase env vars are set
Closes#10684. Running `pnpm config set` triggers `resolveAndInstallConfigDeps` before the new config (e.g. auth token) is saved to `.npmrc`, causing a 401 crash when the config dependency is hosted in a private registry.
This patch adds a `tolerateConfigDependenciesErrors` flag to `installConfigDepsAndLoadHooks`. For `cmd === 'config'`, `'set'` and `'get'`, installation failures are now caught and logged at debug level so the command can proceed and actually write / read the auth token.
`pnpm set` and `pnpm get` are separate top-level commands whose handlers delegate to the `config` command internally, so they are explicitly listed alongside `'config'`; users can hit #10684 via any of the three entry points.
Plugin pnpmfile paths from `configDependencies` are resolved by checking each file's existence on disk, so previously-installed hooks survive a transient install failure and `requireHooks` won't throw `PNPMFILE_NOT_FOUND` when nothing has been installed yet.
Covers the fix with:
- Unit tests in `pnpm/test/getConfig.test.ts` exercising the `installConfigDepsAndLoadHooks` contract (success, tolerated error, rethrown error, store creation/close errors propagating) and the on-disk pnpmfile resolution behavior.
- Four e2e tests in `pnpm/test/configurationalDependencies.test.ts` that spawn the pnpm CLI against a bogus `configDependency` and assert each entrypoint (`pnpm config set`, `pnpm config get`, `pnpm set`, `pnpm get`) still works.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
pnpm v10 setup added PNPM_HOME (not PNPM_HOME/bin) to PATH and wrote
a pnpm bootstrap shim there. After upgrading to v11, that shim still
points into the old .tools/<version> install, so PATH continues to
resolve `pnpm` to the pre-update version even though the new version
was installed under global/v11.
Detect that layout during self-update, refresh the shims at PNPM_HOME
so the upgrade actually takes effect, and warn the user to run
`pnpm setup` for a clean migration to the v11 PATH layout.
Closes#11464.
Fixes#10594, catalogs not being read from the workspace when using the `catalog:` protocol with the `pnpm dlx` / `pnpx` command, resulting in a catalog entry not found error.
Added e2e tests to check if the workspace config is actually loaded. Also added that pnpm dlx reads the retry options from the workspace (Could potentially put that in a separate PR)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
* **Bug Fixes**
* Fixed catalog resolution when using the `catalog:` protocol with `pnpm dlx` / `pnpx` so catalogs are correctly read from the workspace.
* **New Features**
* `dlx` now inherits workspace catalog and fetch retry/timeout settings so CLI runs respect those local configs.
* **Tests**
* Added tests validating catalog inheritance and failure cases for `dlx` catalog resolution.
* **Chores**
* Updated changeset metadata to mark related packages for patch releases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* fix: refresh ignored builds when allowBuilds changes
* refactor: extract isBuildExplicitlyDisallowed into @pnpm/building.policy
Removes the duplicated ignored-build filter from deps-installer and
deps-restorer and exposes it as `isBuildExplicitlyDisallowed` on
`@pnpm/building.policy`, alongside `createAllowBuildFunction`.
* fix: respect ignoredWorkspaceStateSettings in allowBuilds stale-state check
The fallback that flagged installs when allowBuilds went from unset to
non-empty bypassed the ignoredSettings filter, so callers that explicitly
opted out of allowBuilds tracking (via ignoredWorkspaceStateSettings)
could still be forced into a redundant install.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
The `WARN` and `ERR_PNPM_*` labels in pnpm's output relied entirely on a colored background to stand out from the surrounding text. In terminals without color — `NO_COLOR` set, output piped, dumb terminals — the badge collapsed into bare `WARN` / `ERR_PNPM_FOO` and became hard to spot inside the message.
This PR wraps each label in brackets (`[WARN]`, `[ERR_PNPM_FOO]`, `[ERROR]`). The bracket characters are painted in the same color as the badge background, so in a color-capable terminal they appear as plain padding inside the colored badge — the rendering matches what we had before. When ANSI is stripped the brackets reappear as ordinary text, giving the label a clear delimiter.
## Summary
Closes#11423.
`pnpm-darwin-x64.tar.gz` and `@pnpm/macos-x64` are removed because the binaries they contain segfault at startup on Intel Macs and the underlying bug is upstream and unfixed.
## Why this isn't a fix in code
The crash happens in `__cxx_global_var_init` with `EXC_BAD_ACCESS (code=1, address=0x3)` — the unprocessed-chain-entry tag — in dyld's chained-fixup processing. PR #11415's hypothesis was that `ldid`'s page hashes were the cause, but switching to native `codesign` in #11415 didn't fix it: the upstream minimal repro in [nodejs/node#62893](https://github.com/nodejs/node/issues/62893) is `node --build-sea` + `codesign --sign -` + run, with no pnpm and no `ldid`, and it still crashes. The corruption is in LIEF's Mach-O surgery during `--build-sea` for x64 — chained-fixup chain entries get rewritten incorrectly when the SEA segment is inserted, and re-signing produces a valid signature over the broken bytes.
The Node.js team is not going to fix this:
- [nodejs/node#60250](https://github.com/nodejs/node/pull/60250) (merged) — *"It's unlikely that anyone would invest in fixing them on x64 macOS in the near future, now that x64 macOS is being phased out."* They skipped the SEA tests on x64 macOS rather than chase the bug.
- [nodejs/node#59553](https://github.com/nodejs/node/issues/59553) (open) — long-running test failures on macOS x64 with the same root cause (sometimes surfacing as `unsupported thread-local, larger than 4GB`).
`@yao-pkg/pkg` works around it by appending the JS payload to the file tail and using a custom-patched Node binary that reads from the tail at startup; this avoids Mach-O surgery entirely. We can't reuse pack-app for that because vanilla Node from nodejs.org doesn't read tail-appended payloads — only pkg-fetch's patched binaries do — so adopting that path would mean re-implementing pkg-fetch for one target. For now we're dropping the broken artifact rather than introducing a second build mechanism.
## Changes
- **`pnpm/artifacts/exe/package.json`** — remove `@pnpm/macos-x64` from `optionalDependencies`; remove `darwin-x64` from `pnpm.app.targets`.
- **`.meta-updater/src/index.ts`** — remove `@pnpm/macos-x64` from the enforced `optionalDependencies` list (otherwise `meta-updater` would put it back).
- **`pnpm/artifacts/exe/scripts/build-artifacts.ts`** — drop `darwin-x64` from `narrowTargets` so dev-local builds match the published matrix; comment explains why.
- **`__utils__/scripts/src/copy-artifacts.ts`** — stop creating `pnpm-darwin-x64.tar.gz` so the GitHub release page no longer ships it.
- **`pnpm/artifacts/darwin-x64/`** — deleted (was the workspace source for `@pnpm/macos-x64`).
- **`pnpm/artifacts/exe/setup.js`** — wraps the `import.meta.resolve('${pkgName}/package.json')` lookup in `try`/`catch`. On Intel Mac specifically, prints a clear message pointing at this issue, the upstream Node.js issue, and the two workarounds (`npm install -g pnpm` to use the system Node.js, or stay on pnpm 10.x). Other unsupported hosts get a generic message in the same shape. Exits non-zero so the install fails loudly instead of silently leaving a broken `pnpm`.
- **`pnpm-lock.yaml`** — regenerated.
- **`.changeset/drop-darwin-x64-broken-sea.md`** — patch bumps for `@pnpm/exe` and `pnpm` with user-facing explanation and pointers.
Docs side already lists this limitation under `pack-app` Known limitations: pnpm/pnpm.io@36d962f6 / pnpm/pnpm.io@91f45632.
## Compat
- Intel Mac users on existing `@pnpm/exe` (≤ 11.0.4) keep working with the (broken) old binary they already have.
- `pnpm self-update` from an Intel Mac on an older `@pnpm/exe` will hit the new `setup.js` error path with a clear pointer to the workarounds.
- New Intel Mac installs via `npm install -g @pnpm/exe` will fail loudly with the same pointer.
- Install via `npm install -g pnpm` (the JS-only package, uses system Node) is unaffected and remains the recommended path.
- The `install.sh` from `get.pnpm.io` will fail with a 404 on the missing `pnpm-darwin-x64.tar.gz`. That's a separate repo and a follow-up — happy to do that as a second PR.
`pnpm dlx` (and `pnpx`/`pnx`/`pnpm create`) now mirrors the `pnpm add -g` flow when the launched package's transitive deps have install scripts:
- dlx overrides `strictDepBuilds: false` for its install so the v11 default no longer turns ignored builds into an `ERR_PNPM_IGNORED_BUILDS` error. Without this, `pnpx @google/gemini-cli` (and similar — `node-pty`, `@github/keytar`) failed outright and forced users to retry with `--allow-build=<pkg>` for every offending dependency.
- After install, dlx detects skipped builds via `getAutomaticallyIgnoredBuilds` and runs the same interactive `approve-builds` prompt as `pnpm add -g`. In non-interactive mode the install is committed with builds skipped, matching `pnpm add -g` in CI; users who need those scripts can re-invoke with `--allow-build=<pkg>` to force a fresh cache key.
- If the install errors for unrelated reasons (network, etc.) the partially-populated prepare directory is removed so the next dlx run starts clean.
Closes#11444.
### Plumbing
- Exports `getAutomaticallyIgnoredBuilds` from `@pnpm/building.commands` so dlx can detect skipped builds without re-implementing modules-yaml reading.
- Adds `strictDepBuilds` (optional) to `InstallCommandOptions` — already accepted at runtime via the spread, this just makes it explicit at the type level so callers can override it.
Fixes#11439.
When `strictPeerDependencies: true` causes `ERR_PNPM_PEER_DEP_ISSUES`, the peer dependency issues are again rendered inline — using the **same format as `pnpm peers check`** — so users (and CI tools like Renovate) can see what failed without running another command.
The non-strict warning path is unchanged: it still emits the short "Run `pnpm peers check`" hint.
### Behavior
`strictPeerDependencies: true`:
```
ERR_PNPM_PEER_DEP_ISSUES Unmet peer dependencies
✕ unmet peer react
Installed: 17.0.2
Wanted:
^18.2.0:
react-dom@18.2.0
hint: To disable failing on peer dependency issues, add the following to pnpm-workspace.yaml in your project root:
strictPeerDependencies: false
```
`strictPeerDependencies: false` (unchanged):
```
WARN Issues with peer dependencies found. Run "pnpm peers check" to list them.
```
### Implementation
- Added a new `@pnpm/deps.inspection.peers-issues-renderer` package at `deps/inspection/peers-issues-renderer/`, alongside its data producer `@pnpm/deps.inspection.peers-checker`. It exposes a single `renderPeerIssues()` that emits the flat issue list previously inlined in `pnpm peers check`.
- Removed the duplicated formatter from `deps/inspection/commands/src/peers.ts` and made the `pnpm peers check` command consume the new renderer.
- `cli/default-reporter/src/reportError.ts`: `reportPeerDependencyIssuesError` now calls the shared `renderPeerIssues()` and prefixes the hint block with the rendered output. Tests strip ANSI escapes before substring assertions so they stay correct under `FORCE_COLOR=1`.
Result: a single renderer is shared between the install error and the `pnpm peers check` command — output is identical between the two paths.
Restores `--json` / `--parseable` / `--long` support on `pnpm -g ls` and tightens `--depth>0` semantics around isolated global installs. Closes#11440.
- **`--json` / `--parseable` (the regression):** aggregate global packages from all isolated install dirs into a single synthesized `PackageDependencyHierarchy` and dispatch to the existing `renderJson` / `renderParseable` / `renderTree`. Output shape matches pnpm 10 (`result[0].dependencies[name].version`), so tools like `npm-check-updates` work again.
- **`--depth>0`:** the v11 architecture installs each global package into its own isolated dir with its own lockfile, so merging transitive trees across installs would be incoherent. New behavior:
- One global install dir total → fast-path delegate to the regular `list` flow with `params` unchanged, so `listForPackages` can match top-level *or* transitive packages.
- Multiple installs, params narrow to one install dir (top-level alias match) → drop the params and render that install dir's full tree.
- Multiple installs, params don't narrow → throw `ERR_PNPM_GLOBAL_LS_DEPTH_NOT_SUPPORTED` with a message asking the user to filter to a single global package or omit `--depth`.
The regression was introduced by the isolated global packages refactor (#10697), which added a custom `listGlobalPackages` shortcut that always returned plain text and ignored format flags.
The native publish flow introduced in v11 (replacing the `npm publish`
shell-out with `libnpmpublish`) only read the registry from `registries`
config, ignoring `publishConfig.registry` from the package's
`package.json`. Restore the prior behavior by giving
`publishConfig.registry` precedence over the configured registries.
Closes#11419
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.
* fix(config): default minimumReleaseAgeStrict to true when user sets minimumReleaseAge
Without this, a user-set `minimumReleaseAge` would silently fall back to
installing an immature version when no mature version satisfied the
requested range, making the setting look like it had no effect (#11433).
The built-in default of `minimumReleaseAge` (1440) stays non-strict for
backward compatibility, and an explicit `minimumReleaseAgeStrict: false`
is still respected.
* chore(changeset): downgrade to patch
* fix(config): apply minimumReleaseAgeStrict default after env var parsing
Move the strict-default logic to run after `parseEnvVars` so
`pnpm_config_minimum_release_age` is also covered.
* test(config): also assert minimumReleaseAge in the strict=false test
* fix(self-update): do not downgrade when latest dist-tag is older
`pnpm self-update` defaults to the `latest` dist-tag, but `latest` on the
registry can lag the installed version when a new major has shipped
without being tagged. Refuse to downgrade in that case. Users can still
run `pnpm self-update latest` (explicit) to force the downgrade.
Closes#11418
* fix(self-update): use lockfile-pinned version for project-pin downgrade check
When a project pins pnpm via a range (e.g. `devEngines.packageManager.version: ">=8.0.0"`)
and the env lockfile pins an exact version above the range's lower bound,
the previous guard compared the resolved `latest` against `semver.minVersion(spec)`
and missed the downgrade. Read `packageManagerDependencies.pnpm.version` from
`pnpm-lock.yaml` and use the max of (lockfile-pinned, spec.minVersion) as the
current version. Also fix the explicit-`latest` test which mocked `latest`
as newer than the current version, defeating its own assertion.
* chore(engine.pm.commands): add lockfile/fs project reference to tsconfig