From 91b81541ff50d5c3e6017aaa0fcb946e6afa4cfd Mon Sep 17 00:00:00 2001 From: Softov Date: Mon, 24 Aug 2026 17:50:11 -0400 Subject: [PATCH] fix: a credential in .npmrc is what stops OIDC, and setup-node writes one `registry-url` makes actions/setup-node write `_authToken=${NODE_AUTH_TOKEN}` into an .npmrc. With no token to substitute, the line does not disappear - it becomes an empty credential, and npm reads any `_authToken` line as auth being configured. So it never performs the OIDC exchange, and fails as ENEEDAUTH or a 404: a configuration problem wearing a permissions problem's error message (actions/setup-node#1551). The rehearsal is what showed it - `NODE_AUTH_TOKEN: XXXXX-XXXXX-XXXXX-XXXXX` in the environment of a workflow that has no token and wants none. `registry-url` is gone; npmjs.org is the default anyway. check-no-npm-auth.mjs runs before the publish and fails on a credential in any .npmrc npm would read, or on NODE_AUTH_TOKEN being set at all, so the next reintroduction fails loudly instead of at the registry. --- .github/workflows/release.yml | 14 +++++++++- CHANGELOG.md | 9 +++++++ RELEASING.md | 17 ++++++++++++ package.json | 1 + scripts/check-no-npm-auth.mjs | 49 +++++++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 scripts/check-no-npm-auth.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99b3701..3678b06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,11 +24,17 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 + + # Deliberately no `registry-url`. It makes setup-node write + # `_authToken=${NODE_AUTH_TOKEN}` into an .npmrc, and with no token to + # substitute that line becomes an empty credential rather than no + # credential. npm reads it as "auth is already configured", skips the + # OIDC exchange entirely and fails with ENEEDAUTH or a 404 - see + # actions/setup-node#1551. The default registry is npmjs.org anyway. - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - registry-url: https://registry.npmjs.org # Trusted publishing needs npm >= 11.5.1, which is newer than the npm # bundled with any Node 22. @@ -37,6 +43,12 @@ jobs: npm install -g npm@latest npm --version + # Belt and braces: if anything upstream reintroduces an auth line, the + # OIDC exchange is silently skipped and the failure looks like a + # permissions problem rather than a configuration one. + - name: Refuse a stale credential + run: node scripts/check-no-npm-auth.mjs + - run: pnpm install --frozen-lockfile # The same gate CI runs. A tag is not a reason to publish something that diff --git a/CHANGELOG.md b/CHANGELOG.md index cd5bc39..1eb62f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,15 @@ And `pnpm publish` has no OIDC support, while `npm publish` cannot read `workspace:^`; `scripts/release-publish.mjs` resolves the ranges and publishes each package from its own directory in dependency order. +### The OIDC exchange only happens with no credential configured + +`actions/setup-node` writes `_authToken=${NODE_AUTH_TOKEN}` into an `.npmrc` +whenever it is given a `registry-url`. With nothing to substitute, that is an +empty credential rather than no credential, and npm treats any `_authToken` +line as auth already being configured - so it never asks GitHub for a token. +The release workflow no longer sets `registry-url`, and +`scripts/check-no-npm-auth.mjs` fails the run if a credential appears anyway. + ### The facade is `@textui/kit`, not `textui` npm refused the unscoped name: "Package name too similar to existing package diff --git a/RELEASING.md b/RELEASING.md index 4a2ccb2..30b839b 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -80,6 +80,20 @@ without argument. [npm-bootstrap]: https://github.com/npm/cli/issues/8544 +## The `registry-url` trap + +`actions/setup-node` writes `_authToken=${NODE_AUTH_TOKEN}` into an `.npmrc` +whenever it is given a `registry-url`. With no token to substitute, that line +becomes an *empty credential* rather than no credential - and npm reads any +`_authToken` line as "auth is configured", so it never performs the OIDC +exchange and fails as `ENEEDAUTH` or a 404 ([setup-node#1551][sn1551]). + +The failure looks like a permissions problem and is not one, which is why +`release.yml` sets no `registry-url` and `scripts/check-no-npm-auth.mjs` runs +before the publish. npmjs.org is the default registry regardless. + +[sn1551]: https://github.com/actions/setup-node/issues/1551 + ## Cutting one 1. Land everything. `main` green. @@ -126,3 +140,6 @@ unless given `--force`. - **`scripts/release-publish.mjs`** - refuses a set at mixed versions, refuses a dependency cycle, and refuses to publish anything still carrying a `workspace:` range. +- **`scripts/check-no-npm-auth.mjs`** - refuses to publish while any npm + credential is configured, because a credential is what stops the OIDC + exchange happening at all. Runs in the release workflow, before the publish. diff --git a/package.json b/package.json index 141fe72..d4e9542 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "check:exports": "node scripts/check-exports.mjs", + "check:npm-auth": "node scripts/check-no-npm-auth.mjs", "check:version": "node scripts/check-version.mjs", "release:dry": "node scripts/release-publish.mjs --dry-run", "clean": "rm -rf packages/*/dist packages/*/*.tsbuildinfo playground/.dev examples/*/.dev packages/textide/.dev", diff --git a/scripts/check-no-npm-auth.mjs b/scripts/check-no-npm-auth.mjs new file mode 100644 index 0000000..91fce9c --- /dev/null +++ b/scripts/check-no-npm-auth.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +/** + * Refuse to publish with a credential line in any .npmrc. + * + * Trusted publishing works by npm noticing it has *no* credentials and doing + * an OIDC exchange instead. An `_authToken=` line defeats that even when the + * value is empty: npm reads the line as "auth is configured", never asks + * GitHub for a token, and fails as ENEEDAUTH or a 404 - which reads like a + * permissions problem and is not one. + * + * actions/setup-node writes exactly that line whenever it is given a + * `registry-url`, so this is one option away at all times. + */ +import { readFileSync, existsSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join, resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); + +const candidates = [ + process.env.NPM_CONFIG_USERCONFIG, + join(homedir(), '.npmrc'), + join(root, '.npmrc'), +].filter(Boolean); + +const AUTH = /^\s*(\/\/.*:)?(_authToken|_auth|_password|username)\s*=/; + +const problems = []; +for (const file of candidates) { + if (!existsSync(file)) continue; + const lines = readFileSync(file, 'utf8').split('\n'); + lines.forEach((line, i) => { + if (AUTH.test(line)) problems.push(`${file}:${i + 1}: ${line.split('=')[0]}=...`); + }); +} + +// The variable being set at all means something means to authenticate by +// token, including setup-node's own placeholder. +if (process.env.NODE_AUTH_TOKEN) problems.push('NODE_AUTH_TOKEN is set in the environment'); + +if (problems.length) { + console.error('a credential is configured; OIDC will be skipped:'); + for (const p of problems) console.error(` ${p}`); + console.error('\nremove the credential, or the registry-url that generates it.'); + process.exit(1); +} + +console.log(`no npm credential configured (${candidates.length} location(s) checked) - OIDC will be used`);