[ci] Bug Fix: Trusted-publishing follow-ups (TTY-aware setup script, unify nightly, drop NPM_TOKEN)#8597
Conversation
…'s own auth)
The pre-check resolved its own npm token by hand (NPM_TOKEN env, then
parsing ./.npmrc and ~/.npmrc, then `npm config get`) and made a raw
fetch() to /-/package/<name>/trust. After a browser-based `npm login`
on macOS that lookup comes up empty, so every package returned "unable
to check; will try anyway" with existing=[]. In --replace mode that
empty existing meant nothing got revoked, so `npm trust github` then
hit E409 against the still-present call-release.yml config for every
package — the migration never happened.
Replace the hand-rolled token + fetch with `npm trust list <pkg>
--json`, which uses npm's own auth resolution (the same path npm whoami
and npm trust github already use) and is a read, so no OTP. Parse
defensively: 404 / empty => no config, JSON error body or non-zero
exit with stderr => unknown, otherwise the returned config(s).
config matching reads both the flattened shape from `npm trust list
--json` ({file, repository, environment}) and the raw registry shape
({claims:{workflow_ref:{file}}}) via configFile/configRepo/
configEnvironment helpers, so conflicts are detected and their ids are
available for --replace to revoke. Removes the now-unused
fetchAuthToken / readNpmrcAuthTokens / escapedName helpers and warns
when any package couldn't be pre-checked.
https://claude.ai/code/session_01UEAeW4ZkfApZnrJTGc6Xm9
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Root cause of the persistent "unable to check" for every package:
`npm trust list` DOES require auth and may require OTP. npm only runs
its interactive web-auth/OTP recovery when both stdin AND stdout are
TTYs (see otplease in npm's source). Both the original raw-fetch
pre-check and the `npm trust list` rewrite ran under exec()/piped
stdio, so npm hit the OTP branch, couldn't recover, and threw — which
the pre-check swallowed into "unable to check". (Run by hand in a
terminal it works because both streams are TTYs and the cached "skip
2FA" session satisfies it instantly.)
Fixes:
- Add warmTrustAuth(): one fully-interactive `npm trust list` (stdio
inherited) before the per-package loop so npm can do web-auth and the
user can pick "skip 2FA for 5 minutes". The captured reads that
follow then succeed within the window.
- runNpm() gains captureStdout so the same spawn helper can both stream
the OTP URL (inherited) and capture stdout for JSON parsing.
- fetchTrustConfigs() returns {configs}|{error} instead of array|null,
and the caller prints the real error (e.g. the npm stderr summary)
instead of a bare "unable to check", so failures are never silent
again.
https://claude.ai/code/session_01UEAeW4ZkfApZnrJTGc6Xm9
…stConfigs Follow-up to the TTY/OTP fix — the JSDoc still claimed `npm trust list` never triggers OTP, which is exactly the wrong assumption that caused the bug. Document that it captures stdout and relies on warmTrustAuth having established the session. https://claude.ai/code/session_01UEAeW4ZkfApZnrJTGc6Xm9
…orkflow) npm enforces one trust configuration per package, so every publish has to come through the same caller workflow for OIDC to keep working. This was done before but was lost when the previous PR landed as a squash that kept nightly-release.yml — redoing it on top of main. Delete nightly-release.yml; fold its schedule trigger into pre-release.yml with a github.event_name branch that hardcodes the nightly behavior on the schedule path (increment-version=true, channel=nightly, ref=main, ignore-previously-published=true) and otherwise threads inputs through. The channel choice list adds 'nightly' for manual dispatch; the guard job's "increment-version + latest is forbidden" check is restricted to workflow_dispatch (the schedule path always uses nightly). https://claude.ai/code/session_01UEAeW4ZkfApZnrJTGc6Xm9
Trusted publishing is confirmed working, so the dual-auth scaffolding
is no longer needed. Removed:
- \`use-trusted-publishing\` workflow_dispatch input on
pre-release.yml and the matching input on call-release.yml
- \`secrets.NPM_TOKEN\` declaration on call-release.yml and the
passthrough from pre-release.yml
- \`Configure npm auth\` step in call-release.yml's publish job
(set NODE_AUTH_TOKEN from NPM_TOKEN, log which mode is active)
- \`Verify npm authentication\` step (the \`npm whoami\` preflight
only checked the persistent NPM_TOKEN; OIDC mints a per-call
token at publish time)
- Mentions of NPM_TOKEN / use-trusted-publishing in the
maintainers' guide and setup-trusted-publishing.mjs error
message
The publish job still grants \`id-token: write\` (for OIDC) and the
setup-node \`registry-url\` setting (writes the registry into .npmrc
so pnpm publish targets the right host). \`pnpm publish
--provenance\` does the rest.
Misconfigured trust now fails loudly at publish time instead of
silently falling through to token auth.
https://claude.ai/code/session_01UEAeW4ZkfApZnrJTGc6Xm9
Picks up #8593 (linear-time regexes / prototype-pollution guards / faster CSS parsing) and #8598 (TabNode.setTextContent Safari IME fix) — both touch lexical/extension/markdown/playground/yjs and don't intersect with the release tooling on this branch, so the merge is clean. https://claude.ai/code/session_01UEAeW4ZkfApZnrJTGc6Xm9
|
Review by Navi (potatowagon's AI assistant) This is a well-structured follow-up to #8587 that resolves real issues encountered during the first OIDC test publish. I reviewed the full 900-line diff across workflow YAML and the setup script. What I verified:
CI status: ALL GREEN (30+ jobs including all e2e matrices, core tests, integration tests, CodeQL). Verdict: Safe to approve. The changes are logically sound, CI confirms no regressions, and the test plan in the PR description is thorough (real E2E validation on the npm registry). |
potatowagon
left a comment
There was a problem hiding this comment.
Reviewed by Navi (Tater Thoughts Bobblehead) on behalf of @potatowagon.
LGTM ✅ — CI infrastructure cleanup: consolidates release workflows and fully adopts npm trusted publishing.
What this does:
- Removes the separate
nightly-release.ymlworkflow — merges nightly schedule intopre-release.yml(now the single entry point for all publishes). - Removes the
NPM_TOKENsecret anduse-trusted-publishinginput fromcall-release.yml— OIDC trusted publishing is now the ONLY auth method. - Removes the "Configure npm auth" and "Verify npm authentication" steps (no longer needed with OIDC).
- Adds
id-token: writepermission to the publish job (required for OIDC). - Adds
github.repository_owner == 'facebook'guard to prevent forks from running scheduled publishes.
What I checked:
- ✅ Logic correctness: npm trusted publishing via OIDC requires exactly: (a)
id-token: writepermission, (b) the workflow file path matching what's configured on npmjs.com. By consolidating to a single workflow file, all publishes share one OIDC identity — correct approach since npm allows only one trusted-publisher config per package. - ✅ Schedule preserved: The nightly
cron: '30 2 * * 1-5'schedule is preserved in the consolidated workflow. The schedule path correctly forceschannel=nightly,increment-version=true,ref=main,ignore-previously-published=true. - ✅ Guard logic correct: The "refuse increment+latest" guard now correctly gates on
github.event_name == 'workflow_dispatch'so scheduled runs don't hit it. - ✅ Fork protection:
github.repository_owner == 'facebook'in the release job prevents scheduled trigger from running on forks. - ✅ Backward compat: The
NPM_TOKENremoval only affects this workflow (CI infra). No library API changes. No www impact. - ✅ CI: All checks green (including CodeQL).
Safe to land. Clean consolidation of release infrastructure with proper OIDC adoption.
Description
Follow-up to #8587. The first real
dev-channel test publish under OIDC failed end-to-end because the helper script's pre-check was wrong, npm enforces a constraint I didn't account for, and the dual-auth scaffolding hid the actual failure mode. Three changes:1. Fix
setup-trusted-publishing.mjspre-check (scripts/npm/setup-trusted-publishing.mjs)pnpm run setup-trusted-publishing --setup-trust --replacewas supposed to detect each package's existingcall-release.ymltrust config, revoke it, and add the newpre-release.ymlone. Instead it reportedunable to checkfor every package, fell back to "try anyway", which skipped the revoke step, and returnedE409 Conflictfor all 31 packages.The pre-check shells out to
npm trust list <pkg> --jsonto read the existing config.npm trust listrequires auth and can require OTP — I had incorrectly documented it as read-only / no-OTP. npm only runs its interactive web-auth/OTP recovery when bothprocess.stdin.isTTYandprocess.stdout.isTTYare true. The pre-check was capturing stdout (necessary to parse JSON), which makes stdout a non-TTY, so npm fell through to throwingEOTPimmediately. The script'scatchthen collapsed every distinct failure into a single "unable to check" log line, hiding the real cause.Fix:
warmTrustAuth(): runs onenpm trust list <firstPkg>withstdio: 'inherit'(full TTY) before the per-package loop. Lets the user complete web-auth once and choose "Skip 2FA for the next 5 minutes"; subsequent captured reads then succeed inside the window because they hit the warmed session instead of a fresh OTP challenge.fetchTrustConfigsnow returns{configs}or{error}instead of an array-or-null, and the loop prints the real npm message (unable to check (401 Unauthorized …)) instead of a bare string.fetchAuthToken,readNpmrcAuthTokens,escapedName) that read~/.npmrcmanually — npm's own auth resolution is what works, and that's what's used now via the CLI.2. Unify the nightly schedule into
pre-release.ymlAfter running real test publishes I confirmed two npm constraints that change the workflow shape:
workflow_refclaim (the calling workflow), same as PyPI. The earlier setup pointed trust at the reusablecall-release.yml, which never appears in that claim, so every publish 404'd. The setup script now defaults--workflowtopre-release.yml.E409while the old one is present. The setup script's--replaceflag now revokes the existing non-matching config by id and adds the new one, so the migration fromcall-release.ymltopre-release.ymlis a single command instead of 30+ manual revoke/add pairs.To make the one-config rule actually workable, every publish has to come through the same caller workflow. Deleted
nightly-release.ymland folded itsschedule:trigger intopre-release.yml, branching ongithub.event_name:increment-version=true, channel=nightly, ref=main, ignore-previously-published=true)increment-version+latest" check is restricted toworkflow_dispatch(schedule is alwaysnightly)channelchoice list addsnightlyso a manual nightly retry is possible3. Drop
NPM_TOKENfallback and theuse-trusted-publishingtoggleNow that OIDC is confirmed working, the dual-auth scaffolding is dead weight that hides failures. Removed from
call-release.ymlandpre-release.yml:use-trusted-publishingworkflow_dispatch input (forced OIDC whenNPM_TOKENwas set)secrets.NPM_TOKENdeclaration oncall-release.ymland the passthrough frompre-release.ymlConfigure npm authstep (NODE_AUTH_TOKEN=$NPM_TOKENsetup, mode-detection logging)Verify npm authenticationstep (npm whoamipreflight — only meaningful for the persistent-token path; OIDC mints a per-call token at publish time)What stays:
permissions: id-token: write,actions/setup-nodewithregistry-url, andpnpm publish --provenance. A misconfigured trust setup now fails loudly at publish instead of silently falling through to token auth.Defaults review
Confirmed
pre-release.yml'sworkflow_dispatchdefaults still match step 4 of the Release Procedure ("publish to NPM after the version PR is merged to main"):refmainchannellatestincrement-versionversion.ymlalready bumped on the merged PRignore-previously-publishedSo a normal release run is "Actions → Publish to NPM → Run workflow → Run", no toggling.
Test plan
Before
pnpm run setup-trusted-publishing --setup-trust --replacereportedunable to check; will try anywayfor every package, skipped the revoke step, and gotE409on every add.dev-channel manual test publish from a PR branch produced 31E404 Not Found - PUT https://registry.npmjs.org/<pkg>errors after provenance signing succeeded — npm matched the OIDC token'sworkflow_ref(pre-release.yml) against a stored config that saidcall-release.yml.NPM_TOKENas a silent fallback, so a misconfigured OIDC setup wouldn't necessarily be visible.After
pnpm install --frozen-lockfileclean on pnpm 10.34.1pnpm run ci-check— green (tsc, tsc-scripts, tsc-extension, tsc-website, flow, prettier, lint)yaml.safe_load)scripts/npm/setup-trusted-publishing.mjs—node --checkclean; symbol audit shows zero orphan helpers from the old token-resolver pathpnpm run setup-trusted-publishing --setup-trust --replaceon a laptop withnpm whoamiworking:warmTrustAuthopens web-auth once; after ticking "Skip 2FA for 5 minutes" the per-package loop reportsCONFLICT: existing config(s) for [call-release.yml]for each, revokes them, and registerspre-release.yml. Subsequent runs reportalready configuredfor all.ref= PR branch,channel=dev,increment-version= ✓) — should publish0.X.Y-dev.0under thedevdist-tag via OIDC with provenance;latestuntouched.pre-release.yml's schedule path withevent_name == 'schedule'and produces the same0.X.Y-nightly.YYYYMMDDartifact as before.