Skip to content

chore: publish to npm via OIDC trusted publishing - #1457

Merged
vivek7405 merged 2 commits into
mainfrom
chore/npm-oidc-trusted-publishing
Aug 21, 2026
Merged

chore: publish to npm via OIDC trusted publishing#1457
vivek7405 merged 2 commits into
mainfrom
chore/npm-oidc-trusted-publishing

Conversation

@vivek7405

@vivek7405 vivek7405 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

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 main with 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: gains id-token: write, which is what the OIDC exchange needs.
  • Publish to npm and Lockstep-publish wrappers to match @webjsdev/cli both drop NODE_AUTH_TOKEN. The wrapper step is the easily-missed second consumer of the secret.
  • Publish to GitHub Packages keeps GITHUB_TOKEN. Different registry, auto-provisioned, unaffected.
  • Comments record the three couplings that break every publish if changed carelessly: the workflow FILENAME is part of all 8 Trusted Publisher configurations, registry-url is 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.
  • A new republish_paths dispatch input, because the four stranded versions otherwise have no publish path at all (see Recovery).

scripts/publish-npm.js needs no change. Its auth comment already says it relies on standard npm publish token resolution, and npm resolves OIDC before it would fall back to a token.

Deliberately excluded

Do not upgrade npm to latest in CI. Trusted publishing needs npm >= 11.5.1 and Node 24 bundles 11.19.x, so the floor is already cleared. npm latest is 12.0.2, and npm 12 enables install-time security defaults (allowScripts off) that block esbuild's postinstall, so packages/core/dist would 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 2fdbfd44 and 844f9403, neither of which has id-token: write, so they would fail again, and once NPM_TOKEN is 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 touching changelog/**.

So the four stranded versions had no publish path, and this PR adds one: a republish_paths 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. The set is then sorted by its date: frontmatter ASC exactly as the push path does, so core still publishes before server and npm's latest lands 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:

  • YAML parses and permissions reads {contents: write, packages: write, id-token: write}
  • No secrets.NPM_TOKEN reference remains in the workflow
  • The republish listing sorts a deliberately shuffled input to core 0.7.52, core 0.7.53, server 0.8.66, cli 0.10.57
  • A non-existent path fails the step before anything publishes
  • Dispatch with republish_paths set to the four stranded files publishes them over OIDC with no token present
  • npm view @webjsdev/core version reports 0.7.53, proving latest landed on the newer version rather than 0.7.52
  • GitHub Releases exist for all four versions
  • Delete the NPM_TOKEN repo secret once the above is green

Risk

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/intellisense and the two packages/wrappers/*. The documented mitigation is a correct repository.url plus repository.directory in 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.
  • Docs site, README, scaffold templates, MCP, editor plugins: N/A because this changes CI credentials only, with no user-facing or agent-facing surface.
  • Tests: N/A because a GitHub Actions workflow has no test layer here; verified by the live re-run above.
  • Bun parity: N/A because no runtime-sensitive source is touched.
  • Dogfood apps: N/A because nothing that affects what the apps serve is touched.

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
@vivek7405 vivek7405 self-assigned this Aug 21, 2026
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.
@vivek7405
vivek7405 marked this pull request as ready for review August 21, 2026 16:31
@vivek7405
vivek7405 merged commit f3e43b2 into main Aug 21, 2026
10 checks passed
@vivek7405
vivek7405 deleted the chore/npm-oidc-trusted-publishing branch August 21, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: publish to npm via OIDC trusted publishing instead of NPM_TOKEN

1 participant