chore: publish to npm via OIDC trusted publishing - #1457
Merged
Conversation
The NPM_TOKEN repo secret expired at npm's 90-day cap on granular write tokens and silently failed two consecutive releases, leaving core 0.7.52, core 0.7.53, server 0.8.66 and cli 0.10.57 merged to main but absent from the registry. It failed as a 404 on the publish PUT rather than as an auth error, which is why nothing looked like a credential problem. Rotating it would buy 90 days. npm removes direct publishing for bypass-2FA tokens around January 2027, leaving only OIDC or a staged publish a human approves with 2FA, and GitHub has said no CI/CD exemption is planned. So the token goes away entirely: the job declares id-token: write, and npm exchanges it for a short-lived workflow-scoped credential per run. Both npm-publishing steps drop NODE_AUTH_TOKEN. The wrapper lockstep step is the easily-missed second consumer of the secret. The GitHub Packages step keeps GITHUB_TOKEN, which is a different registry and auto-provisioned. scripts/publish-npm.js needs no change, since npm resolves OIDC before it would fall back to a token. Closes #1456
The four versions stranded by the expired NPM_TOKEN had no publish path at all. Re-running the two failed runs cannot work: a re-run replays the workflow file from its ORIGINAL commit, so neither run would see the OIDC change, and once the secret is deleted they would have no auth either. Merging a fix does not re-trigger anything, since the workflow fires only on a push touching changelog/**. So the recovery path becomes an explicit dispatch input naming the changelog files to publish. Paths are validated before anything publishes, because a typo that silently published nothing would look exactly like success, and the set is sorted by its date: frontmatter ASC like the push path, so core still publishes before server and npm's latest tag lands on the newest version rather than whichever path was typed last.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1456
Summary
Publishes to npm through trusted publishing (OIDC) instead of a stored
NPM_TOKEN, so the release pipeline has no credential to rotate and nothing that expires.The secret hit npm's 90-day cap on granular write tokens (set 2026-05-21, last good publish 2026-08-16, first failure 2026-08-20) and silently failed two consecutive releases. Four versions are merged to
mainwith changelogs but absent from the registry:core@0.7.52,core@0.7.53,server@0.8.66,cli@0.10.57. It failed as a 404 on the publish PUT rather than as an auth error, which is why nothing looked like a credential problem until the dates were lined up.Rotating would buy another 90 days. npm removes direct publishing for bypass-2FA tokens around January 2027, leaving only OIDC or a staged publish that a human approves with 2FA, and GitHub has said no CI/CD exemption is planned.
What changed
permissions:gainsid-token: write, which is what the OIDC exchange needs.Publish to npmandLockstep-publish wrappers to match @webjsdev/cliboth dropNODE_AUTH_TOKEN. The wrapper step is the easily-missed second consumer of the secret.Publish to GitHub PackageskeepsGITHUB_TOKEN. Different registry, auto-provisioned, unaffected.registry-urlis load-bearing rather than token decoration, and npm must stay on the bundled 11.19.x.framework-dev.md's release section documents OIDC instead of the secret, and the recovery path below.republish_pathsdispatch input, because the four stranded versions otherwise have no publish path at all (see Recovery).scripts/publish-npm.jsneeds no change. Its auth comment already says it relies on standardnpm publishtoken resolution, and npm resolves OIDC before it would fall back to a token.Deliberately excluded
Do not upgrade npm to
latestin CI. Trusted publishing needs npm >= 11.5.1 and Node 24 bundles 11.19.x, so the floor is already cleared. npmlatestis 12.0.2, and npm 12 enables install-time security defaults (allowScriptsoff) that block esbuild's postinstall, sopackages/core/distwould never build and both the release and the e2e/Bun suites would fail with an error pointing nowhere near install policy. That is a separate piece of work with its own clock.Recovery: why re-running the failed runs cannot work
The obvious plan was to merge this and re-run the two failed runs. That does not work, and the reason is easy to miss: a re-run replays the workflow file from its ORIGINAL commit. Those runs sit at
2fdbfd44and844f9403, neither of which hasid-token: write, so they would fail again, and onceNPM_TOKENis deleted they would have no auth at all. Merging this PR does not re-trigger anything either, since the workflow fires only on a push touchingchangelog/**.So the four stranded versions had no publish path, and this PR adds one: a
republish_pathsdispatch input naming the changelog files to publish. Paths are validated before anything publishes, because a typo that silently published nothing would look exactly like success. The set is then sorted by itsdate:frontmatter ASC exactly as the push path does, so core still publishes before server and npm'slatestlands on the newest version rather than on whichever path was typed last. Every publish script is already idempotent, so naming an already-published version is a no-op.Test plan
There is no test layer for a workflow file, so verification is the live run:
permissionsreads{contents: write, packages: write, id-token: write}secrets.NPM_TOKENreference remains in the workflowcore 0.7.52, core 0.7.53, server 0.8.66, cli 0.10.57republish_pathsset to the four stranded files publishes them over OIDC with no token presentnpm view @webjsdev/core versionreports0.7.53, provinglatestlanded on the newer version rather than 0.7.52NPM_TOKENrepo secret once the above is greenRisk
There are reports of the OIDC token exchange failing for packages nested below the top level of a monorepo. The exposure here is
packages/editors/intellisenseand the twopackages/wrappers/*. The documented mitigation is a correctrepository.urlplusrepository.directoryin each package.json, and all 8 already carry both, so this should hold. If exactly one package fails while the rest succeed, that is the cause, and the fallback is staged publishing for that package rather than reverting.Docs surfaces
framework-dev.md: updated, release section.