diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index acafc1d..9422a51 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -99,9 +99,25 @@ jobs: - name: Verify the release tarball run: pnpm --filter @workspacejson/cli exec node ../../scripts/verify-package-tarball.mjs + # Both variables carry the same secret, because two different config + # layers each want their own name and the wrong one alone is silently + # useless: + # + # NODE_AUTH_TOKEN — the userconfig `setup-node` writes to $RUNNER_TEMP. + # NPM_TOKEN — the repository's own `.npmrc`, which reads + # `//registry.npmjs.org/:_authToken=${NPM_TOKEN}`. + # + # npm's project config outranks the userconfig, so the repo `.npmrc` wins + # for any command run from the repository root. With NPM_TOKEN unset it + # does not fall back to the userconfig token — it sends the *literal* + # string `Bearer ${NPM_TOKEN}`, which the registry rejects as 401. That is + # exactly how the 0.5.0 release failed, and it reads as a bad credential + # rather than a misconfigured workflow, which is what made it worth a + # comment this long. - name: Confirm the publish credential reached the runner env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: node scripts/verify-npm-publish-access.mjs # npm rather than pnpm, for two reasons: pnpm 9 exposes no --provenance @@ -119,6 +135,7 @@ jobs: working-directory: packages/cli env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npm publish --provenance --access public # A publish that npm accepts can still be unusable — wrong files, broken diff --git a/scripts/verify-npm-publish-access.mjs b/scripts/verify-npm-publish-access.mjs index eafca51..8e00dd6 100644 --- a/scripts/verify-npm-publish-access.mjs +++ b/scripts/verify-npm-publish-access.mjs @@ -2,6 +2,22 @@ import { spawnSync } from "node:child_process"; +// The repository `.npmrc` reads `//registry.npmjs.org/:_authToken=${NPM_TOKEN}`, +// and npm's project config outranks the userconfig `setup-node` writes. If +// NPM_TOKEN is missing npm does not fall back — it sends the literal string +// `Bearer ${NPM_TOKEN}` and the registry answers 401, which looks identical to a +// revoked or mistyped token. Checking the variable first turns half an hour of +// suspecting the credential into one accurate line. +const token = process.env.NPM_TOKEN; +if (!token || token.startsWith("${")) { + console.error( + token + ? `NPM_TOKEN is present but unexpanded (${JSON.stringify(token)}). The repository .npmrc interpolates it, so npm would send that literal text as the bearer token and the registry would reject it as 401.` + : "NPM_TOKEN is not set. The repository .npmrc interpolates it, so npm would send the literal text \"${NPM_TOKEN}\" as the bearer token and the registry would reject it as 401 — which reads as a bad credential rather than a missing one.", + ); + process.exit(1); +} + const username = JSON.parse(run("npm", ["whoami", "--json"])); // `npm access` reports package visibility or package enumeration; it does not