Skip to content

ci(release): drop the inherited npm_config_globalconfig, so the nested pnpm can't hand npm a self-colliding config - #10148

Merged
os-elon merged 2 commits into
mainfrom
claude/release-failure-83i4qc
Aug 20, 2026
Merged

ci(release): drop the inherited npm_config_globalconfig, so the nested pnpm can't hand npm a self-colliding config#10148
os-elon merged 2 commits into
mainfrom
claude/release-failure-83i4qc

Conversation

@os-elon

@os-elonos-elon commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes#10146

What broke

17.1.0 failed to publish after a fully green build (run 32355381481), with an error carrying no npm error code at all:

Received an unexpected error for @objectstack/account: (no code)
Exit prior to config file resolving
cause
double-loading config "/home/runner/.config/pnpm/rc" as "global", previously loaded as "user"

npm refuses to start when its "user" and "global" config resolve to the same file — it aborts inside @npmcli/config before it parses argv, which is why changesets could only report (no code). @objectstack/account is just the first package to report; every package fails identically. Nothing was published — the run died before the first registry write, and @objectstack/cli@17.1.0 is still 404 on the registry.

Why that file arrives twice

  1. pnpm run release exports npm_config_globalconfig=~/.config/pnpm/rc to every child; pnpm forces {globalconfig: join(configDir, 'rc')} into its rawConfig unconditionally.
  2. changeset publish v3 detects the pnpm workspace and shells out to pnpm info / pnpm publish per package, where v2 always shelled out to npm. The publish therefore runs a nested pnpm underneath pnpm run.
  3. That nested pnpm reads the inherited value back in and, delegating info to npm, hands the npm child bothnpm_config_userconfigandnpm_config_globalconfig pointing at that one file (pnpm/pnpm#10914, unfixed on the 10.31.0 line we pin).

Only layer 2 is new — which is why 2026-08-14's release was green on the same pnpm pin. It ran @changesets/cli@^2.31.1, whose npm-only spawn saw userconfig=$HOME/.npmrc against globalconfig=~/.config/pnpm/rc: two files, no collision. The bump to ^3.0.0 routed the publish through a nested pnpm.

The change

One line in scripts/release-publish.sh, ahead of changeset publish:

unset npm_config_globalconfig NPM_CONFIG_GLOBALCONFIG

plus the comment block explaining why it cannot be simplified away. pnpm recomputes its own global config path from configDir either way, so nothing pnpm needs is lost, and npm falls back to its own default global config while keeping $HOME/.npmrc — where release.yml writes NPM_TOKEN — as the user config. pnpm publish's npm child was already passed only globalconfig and never userconfig, so auth resolution is unchanged.

Verification

Reproduced and fixed in the release lane's exact shape on the pinned pnpm 10.31.0 (pnpm run → bash script → nested pnpm):

  • negative control (today's script): reproduces the CI error verbatim, exit 1.
  • with the unset: the nested call returns registry JSON, exit 0.
  • auth path: with the unset, pnpm publish's npm child is handed neither userconfig nor globalconfig, so npm resolves $HOME/.npmrc as its user config — the file the workflow writes the token into.

Two alternatives were tested and rejected: forcing npm_config_userconfig=$HOME/.npmrc does not work (the nested pnpm recomputes and overrides it), and pinning a newer pnpm is a repo-wide package-manager move for a bug with no known fixed version on this line.

Notes

  • Pure bug fix in a CI script — no changeset, skip-changeset applied per the gate's route 2 (nothing here releases).
  • Touches no governed surface (scripts/** is not in the GOVERNED_SURFACES register).
  • Merging this does not re-publish anything. The push lane can only run version-pr and release-integrity; the publish job is workflow_dispatch-only behind environment: release. Re-dispatching release.yml for 17.1.0 is the maintainer's act (Prime Directive Add missing Field.phone() helper and factory methods for Action/Dashboard/Report #15).

…d pnpm can't hand npm a self-colliding config
17.5.0 failed to publish after a fully green build (run 32355381481) with an
error carrying no npm error code at all:
Received an unexpected error for @objectstack/account: (no code)
Exit prior to config file resolving
cause
double-loading config "/home/runner/.config/pnpm/rc" as "global", previously loaded as "user"
npm refuses to start when its "user" and "global" config resolve to the SAME
file — it aborts inside @npmcli/config before it parses argv, which is why
changesets could only report "(no code)". Three layers stack up to produce that
one file twice:
1. `pnpm run release` exports npm_config_globalconfig=~/.config/pnpm/rc to
every child; pnpm forces `{globalconfig: join(configDir, 'rc')}` into its
rawConfig unconditionally.
2. `changeset publish` v3 detects the pnpm workspace and shells out to
`pnpm info` / `pnpm publish` per package, where v2 always shelled out to
`npm`. The publish therefore runs a NESTED pnpm underneath `pnpm run`.
3. That nested pnpm reads the inherited value back in and, delegating `info`
to npm, hands the npm child BOTH npm_config_userconfig AND
npm_config_globalconfig pointing at that file (pnpm/pnpm#10914, unfixed on
the 10.31.0 line we pin).
Only layer 2 is new, which is why 2026-08-14's release was green on the same
pnpm pin: @changesets/cli@^2.31.1 spawned npm directly, so npm saw
userconfig=$HOME/.npmrc against globalconfig=~/.config/pnpm/rc — two files, no
collision. The bump to ^3.0.0 routed the publish through a nested pnpm.
Dropping the inherited value is the whole fix. pnpm recomputes its own global
config path from configDir either way, so nothing pnpm needs is lost, and npm
falls back to its own default global config while keeping $HOME/.npmrc — where
release.yml writes NPM_TOKEN — as the user config. `pnpm publish`'s npm child
was already passed only globalconfig and never userconfig, so auth resolution is
unchanged.
Verified in the release lane's exact shape on the pinned pnpm 10.31.0
(`pnpm run` -> bash script -> nested pnpm): the negative control reproduces the
CI error verbatim, and with the unset the nested call returns registry JSON.
Forcing npm_config_userconfig=$HOME/.npmrc instead was tested and does NOT work
— the nested pnpm recomputes it and overrides whatever we export.
Fixes#10146
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRPcZzc7rfBQsNxJw3BSRh
@os-elonos-elon added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 20, 2026 — with Claude
… 17.1.0, not 17.5.0
17.5.0 in that run's log is `@object-ui/console@17.5.0`, the vendored objectui
build, not this repo's version. packages/cli/package.json at the run's SHA
(47d1ae8, main's tip) declares 17.1.0, and the publish job's guard only proceeds
when the dispatched input equals that string — so 17.1.0 is what failed, and
17.1.0 is what the re-dispatch publishes. npm still has latest 17.0.0 and 404s
on 17.1.0, confirming the failed run wrote nothing.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRPcZzc7rfBQsNxJw3BSRh
@os-elon
os-elon added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit 2d3860dAug 20, 2026
22 checks passed
@os-elon
os-elon deleted the claude/release-failure-83i4qc branch August 20, 2026 11:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Release publish dies before the registry: nested pnpm hands npm a self-colliding config (userconfig == globalconfig)

1 participant

@os-elon