Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
📜 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)
🧰 Additional context used📓 Path-based instructions (1)**/*.{ts,tsx,js,jsx}📄 CodeRabbit inference engine (AGENTS.md)
Files:
🧠 Learnings (1)📚 Learning: 2026-05-14T09:04:00.133ZApplied to files:
🔇 Additional comments (2)
📝 WalkthroughWalkthroughThis PR treats unresolved unescaped ChangesOIDC Unresolved Env Placeholder Fix
Sequence DiagramsequenceDiagram
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
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)
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. Comment |
There was a problem hiding this comment.
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
.npmrcenv substitution handling so unresolved${VAR}placeholders are dropped (become empty) when substitution fails. - Add regression tests covering unset vs set
NODE_AUTH_TOKEN, andparseCreds({ authToken: '' })behavior. - Add a changeset for patch releases of
@pnpm/config.readerandpnpm.
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
.changeset/oidc-unresolved-env-placeholder.mdconfig/reader/src/loadNpmrcFiles.tsconfig/reader/test/index.tsconfig/reader/test/parseCreds.test.ts
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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
Micro-Benchmark ResultsLinux |
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
.changeset/oidc-unresolved-env-placeholder.mdconfig/reader/src/loadNpmrcFiles.tsconfig/reader/test/index.tsconfig/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)
Integrated-Benchmark Report (Linux)Scenario: Frozen Lockfile
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)
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).
….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).
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).
…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
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).
…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>
Summary
Closes #11513.
actions/setup-nodewrites_authToken=${NODE_AUTH_TOKEN}to.npmrc. When the user relies on OIDC trusted publishing without settingNODE_AUTH_TOKEN, pnpm previously passed the literal placeholder through verbatim — so any time OIDC fallback failed, pnpm sentAuthorization: Bearer ${NODE_AUTH_TOKEN}to the registry and the publish came back as a 404.This worked in v10 because
pnpm publishshelled out tonpm publish, whose own OIDC flow handled the case.The fix lives in
@pnpm/config.env-replace@4.1.0, which adds anenvReplaceLossyvariant 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 likepre-${SET}-mid-${UNSET}-${OTHER-default}-postnow producespre-AAA-mid--default-postrather than dropping every placeholder.Also treats
{ KEY: undefined }in the env object the same as a missing key (theRecord<string, string | undefined>contract), so a${KEY-default}reaches the fallback in that case.Changes
@pnpm/config.env-replacecatalog bumped from^3.0.2→^4.1.0(pnpm-workspace.yaml,pnpm-lock.yaml)config/reader/src/loadNpmrcFiles.ts—substituteEnvnow callsenvReplaceLossyand pushes one warning per unresolved placeholderconfig/reader/test/index.ts+parseCreds.test.ts— regression tests covering the OIDC case, mixed resolvable/unresolved placeholders, explicit-undefined env values, andparseCreds({ authToken: '' }).changeset/oidc-unresolved-env-placeholder.md— patch bump for@pnpm/config.readerandpnpmpacquet/crates/config/{env_replace.rs, npmrc_auth.rs, npmrc_auth/tests.rs}— mirrors the lossy semantics in pacquet's localenv_replace_lossy, with matching test coverageTest plan
${VAR}drops to''with a warning; resolvable +-defaultplaceholders still expand in the same value; explicit{ KEY: undefined }falls through to-default;parseCreds({ authToken: '' })returns undefined; normal substitution still workspnpm --filter @pnpm/config.reader testpasses the new tests; the 6 unrelated pre-existing failures intest/index.tsare present on the base branch toopnpm --filter pnpm run compilebuilds the bundle successfullycargo nextest run -p pacquet-config— all 154 pacquet-config tests pass;cargo clippy --deny warnings+cargo fmt --checkcleanWritten by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
Bug Fixes
Tests
Chores