Skip to content

fix(config): drop unresolved ${VAR} placeholders from .npmrc auth values#11526

Merged
zkochan merged 5 commits into
mainfrom
fix/11513
May 15, 2026
Merged

fix(config): drop unresolved ${VAR} placeholders from .npmrc auth values#11526
zkochan merged 5 commits into
mainfrom
fix/11513

Conversation

@zkochan

@zkochan zkochan commented May 7, 2026

Copy link
Copy Markdown
Member

Summary

Closes #11513.

actions/setup-node writes _authToken=${NODE_AUTH_TOKEN} to .npmrc. When the user relies on OIDC trusted publishing without setting NODE_AUTH_TOKEN, pnpm previously passed the literal placeholder through verbatim — so any time OIDC fallback failed, pnpm sent Authorization: Bearer ${NODE_AUTH_TOKEN} to the registry and the publish came back as a 404.

This worked in v10 because pnpm publish shelled out to npm publish, whose own OIDC flow handled the case.

The fix lives in @pnpm/config.env-replace@4.1.0, which adds an envReplaceLossy variant that returns { value, unresolved } instead of throwing. Unresolved ${VAR} placeholders become '' and are reported back as a list — leaving OIDC trusted publishing as the sole auth source. Resolvable placeholders and ${VAR-default} / ${VAR:-default} fallbacks elsewhere in the same string still expand normally, so a value like pre-${SET}-mid-${UNSET}-${OTHER-default}-post now produces pre-AAA-mid--default-post rather than dropping every placeholder.

Also treats { KEY: undefined } in the env object the same as a missing key (the Record<string, string | undefined> contract), so a ${KEY-default} reaches the fallback in that case.

Changes

  • @pnpm/config.env-replace catalog bumped from ^3.0.2^4.1.0 (pnpm-workspace.yaml, pnpm-lock.yaml)
  • config/reader/src/loadNpmrcFiles.tssubstituteEnv now calls envReplaceLossy and pushes one warning per unresolved placeholder
  • config/reader/test/index.ts + parseCreds.test.ts — regression tests covering the OIDC case, mixed resolvable/unresolved placeholders, explicit-undefined env values, and parseCreds({ authToken: '' })
  • .changeset/oidc-unresolved-env-placeholder.md — patch bump for @pnpm/config.reader and pnpm
  • pacquet/crates/config/{env_replace.rs, npmrc_auth.rs, npmrc_auth/tests.rs} — mirrors the lossy semantics in pacquet's local env_replace_lossy, with matching test coverage

Test plan

  • New tests assert: unresolved ${VAR} drops to '' with a warning; resolvable + -default placeholders still expand in the same value; explicit { KEY: undefined } falls through to -default; parseCreds({ authToken: '' }) returns undefined; normal substitution still works
  • pnpm --filter @pnpm/config.reader test passes the new tests; the 6 unrelated pre-existing failures in test/index.ts are present on the base branch too
  • pnpm --filter pnpm run compile builds the bundle successfully
  • cargo nextest run -p pacquet-config — all 154 pacquet-config tests pass; cargo clippy --deny warnings + cargo fmt --check clean
  • Reporter to verify the fix resolves the OIDC publish flow on a next-release build

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

Summary by CodeRabbit

  • Bug Fixes

    • Unresolved environment placeholders in auth config are now treated as empty instead of literal tokens, preventing failed publishes and allowing OIDC fallback to work.
  • Tests

    • Added regression tests covering placeholder substitution behavior and that empty auth tokens are treated as absent.
  • Chores

    • Added a changeset documenting the patch release.

Review Change Stack

Copilot AI review requested due to automatic review settings May 7, 2026 15:49
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23aeb6fe-5437-40bf-80da-27e11d397eb6

📥 Commits

Reviewing files that changed from the base of the PR and between c82231c and 9b14ac4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • config/reader/src/loadNpmrcFiles.ts
  • pnpm-workspace.yaml
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Standard Style with modifications: trailing commas are used, functions are preferred over classes, functions are declared after they are used (hoisting is relied upon).
Functions should have no more than two or three arguments. If a function needs more parameters, use a single options object instead.
Maintain import order: (1) Standard libraries, (2) External dependencies (sorted alphabetically), (3) Relative imports.

Files:

  • config/reader/src/loadNpmrcFiles.ts
🧠 Learnings (1)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • config/reader/src/loadNpmrcFiles.ts
🔇 Additional comments (2)
config/reader/src/loadNpmrcFiles.ts (1)

5-5: LGTM!

Also applies to: 156-167

pnpm-workspace.yaml (1)

86-86: LGTM!


📝 Walkthrough

Walkthrough

This PR treats unresolved unescaped ${VAR} placeholders in .npmrc auth values as empty (not literal), switches substitution to a lossy mode that reports each unresolved token, adds regression/unit tests, and includes a changeset and workspace catalog bump.

Changes

OIDC Unresolved Env Placeholder Fix

Layer / File(s) Summary
Core implementation: lossy env substitution
config/reader/src/loadNpmrcFiles.ts, pnpm-workspace.yaml
Switches import to envReplaceLossy and updates substituteEnv to return substituted strings while emitting per-placeholder warnings; bumps @pnpm/config.env-replace catalog entry.
Tests: regression & parseCreds unit
config/reader/test/index.ts, config/reader/test/parseCreds.test.ts
Adds Jest regression tests for unresolved ${VAR} in auth values (unset -> '', set -> substituted, mixed placeholders, ${VAR-default} with undefined env) and a unit test ensuring parseCreds treats authToken: '' as absent.
Documentation / Changeset
.changeset/oidc-unresolved-env-placeholder.md
Adds a changeset documenting patch releases for @pnpm/config.reader and pnpm describing the fix for unresolved auth placeholders.

Sequence Diagram

sequenceDiagram
  participant CI as CI / Test Suite
  participant Loader as loadNpmrcFiles.substituteEnv
  participant EnvReplace as envReplaceLossy
  participant Parser as parseCreds

  CI->>Loader: load .npmrc with _authToken=${NODE_AUTH_TOKEN}
  Loader->>EnvReplace: envReplaceLossy(substitution)
  EnvReplace-->>Loader: substituted string + unresolved list
  alt unresolved placeholders present
    Loader->>Loader: emit per-placeholder warnings, treat unresolved as ''
  end
  Loader->>Parser: pass final auth string
  Parser-->>CI: credentials parsed (token or undefined)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 I snuffled through config, by moonlight bright,
Found a ${VAR} that gave the registry a fright,
I nudged it to nothing, no token to show,
So OIDC can bloom and the publishers go,
Hop, patch, and drumroll — the pipeline's alight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 specifically describes the main change: fixing environment variable placeholder substitution in .npmrc auth values by dropping unresolved ones.
Linked Issues check ✅ Passed The PR fully addresses #11513's objective: it prevents unresolved ${NODE_AUTH_TOKEN} placeholders from being forwarded as auth tokens, allowing OIDC fallback to work correctly in GitHub Actions workflows.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the placeholder substitution issue: config reader logic, regression tests, dependency version bump, and changeset documentation.

✏️ 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/11513

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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 fixes a pnpm publish regression in OIDC trusted publishing scenarios where .npmrc contains _authToken=${NODE_AUTH_TOKEN} but NODE_AUTH_TOKEN is unset (as written by actions/setup-node). The change ensures unresolved ${VAR} placeholders don’t get passed through as a literal bearer token.

Changes:

  • Update .npmrc env substitution handling so unresolved ${VAR} placeholders are dropped (become empty) when substitution fails.
  • Add regression tests covering unset vs set NODE_AUTH_TOKEN, and parseCreds({ authToken: '' }) behavior.
  • Add a changeset for patch releases of @pnpm/config.reader and pnpm.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
config/reader/src/loadNpmrcFiles.ts Changes env-substitution failure behavior to drop unresolved ${VAR} placeholders.
config/reader/test/index.ts Adds regression tests reproducing the GitHub Actions OIDC publishing case.
config/reader/test/parseCreds.test.ts Adds coverage ensuring empty authToken is treated as absent.
.changeset/oidc-unresolved-env-placeholder.md Adds patch bumps and release note entry for the fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread config/reader/src/loadNpmrcFiles.ts Outdated

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

Actionable comments posted: 1

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

Inline comments:
In `@config/reader/src/loadNpmrcFiles.ts`:
- Around line 161-168: The current fallback unconditionally strips every
unescaped `${...}` from value, erasing valid placeholders; instead, change the
single global replace to a replace with a callback that inspects each unescaped
placeholder (the same regex context around the existing return value.replace)
and only returns '' for placeholders whose env name has no value and no default
(e.g., process.env[VAR] === undefined and no `-default` in the token); for all
other matches return the original match so mixed resolved/unknown placeholders
are preserved. Target the code around the return
value.replace(/(?<!\\)\$\{[^${}]+\}/g, '') in loadNpmrcFiles.ts and operate on
the captured var name to decide per-match removal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7eb6979a-cfcd-44f5-bdcd-4139c59ec1eb

📥 Commits

Reviewing files that changed from the base of the PR and between 24f3669 and de143ee.

📒 Files selected for processing (4)
  • .changeset/oidc-unresolved-env-placeholder.md
  • config/reader/src/loadNpmrcFiles.ts
  • config/reader/test/index.ts
  • config/reader/test/parseCreds.test.ts

Comment thread config/reader/src/loadNpmrcFiles.ts Outdated
zkochan added 3 commits May 15, 2026 12:31
When `actions/setup-node` writes `_authToken=${NODE_AUTH_TOKEN}` to .npmrc
and the user relies on OIDC trusted publishing without setting
`NODE_AUTH_TOKEN`, pnpm previously passed the literal placeholder through
as an auth token. If OIDC fallback failed for any reason, pnpm sent
`Authorization: Bearer ${NODE_AUTH_TOKEN}` to the registry and the publish
failed with a 404. v10 worked because it shelled out to `npm publish`,
which has its own OIDC flow.

Drop unresolved placeholders when env-replace fails so the value reads as
empty and `parseCreds` treats it as no static credentials, leaving OIDC
trusted publishing as the sole auth source. Closes #11513.

---
Written by an agent (Claude Code, claude-opus-4-7).
The previous fallback in `substituteEnv` stripped every `${...}` token
from the value on any substitution failure. That dropped not only the
unresolved placeholder but also any resolvable ones (e.g. mixed values
like `pre-${SET}-mid-${UNSET}-post`) and `${VAR:-default}` fallbacks
sitting elsewhere in the same string.

Replace the global regex with a per-placeholder callback that mirrors
`@pnpm/config.env-replace`'s `getEnvValue` logic — including the
`${VAR-default}` / `${VAR:-default}` semantics and the source-backslash
escape handling — but treats undefined as `""` instead of throwing. Only
the genuinely unresolved bare placeholders get dropped to empty; the rest
of the value is preserved.

Adds a regression test covering the mixed case the original implementation
silently corrupted.

Addresses CodeRabbit / Copilot review feedback on #11526.

---
Written by an agent (Claude Code, claude-opus-4-7).
Mirror the upstream pnpm fix in `loadNpmrcFiles.ts`: when a `${VAR}`
placeholder in an `.npmrc` value has no resolution and no
`${VAR:-default}` fallback, substitute `""` instead of erroring or
keeping the literal. Resolvable placeholders and defaults elsewhere
in the same string still expand normally.

`env_replace` is replaced with `env_replace_lossy`, which returns
`(String, Vec<String>)` — the substituted text plus the unresolved
placeholders (used by `NpmrcAuth::from_ini` to push warnings, one per
occurrence). The strict `env_replace` and `EnvReplaceError` types are
removed since there are no callers; the lossy variant covers both
the production path and the test suite.

Updates the two `npmrc_auth` tests that asserted "raw value/key kept
verbatim on substitution failure" to assert the new lossy semantics,
and adds a regression test for the mixed-resolvable-and-unresolved case.

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

codecov-commenter commented May 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.12%. Comparing base (4ababc0) to head (9b14ac4).

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #11526   +/-   ##
=======================================
  Coverage   89.12%   89.12%           
=======================================
  Files         127      127           
  Lines       14458    14470   +12     
=======================================
+ Hits        12885    12896   +11     
- Misses       1573     1574    +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.9±0.09ms   551.2 KB/sec    1.00      7.8±0.21ms   553.5 KB/sec

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

Actionable comments posted: 1

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

Inline comments:
In `@config/reader/src/loadNpmrcFiles.ts`:
- Around line 181-187: resolveEnvValue currently treats a present property with
value undefined incorrectly; update it so undefined counts as "unset" and
applies the fallback, and only apply the empty-string fallback when the colon is
present. In function resolveEnvValue, after extracting varName/colon/fallback
and confirming the property exists, read const v = env[varName]; if v ===
undefined return fallback; if (colon && v === '') return fallback; otherwise
return v. This preserves handling for ${VAR-default} (unset → fallback) and
${VAR:-default} (unset or empty → fallback).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b7f468d0-9466-4cfe-9846-edf9419c8f8d

📥 Commits

Reviewing files that changed from the base of the PR and between de143ee and 433a19d.

📒 Files selected for processing (4)
  • .changeset/oidc-unresolved-env-placeholder.md
  • config/reader/src/loadNpmrcFiles.ts
  • config/reader/test/index.ts
  • config/reader/test/parseCreds.test.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/oidc-unresolved-env-placeholder.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • config/reader/test/parseCreds.test.ts
  • config/reader/test/index.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Standard Style with modifications: trailing commas are used, functions are preferred over classes, functions are declared after they are used (hoisting is relied upon).
Functions should have no more than two or three arguments. If a function needs more parameters, use a single options object instead.
Maintain import order: (1) Standard libraries, (2) External dependencies (sorted alphabetically), (3) Relative imports.

Files:

  • config/reader/src/loadNpmrcFiles.ts
🧠 Learnings (1)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • config/reader/src/loadNpmrcFiles.ts
🪛 OpenGrep (1.20.0)
config/reader/src/loadNpmrcFiles.ts

[ERROR] 182-182: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

Comment thread config/reader/src/loadNpmrcFiles.ts Outdated
@github-actions

github-actions Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.596 ± 0.053 2.511 2.694 1.03 ± 0.02
pacquet@main 2.523 ± 0.031 2.486 2.575 1.00
pnpm 4.904 ± 0.042 4.831 4.977 1.94 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.5962799787999997,
      "stddev": 0.05262147057391274,
      "median": 2.6069530535,
      "user": 2.7281546800000003,
      "system": 3.8912829,
      "min": 2.5111698154999997,
      "max": 2.6943282275,
      "times": [
        2.5910096595,
        2.6943282275,
        2.5111698154999997,
        2.6244082875,
        2.6241638875,
        2.5315512315,
        2.6114917204999997,
        2.6161533814999998,
        2.5561091905,
        2.6024143865
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.5232863092,
      "stddev": 0.03146990991376137,
      "median": 2.520472798,
      "user": 2.7272911800000004,
      "system": 3.9194772999999996,
      "min": 2.4858337075,
      "max": 2.5754146605,
      "times": [
        2.4858337075,
        2.5754146605,
        2.4860982335,
        2.4982227934999996,
        2.5718273665,
        2.5253890755,
        2.5106330834999997,
        2.5178273145,
        2.5231182815,
        2.5384985755
      ]
    },
    {
      "command": "pnpm",
      "mean": 4.9039411587,
      "stddev": 0.042427338567906654,
      "median": 4.9085031585,
      "user": 8.336317480000002,
      "system": 4.297350100000001,
      "min": 4.8309618285,
      "max": 4.9767835485,
      "times": [
        4.896617275500001,
        4.8972234525000005,
        4.8497748435000005,
        4.9215334175,
        4.9767835485,
        4.9393158205,
        4.884682482500001,
        4.9227360535,
        4.9197828645,
        4.8309618285
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 771.0 ± 40.9 740.3 883.3 1.00
pacquet@main 811.2 ± 72.3 723.8 958.1 1.05 ± 0.11
pnpm 2595.6 ± 125.3 2502.1 2936.8 3.37 ± 0.24
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.77096379092,
      "stddev": 0.040874453761418385,
      "median": 0.75869038992,
      "user": 0.39511521999999993,
      "system": 1.6769947600000001,
      "min": 0.7403412099200001,
      "max": 0.8832595579200001,
      "times": [
        0.8832595579200001,
        0.74643312092,
        0.75488519292,
        0.7633110549200001,
        0.77043809092,
        0.7592045849200001,
        0.75600735592,
        0.7403412099200001,
        0.7775815459200001,
        0.75817619492
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.8111982705199999,
      "stddev": 0.07227193162303999,
      "median": 0.7857245744200001,
      "user": 0.38214462,
      "system": 1.6619002600000001,
      "min": 0.72379961492,
      "max": 0.9580896559200001,
      "times": [
        0.9580896559200001,
        0.8853933579200001,
        0.78543763292,
        0.7753318479200001,
        0.75709379192,
        0.87837772392,
        0.79650247592,
        0.76594508792,
        0.72379961492,
        0.7860115159200001
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.5956000828200003,
      "stddev": 0.1253323778190843,
      "median": 2.5607208314200003,
      "user": 3.33963002,
      "system": 2.2480352599999995,
      "min": 2.5021420179200002,
      "max": 2.93677706892,
      "times": [
        2.5930194179200003,
        2.60638383792,
        2.93677706892,
        2.60485303092,
        2.5021420179200002,
        2.5462593259200004,
        2.5369322389200004,
        2.55441811192,
        2.5081922269200003,
        2.56702355092
      ]
    }
  ]
}

Callers that construct the env object directly (notably tests) commonly
include `{ KEY: undefined }` to model an unset variable — the
`Record<string, string | undefined>` signature explicitly allows this.
Using `hasOwnProperty` to decide whether to apply a `${VAR-default}`
fallback couldn't tell those two cases apart and incorrectly returned
`undefined` (→ `""` to the caller) instead of the fallback.

Check `env[varName] === undefined` after the lookup so explicit-undefined
values fall through to the fallback the same way an absent key would.

Diverges from `@pnpm/config.env-replace`'s strict path, which uses
`hasOwnProperty` — but the strict path consumes `process.env`, which
never holds `undefined` values, so the divergence is invisible in
practice. The lossy path here was already deliberately diverging
(`""` vs throw on unresolved), so handling `undefined` consistently
with the contract is a natural extension.

Addresses CodeRabbit review feedback on #11526.

---
Written by an agent (Claude Code, claude-opus-4-7).
Copilot AI review requested due to automatic review settings May 15, 2026 11:36

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

….1.0

The package now ships a `envReplaceLossy` variant that returns
`{ value, unresolved }` — the substituted text plus a list of placeholders
that had no value and no default. That's exactly the behavior the local
`substituteEnv` fallback was reproducing, including:
  - per-placeholder substitution (preserves resolvable placeholders
    and `${VAR-default}` / `${VAR:-default}` fallbacks alongside
    unresolved bare ones),
  - treating `{ KEY: undefined }` the same as a missing key.

Drop the inline regex + `resolveEnvValue` helper and call `envReplaceLossy`,
pushing one warning per unresolved placeholder. Bumps the
`@pnpm/config.env-replace` catalog entry from ^3.0.2 to ^4.1.0.

---
Written by an agent (Claude Code, claude-opus-4-7).
@zkochan zkochan merged commit a62f959 into main May 15, 2026
27 of 28 checks passed
@zkochan zkochan deleted the fix/11513 branch May 15, 2026 14:25
zkochan added a commit that referenced this pull request May 15, 2026
Two pre-existing perfectionist findings landed via #11526 and broke
the Dylint CI job on main:

- `perfectionist::macro-trailing-comma` flagged a trailing comma in a
  single-line `assert_eq!(...)` that `cargo fmt` had collapsed from a
  multi-line form without dropping the comma. Removed the comma.
- `perfectionist::unicode_ellipsis_in_comments` warned about a U+2026
  `…` character in a test comment. CI runs with `RUSTFLAGS=-D warnings`,
  so the warning fails the build. Replaced with ASCII `...`.

Verified locally with `cargo dylint --all -- --all-targets --workspace`
under `RUSTFLAGS=-D warnings`; passes clean.

---
Written by an agent (Claude Code, claude-opus-4-7).
github-actions Bot pushed a commit to Eyalm321/pnpm that referenced this pull request May 18, 2026
…ues (pnpm#11526)

Closes pnpm#11513.

`actions/setup-node` writes `_authToken=${NODE_AUTH_TOKEN}` to `.npmrc`. When the user relies on OIDC trusted publishing without setting `NODE_AUTH_TOKEN`, pnpm previously passed the literal placeholder through verbatim — so any time OIDC fallback failed, pnpm sent `Authorization: Bearer ${NODE_AUTH_TOKEN}` to the registry and the publish came back as a 404.

This worked in v10 because `pnpm publish` shelled out to `npm publish`, whose own OIDC flow handled the case.

The fix lives in `@pnpm/config.env-replace@4.1.0`, which adds an `envReplaceLossy` variant that returns `{ value, unresolved }` instead of throwing. Unresolved `${VAR}` placeholders become `''` and are reported back as a list — leaving OIDC trusted publishing as the sole auth source. Resolvable placeholders and `${VAR-default}` / `${VAR:-default}` fallbacks elsewhere in the same string still expand normally, so a value like `pre-${SET}-mid-${UNSET}-${OTHER-default}-post` now produces `pre-AAA-mid--default-post` rather than dropping every placeholder.

Also treats `{ KEY: undefined }` in the env object the same as a missing key (the `Record<string, string | undefined>` contract), so a `${KEY-default}` reaches the fallback in that case.

### Changes

- `@pnpm/config.env-replace` catalog bumped from `^3.0.2` → `^4.1.0` (`pnpm-workspace.yaml`, `pnpm-lock.yaml`)
- `config/reader/src/loadNpmrcFiles.ts` — `substituteEnv` now calls `envReplaceLossy` and pushes one warning per unresolved placeholder
- `config/reader/test/index.ts` + `parseCreds.test.ts` — regression tests covering the OIDC case, mixed resolvable/unresolved placeholders, explicit-undefined env values, and `parseCreds({ authToken: '' })`
- `.changeset/oidc-unresolved-env-placeholder.md` — patch bump for `@pnpm/config.reader` and `pnpm`
- `pacquet/crates/config/{env_replace.rs, npmrc_auth.rs, npmrc_auth/tests.rs}` — mirrors the lossy semantics in pacquet's local `env_replace_lossy`, with matching test coverage
github-actions Bot pushed a commit to Eyalm321/pnpm that referenced this pull request May 18, 2026
Two pre-existing perfectionist findings landed via pnpm#11526 and broke
the Dylint CI job on main:

- `perfectionist::macro-trailing-comma` flagged a trailing comma in a
  single-line `assert_eq!(...)` that `cargo fmt` had collapsed from a
  multi-line form without dropping the comma. Removed the comma.
- `perfectionist::unicode_ellipsis_in_comments` warned about a U+2026
  `…` character in a test comment. CI runs with `RUSTFLAGS=-D warnings`,
  so the warning fails the build. Replaced with ASCII `...`.

Verified locally with `cargo dylint --all -- --all-targets --workspace`
under `RUSTFLAGS=-D warnings`; passes clean.

---
Written by an agent (Claude Code, claude-opus-4-7).
wanseob added a commit to ggui-ai/ggui that referenced this pull request May 24, 2026
…cause of rc.2 404s)

Root cause: actions/setup-node's `registry-url:` parameter writes
`//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}` to `.npmrc`.
When NODE_AUTH_TOKEN is unset (which it is, since we use OIDC Trusted
Publishing instead of a token), pnpm ≤9.x passes the literal
`${NODE_AUTH_TOKEN}` placeholder through verbatim — registry returns
404 (npm's masked auth-failure response) BEFORE OIDC fallback fires.

This is the bug pnpm/pnpm#11513 documents — fixed in pnpm 11.0.9+ via
PR pnpm/pnpm#11526 (drops unresolved ${VAR} placeholders to empty
string + warns). Not backported to 9.x. pnpm 10 works because
`pnpm publish` shelled out to `npm publish` whose own OIDC flow
handled the case; pnpm 9 + 11 implement OIDC natively and need the
fix.

The surgical workaround: don't have setup-node write the broken
`.npmrc` in the first place. `registry-url:` exists to authenticate
via NODE_AUTH_TOKEN — for OIDC-only publishing it's pure liability.
Dropping it means no placeholder for pnpm to mis-handle; OIDC's
ACTIONS_ID_TOKEN_REQUEST_TOKEN env path engages cleanly.

pnpm publish still targets registry.npmjs.org by default (workspace
+ scope config resolve there). Only loss is the auto-written `.npmrc`
auth line — which we explicitly don't want anyway.

Did NOT touch the other two registry-url uses in this file
(preflight's collision check + smoke's install) — those are
anonymous-OK operations that work fine with or without the broken
`.npmrc`. Will revisit only if smoke surfaces an issue.

Fixes the rc.2 publish failures observed across multiple retries
during this session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

pnpm publish on GitHub Trusted puslishing via oidc to npmjs fails on pnpm11

3 participants