fix(publish): let trusted publishing (OIDC) override a static _authToken#11495
Conversation
When a registry has trusted publishing configured for a package, the OIDC-derived token now takes precedence over any statically configured `_authToken` (e.g. one written by `pnpm config set` from an `NPM_TOKEN` secret in CI). This mirrors the npm CLI's behavior in `lib/utils/oidc.js`. Previously `fetchTokenAndProvenanceByOidcIfApplicable` bailed out as soon as a token was already set on the publish options, so any release workflow still wiring in `NPM_TOKEN` would never even attempt the OIDC exchange, silently downgrading to legacy token-based publishing (no `trustedPublisher` on the npm metadata, no provenance attestation). Now the exchange is always attempted in supported CI environments, succeeds per-package, and falls back to the static token only when OIDC is not applicable (no CI env, no trusted publisher configured, exchange fails). This also applies on every iteration of recursive publish, so each workspace package independently attempts trusted publishing. As a small bonus, a transient failure of the provenance-determination step no longer discards the freshly-fetched OIDC authToken — again matching npm's behavior.
|
Caution Review failedFailed to post review comments Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughOIDC-based trusted publishing now takes precedence over a configured static token during pnpm publish: when an OIDC exchange yields an auth token, that token is used (with provenance attempted); if OIDC is unavailable or exchange fails, the configured static token is used as a fallback. NPM_ID_TOKEN is recognized as a CI-agnostic injection point. ChangesOIDC Precedence Implementation
Sequence DiagramsequenceDiagram
participant Publisher
participant CI
participant OIDC as OIDC Provider
participant Auth as Auth Derivation
participant Registry
participant Static as Static Token Store
Publisher->>CI: Check for id token (env / CI OIDC)
alt id token available
Publisher->>OIDC: Exchange id token for auth token
OIDC-->>Auth: Return auth token
Auth->>Registry: Optional provenance/visibility check
alt provenance succeeds
Registry-->>Publisher: Provenance confirmed
Publisher->>Registry: Publish using OIDC-derived token
else provenance fails
Registry-->>Publisher: Provenance check failed
Publisher->>Registry: Publish using OIDC-derived token (no provenance)
end
else no id token or exchange fails
Static-->>Publisher: Provide configured static token
Publisher->>Registry: Publish using static token
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR updates pnpm publish’s authentication precedence so that an OIDC-derived token from npm Trusted Publishing can override a statically configured _authToken, aligning pnpm’s behavior with npm CLI and restoring provenance/trustedPublisher metadata when Trusted Publishing is configured.
Changes:
- Always attempt OIDC token exchange (when applicable) and overwrite
publishOptions.tokenwhen OIDC returns an auth token. - Preserve the OIDC auth token even if provenance detection fails (don’t discard the token on
ProvenanceError). - Add a changeset documenting the new precedence behavior for both single and recursive publish.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| releasing/commands/src/publish/publishPackedPkg.ts | Changes auth precedence to let OIDC override static tokens; adjusts provenance error handling; exports OIDC helper for unit tests. |
| .changeset/oidc-precedence-over-static-token.md | Announces the behavior change as a patch for pnpm publish and releasing commands. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
After the previous commit, `fetchTokenAndProvenanceByOidc` runs on every
publish — including local ones where the user has a static token configured
and is not in any CI environment. In that case `getIdToken` returns
`undefined` (silently, by design), but the orchestrator was still emitting
`globalWarn('Skipped OIDC: idToken is not available')`, which would now
appear on most local publishes.
Drop the warn: the missing-idToken signal means "OIDC isn't applicable
here", which is the normal case for any local publish. Real configuration
errors in a CI environment (e.g. missing `id-token: write` permission)
already surface separately as `IdTokenError` and are still warned about.
`NPM_TOKEN` is a CI-workflow naming convention for an npm secret — pnpm itself only reads `_authToken` from its own config. The comment should describe what pnpm sees, not the upstream env var that some workflow happens to copy into the config.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
releasing/commands/src/publish/publishPackedPkg.ts (2)
145-182:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMake
tokenHelpera real fallback, not an eager precondition.
publishOptions.tokenis resolved fromextractToken(creds)on Line 169 before the OIDC branch on Lines 179-182 runs. If the registry usestokenHelper, we still execute the helper — and can fail the publish — even when OIDC would succeed and should take precedence.Proposed fix
+ const staticAuthToken = creds?.authToken + const tokenHelper = creds?.tokenHelper const publishOptions: PublishOptions = { access, defaultTag, fetchRetries, fetchRetryFactor, @@ ca: tls?.ca, cert: tls?.cert, key: tls?.key, npmCommand: 'publish', - token: creds && extractToken(creds), + token: staticAuthToken, username: creds?.basicAuth?.username, password: creds?.basicAuth?.password, } if (registry) { const oidcTokenProvenance = await fetchTokenAndProvenanceByOidc(manifest.name, registry, options) if (oidcTokenProvenance?.authToken) { publishOptions.token = oidcTokenProvenance.authToken } publishOptions.provenance ??= oidcTokenProvenance?.provenance appendAuthOptionsForRegistry(publishOptions, registry) } + + if (publishOptions.token == null && tokenHelper) { + publishOptions.token = executeTokenHelper(tokenHelper, { globalWarn }) + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@releasing/commands/src/publish/publishPackedPkg.ts` around lines 145 - 182, The code eagerly calls extractToken(creds) when creating publishOptions, causing tokenHelper logic to run even when OIDC should win; remove the token extraction from the initial publishOptions object (set token undefined or omit it) and instead set publishOptions.token after the OIDC check: call fetchTokenAndProvenanceByOidc(manifest.name, registry, options) and if oidcTokenProvenance?.authToken use that, otherwise then call extractToken(creds) as the fallback; update the assignment logic around publishOptions.token to ensure extractToken(creds) is only invoked when OIDC did not provide a token.
68-75:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSkip the OIDC exchange during
--dry-run.The dry-run return happens on Lines 93-96, after
createPublishOptions()has already reached the new OIDC path on Line 179. That meanspnpm publish --dry-runcan now hit token/provenance network calls and fail on transient auth or registry errors even though nothing is being published.Proposed fix
- if (registry) { + if (registry && !options.dryRun) { // OIDC takes precedence over a configured static `_authToken`, mirroring the npm CLI's // behavior (see https://github.com/npm/cli/blob/7d900c46/lib/utils/oidc.js). Trusted // publishing wins whenever the registry has it configured for the package; the static // token is used only as a fallback when OIDC is not applicable. const oidcTokenProvenance = await fetchTokenAndProvenanceByOidc(manifest.name, registry, options)Also applies to: 93-96, 174-180
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@releasing/commands/src/publish/publishPackedPkg.ts` around lines 68 - 75, publishPackedPkg is calling createPublishOptions (which performs the OIDC exchange) even for dry runs, causing network/auth calls on --dry-run; fix by short-circuiting OIDC when opts.dryRun is true: either move the dry-run early-return to before the createPublishOptions call in publishPackedPkg or add/propagate a skipOidc (or skipProvenanceExchange) boolean to createPublishOptions so it skips any network/OIDC flow when opts.dryRun is true; ensure all createPublishOptions invocations in this function use that flag so no OIDC exchange occurs during dry-run.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@releasing/commands/src/publish/publishPackedPkg.ts`:
- Around line 145-182: The code eagerly calls extractToken(creds) when creating
publishOptions, causing tokenHelper logic to run even when OIDC should win;
remove the token extraction from the initial publishOptions object (set token
undefined or omit it) and instead set publishOptions.token after the OIDC check:
call fetchTokenAndProvenanceByOidc(manifest.name, registry, options) and if
oidcTokenProvenance?.authToken use that, otherwise then call extractToken(creds)
as the fallback; update the assignment logic around publishOptions.token to
ensure extractToken(creds) is only invoked when OIDC did not provide a token.
- Around line 68-75: publishPackedPkg is calling createPublishOptions (which
performs the OIDC exchange) even for dry runs, causing network/auth calls on
--dry-run; fix by short-circuiting OIDC when opts.dryRun is true: either move
the dry-run early-return to before the createPublishOptions call in
publishPackedPkg or add/propagate a skipOidc (or skipProvenanceExchange) boolean
to createPublishOptions so it skips any network/OIDC flow when opts.dryRun is
true; ensure all createPublishOptions invocations in this function use that flag
so no OIDC exchange occurs during dry-run.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 953a84bd-4869-4d60-96d2-fb36ce0051b6
📒 Files selected for processing (1)
releasing/commands/src/publish/publishPackedPkg.ts
Adds unit tests for `fetchTokenAndProvenanceByOidc` that lock in the behavioral changes from this PR: - Token-exchange success + visibility success → returns OIDC authToken and provenance=true. - Token-exchange 4xx (registry has no trusted publisher for the package) → returns undefined; visibility endpoint is NOT called, so the caller can fall back to a static `_authToken`. - Token-exchange success + visibility 5xx → still returns the OIDC authToken (provenance undefined). Regression coverage for the bonus fix that stops a transient visibility failure from discarding the freshly-minted OIDC token. - options.provenance set explicitly → visibility endpoint is skipped. - No NPM_ID_TOKEN in env → orchestrator returns undefined silently and makes no HTTP calls (the common case for local publishes — must not warn or hit the network). Network mocking is done via undici's `MockAgent` (intercepting at the real HTTP layer that `@pnpm/network.fetch` wraps), with `disableNetConnect()` so any unmocked traffic fails loudly. `ci-info` is mocked to claim the runtime is GitLab CI so `NPM_ID_TOKEN` from env becomes the idToken without GitHub-Actions request-token endpoints being hit. Pulls in `undici` as a devDependency of `@pnpm/releasing.commands` (it's already a transitive dep through `@pnpm/network.fetch`, just not declared at this layer).
cspell flags the literal `%2fpkg` in the URL-encoded path; deriving the
escaped form with `replace('/', '%2f')` matches what the source does
(via `npa(...).escapedName`) and avoids the dictionary hit.
The previous comment listed `GITLAB || GITHUB_ACTIONS` as if those were the only CIs that support OIDC. They're actually the only CIs pnpm (and npm CLI) currently *recognize* — CircleCI exposes CIRCLE_OIDC_TOKEN_V2 and others have similar mechanisms, but they're not gated in. Worth a follow-up; flagging it next to the mock so the gap doesn't get lost.
`getIdToken` previously gated all OIDC attempts on `ciInfo.GITHUB_ACTIONS || ciInfo.GITLAB`, which blocked CI providers that expose OIDC tokens through their own env vars (CircleCI's `CIRCLE_OIDC_TOKEN_V2`, Buildkite, etc.). The `NPM_ID_TOKEN` env var was always intended as a CI-agnostic injection point, but the gate made it useless for any CI we didn't recognize. Drop the gate. Now any environment with `NPM_ID_TOKEN` set in env will attempt the OIDC token exchange, regardless of which CI (or none) we're running in. The GitHub Actions request-token flow remains gated on `GITHUB_ACTIONS` because it depends on GHA-specific env vars. `determineProvenance` is intentionally unchanged: deciding whether to attach a SLSA provenance attestation requires reading CI-specific JWT claims (`repository_visibility` for GHA, `project_visibility` + `SIGSTORE_ID_TOKEN` for GitLab), so until we add claim-parsing for other providers, CircleCI users will get trusted publishing without provenance. They can opt in to provenance explicitly with `--provenance=true` if their setup supports it. Test comment updated to match. Changeset notes the broadened support.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
releasing/commands/src/publish/oidc/idToken.ts (1)
11-14: 💤 Low valueConsider removing the unused
GITLABproperty fromIdTokenCIInfo.Since GitLab-specific handling was removed and
GITLABis no longer destructured ingetIdToken(line 79), this interface property is now dead code. Removing it would keep the interface in sync with actual usage.export interface IdTokenCIInfo { GITHUB_ACTIONS?: boolean - GITLAB?: boolean }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@releasing/commands/src/publish/oidc/idToken.ts` around lines 11 - 14, The IdTokenCIInfo interface contains a dead property GITLAB that is no longer used (getIdToken no longer destructures it); remove the GITLAB property from the IdTokenCIInfo interface to keep the type in sync with actual usage and avoid dead code, ensuring only GITHUB_ACTIONS remains defined.releasing/commands/test/publish/oidcOrchestrator.test.ts (1)
72-76: 💤 Low valueConsider restoring original env var values in
afterAll.If
NPM_ID_TOKENorSIGSTORE_ID_TOKENwere set before the test suite ran (e.g., in a CI environment), deleting them inafterAllcould affect subsequent test files. Capturing and restoring originals would improve test isolation.+let originalNpmIdToken: string | undefined +let originalSigstoreIdToken: string | undefined + beforeAll(() => { originalDispatcher = getGlobalDispatcher() + originalNpmIdToken = process.env['NPM_ID_TOKEN'] + originalSigstoreIdToken = process.env['SIGSTORE_ID_TOKEN'] }) // ... afterAll(() => { setGlobalDispatcher(originalDispatcher) - delete process.env['NPM_ID_TOKEN'] - delete process.env['SIGSTORE_ID_TOKEN'] + if (originalNpmIdToken !== undefined) { + process.env['NPM_ID_TOKEN'] = originalNpmIdToken + } else { + delete process.env['NPM_ID_TOKEN'] + } + if (originalSigstoreIdToken !== undefined) { + process.env['SIGSTORE_ID_TOKEN'] = originalSigstoreIdToken + } else { + delete process.env['SIGSTORE_ID_TOKEN'] + } })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@releasing/commands/test/publish/oidcOrchestrator.test.ts` around lines 72 - 76, The afterAll block currently deletes NPM_ID_TOKEN and SIGSTORE_ID_TOKEN which can wipe pre-existing CI envs; capture their original values before the suite runs (e.g., store const originalNpmIdToken = process.env['NPM_ID_TOKEN'] and const originalSigstoreIdToken = process.env['SIGSTORE_ID_TOKEN'] at top-level or in beforeAll) and then in afterAll restore them (set process.env[...] = original... or delete if the original was undefined) while continuing to restore originalDispatcher; update the afterAll to use these saved originals instead of unconditionally deleting the vars.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@releasing/commands/src/publish/oidc/idToken.ts`:
- Around line 11-14: The IdTokenCIInfo interface contains a dead property GITLAB
that is no longer used (getIdToken no longer destructures it); remove the GITLAB
property from the IdTokenCIInfo interface to keep the type in sync with actual
usage and avoid dead code, ensuring only GITHUB_ACTIONS remains defined.
In `@releasing/commands/test/publish/oidcOrchestrator.test.ts`:
- Around line 72-76: The afterAll block currently deletes NPM_ID_TOKEN and
SIGSTORE_ID_TOKEN which can wipe pre-existing CI envs; capture their original
values before the suite runs (e.g., store const originalNpmIdToken =
process.env['NPM_ID_TOKEN'] and const originalSigstoreIdToken =
process.env['SIGSTORE_ID_TOKEN'] at top-level or in beforeAll) and then in
afterAll restore them (set process.env[...] = original... or delete if the
original was undefined) while continuing to restore originalDispatcher; update
the afterAll to use these saved originals instead of unconditionally deleting
the vars.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d3fb62cc-1a7d-4afb-9046-07316b65fcd5
📒 Files selected for processing (4)
.changeset/oidc-precedence-over-static-token.mdreleasing/commands/src/publish/oidc/idToken.tsreleasing/commands/src/publish/publishPackedPkg.tsreleasing/commands/test/publish/oidcOrchestrator.test.ts
CodeQL flags `replace('/', '%2f')` as incomplete because it replaces
only the first occurrence. npm package names only ever have one `/`
(the scope separator), so this isn't a real bug, but the global form
is just as correct and silences the alert.
…ng (#11496) 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.
…ng (#11496) 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.
…ken (#11495) ## 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.
Summary
pnpm publishwill now let an OIDC-derived token from npm's trusted publishing flow take precedence over a statically configured_authToken(e.g. one written from anNPM_TOKENCI secret), mirroring the npm CLI's behavior.Background
We noticed that
pnpm@11.0.0-alpha.5was published with trusted publishing on npm (its registry metadata has_npmUser.trustedPublisherand a SLSAattestations.provenanceblock) — butpnpm@11.0.6was not (_npmUser: pnpmuser, no attestations). The two were published by different clients: alpha.5 went out vianpm publish(its metadata carries_npmVersion: 11.6.2, whichpnpm publishnever sets), while 11.0.6 went out viapn releasefrom.github/workflows/release.yml.Both paths run in CI with
id-token: writegranted, and both have the samepn config set //registry.npmjs.org/:_authToken "\${NPM_TOKEN}"step before publish. The difference is purely in how each client orders the auth precedence:_authToken. The static token only acts as a fallback when OIDC isn't applicable (no trusted publisher configured, exchange fails, etc.).releasing/commands/src/publish/publishPackedPkg.ts, the oldfetchTokenAndProvenanceByOidcIfApplicable). 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_TOKENsilently downgraded to legacy token-based publishing — notrustedPublisheron the metadata, no provenance attestation. That's the fix here.Changes
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.
Suggested follow-up (separate PR)
Once this lands, the `pn config set "//registry.npmjs.org/:_authToken" "${NPM_TOKEN}"` step in `.github/workflows/release.yml` becomes unnecessary as long as a trusted publisher is configured on npm for every package the release script publishes. Keeping it around is harmless — the static token now just acts as a fallback — but cleaner to drop.
Test plan
Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
New Features
Bug Fixes
Tests