Skip to content

fix(publish): let trusted publishing (OIDC) override a static _authToken#11495

Merged
zkochan merged 8 commits into
mainfrom
fix/oidc-precedence
May 6, 2026
Merged

fix(publish): let trusted publishing (OIDC) override a static _authToken#11495
zkochan merged 8 commits into
mainfrom
fix/oidc-precedence

Conversation

@zkochan

@zkochan zkochan commented May 6, 2026

Copy link
Copy Markdown
Member

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.

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.

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

  • `tsgo --build` clean across the workspace
  • `eslint` clean on the changed file
  • All 44 existing OIDC unit tests pass (`test/publish/oidc*`)
  • End-to-end verification will land naturally on the next tagged release: the published package's npm registry metadata should show `_npmUser.trustedPublisher` and an `attestations.provenance` block, matching `11.0.0-alpha.5`'s metadata rather than `11.0.6`'s.

Written by an agent (Claude Code, claude-opus-4-7).

Summary by CodeRabbit

  • New Features

    • OIDC trusted publishing now takes precedence over configured static tokens; each package in recursive/workspace publishes attempts OIDC independently.
    • NPM_ID_TOKEN is now honored as a CI-agnostic injection point for OIDC ID tokens.
  • Bug Fixes

    • Provenance retrieval failures no longer block use of a successful OIDC-derived token.
  • Tests

    • Added tests covering OIDC token exchange, provenance handling, and fallback behaviors.

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.
Copilot AI review requested due to automatic review settings May 6, 2026 14:04
@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown

Caution

Review failed

Failed to post review comments

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

OIDC-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.

Changes

OIDC Precedence Implementation

Layer / File(s) Summary
Behavior Specification
.changeset/oidc-precedence-over-static-token.md
Adds changeset documenting that OIDC-derived trusted publishing overrides static tokens, applied per-package in recursive publishes, with static-token fallback and NPM_ID_TOKEN as CI-agnostic injection point.
Call Site Integration
releasing/commands/src/publish/publishPackedPkg.ts
createPublishOptions now calls the new exported fetchTokenAndProvenanceByOidc unconditionally and applies any returned auth token/provenance to publish options, replacing the prior internal conditional helper.
Core Token Fetch Logic
releasing/commands/src/publish/publishPackedPkg.ts
Adds exported fetchTokenAndProvenanceByOidc(packageName, registry, options) that obtains an ID token, exchanges it for an auth token, and attempts provenance/visibility determination; on provenance errors it preserves and returns the auth token without provenance. Removes the old internal helper.
OIDC ID Token Retrieval
releasing/commands/src/publish/oidc/idToken.ts
Streamlines id token acquisition to be CI-agnostic: removes GitLab-specific branching from the flow, documents NPM_ID_TOKEN as the canonical injection point, and relies on GitHub Actions OIDC flow when applicable.
Tests
releasing/commands/test/publish/oidcOrchestrator.test.ts
Adds tests covering successful token exchange and provenance, token-exchange 4xx failure, preservation of auth token when visibility check fails, skipping visibility check when provenance option disabled, and no network activity when no id token present.
Dev Dependency
releasing/commands/package.json
Adds undici as a devDependency to support HTTP mocking in tests.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • pnpm/pnpm#11478: Modifies the same publishPackedPkg.ts area—related changes to OIDC token/provenance logic and publish return shape.

Poem

🐇 I sniffed the CI breeze at dawn,

An ID token hopped into my paw.
If provenance stumbles, I still clutch the key,
Each package hops onward, publish and be free.
NPM_ID_TOKEN helps more CIs agree.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: OIDC (trusted publishing) now takes precedence over static authentication tokens in pnpm publish.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/oidc-precedence

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.token when 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.

Comment thread releasing/commands/src/publish/publishPackedPkg.ts Outdated
zkochan added 2 commits May 6, 2026 16:21
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.
Copilot AI review requested due to automatic review settings May 6, 2026 14:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Make tokenHelper a real fallback, not an eager precondition.

publishOptions.token is resolved from extractToken(creds) on Line 169 before the OIDC branch on Lines 179-182 runs. If the registry uses tokenHelper, 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 win

Skip 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 means pnpm publish --dry-run can 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2720e57 and a2c66a5.

📒 Files selected for processing (1)
  • releasing/commands/src/publish/publishPackedPkg.ts

zkochan added 2 commits May 6, 2026 16:39
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.
Comment thread releasing/commands/test/publish/oidcOrchestrator.test.ts Fixed
zkochan added 2 commits May 6, 2026 16:44
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.
Copilot AI review requested due to automatic review settings May 6, 2026 14:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
releasing/commands/src/publish/oidc/idToken.ts (1)

11-14: 💤 Low value

Consider removing the unused GITLAB property from IdTokenCIInfo.

Since GitLab-specific handling was removed and GITLAB is no longer destructured in getIdToken (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 value

Consider restoring original env var values in afterAll.

If NPM_ID_TOKEN or SIGSTORE_ID_TOKEN were set before the test suite ran (e.g., in a CI environment), deleting them in afterAll could 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

📥 Commits

Reviewing files that changed from the base of the PR and between 108674b and 7c8b656.

📒 Files selected for processing (4)
  • .changeset/oidc-precedence-over-static-token.md
  • releasing/commands/src/publish/oidc/idToken.ts
  • releasing/commands/src/publish/publishPackedPkg.ts
  • releasing/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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

zkochan added a commit that referenced this pull request May 6, 2026
…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.
@zkochan zkochan merged commit 6a5156b into main May 6, 2026
13 checks passed
@zkochan zkochan deleted the fix/oidc-precedence branch May 6, 2026 15:35
zkochan added a commit that referenced this pull request May 6, 2026
…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.
zkochan added a commit that referenced this pull request May 6, 2026
…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.
@coderabbitai coderabbitai Bot mentioned this pull request May 12, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants