Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
49 changes: 49 additions & 0 deletions scripts/check-no-npm-auth.mjs
Original file line number Diff line number Diff line change
@@ -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`);
Loading