ci: publish to npmjs.com via OIDC trusted publishing - #3
Merged
Merged
Conversation
…ublishing The npmjs.com publish authenticated with a long-lived NPM_TOKEN read from an org-level Actions secret. It now exchanges the release job's OIDC identity for a short-lived registry token instead, so no npm credential is stored anywhere. The job gains id-token: write, and NODE_AUTH_TOKEN is blanked rather than dropped: setup-node exports a dummy value when none is supplied, and a blank one makes a missing or misconfigured trusted publisher fail the publish outright instead of silently falling back to token auth. Node moves from 20 to 22 on that step and npm is upgraded to latest, because trusted publishing needs npm 11.5.1 or later on Node 22.14.0 or higher and Node 22 still ships npm 10.x. registry-url and scope stay in place. The GitHub Packages publish earlier in the same job maps @ExaDev to npm.pkg.github.com in the same .npmrc, so remapping the scope here is what keeps this step pointed at npmjs.com. The GitHub Packages publish itself is unchanged and still uses GITHUB_TOKEN.
The npmjs.com publish step ran on every push to the default branch, regardless of whether semantic-release had released anything. When the commits since the last tag warrant no release, package.json keeps its current version and `npm publish` re-publishes that version, which the registry rejects as a duplicate and which fails the job. The Release step now compares package.json's version before and after semantic-release and exposes a `released` output, and the three npmjs steps are conditional on it. package.json is the signal because semantic-release exposes no output of its own, and @semantic-release/npm's prepare step writes the next version into it exactly when there is a release to make. This matches the GitHub Packages leg, which is already conditional in the same way: semantic-release only reaches its publish phase when it has something to publish.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
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.
Moves the npmjs.com publish off the org-level
NPM_TOKENsecret and onto npm trusted publishing, so the job authenticates with its own OIDC identity and a short-lived registry token instead of a stored credential.Registration for this (done by the owner, needed before merge):
@exadev/breadboard-client, organisationExaDev, repositorybreadboard-client, workflow filenameci.yml, no environment.What changed in the job:
id-token: writeadded to the release job's permissions, which is what lets npm request the OIDC token.NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}replaced with blankNODE_AUTH_TOKEN/NPM_TOKEN. Blank rather than removed, because setup-node exports a dummyNODE_AUTH_TOKENwhen none is given; blanking it means a missing or mismatched trusted publisher fails the publish loudly rather than falling back to some other credential.npm install -g npm@latest. npm's docs require npm 11.5.1 or later on Node 22.14.0 or higher for trusted publishing, and Node 22 still ships npm 10.x, so the upgrade is not optional.The publish step was unconditional, and would have failed on this very merge
The second commit fixes a real bug that this PR would otherwise have tripped on immediately.
npm publishran on every push to the default branch, whether or not semantic-release had released anything. When the commits since the last tag warrant no release,@semantic-release/npmleavespackage.jsonalone, sonpm publishre-publishes the version that is already on the registry and gets rejected as a duplicate.That is exactly what a merge of this PR would have done. Every commit since
v1.0.0isci:orbuild:, and@semantic-release/commit-analyzer's default release rules (lib/default-release-rules.js) only release onfeat,fix,perf, a breaking change or a revert. So semantic-release would have released nothing,package.jsonwould have stayed at1.0.0,@exadev/breadboard-client@1.0.0is already published, and the job would have gone red on main.Worth being clear that this is pre-existing rather than something the OIDC change introduced: the step has been unconditional since it was added, and with the old token it would have failed the same way. It has just never actually run. The step was added in f8b5a7b, which is the current tip of main, one day after the
v1.0.0release commit, and there has been no push to main since.The fix: the Release step now records
package.json's version before and after semantic-release and exposes areleasedoutput, and the three npmjs steps are conditional on it.package.jsonis the signal because semantic-release exposes no step output of its own, and@semantic-release/npm's prepare step writes the next version into it exactly when there is a release to make. This mirrors the GitHub Packages leg, which is already conditional in the same way, because semantic-release only reaches its publish phase when it has something to publish.Practical consequence: merging this publishes nothing to either registry. The trusted publisher gets exercised on the first
feat/fix/perfcommit that lands afterwards.One thing I deliberately left alone
registry-urlandscopestay on that setup-node step, which is different from how trilean/wire-mesh/documents.js do it. Those repos publish to one registry and can omitregistry-urlentirely. This job publishes twice: semantic-release pushes to GitHub Packages first, which maps@exadevtonpm.pkg.github.comin the same.npmrc. Dropregistry-urlhere andnpm publishwould push to GitHub Packages a second time instead of npmjs.com. The_authTokenline setup-node writes alongside it does not get in the way:npm publishruns the OIDC exchange before it reads any credential (lib/commands/publish.jscallsoidc()beforegetCredentialsByURI) and writes the exchanged token over the same user-level config key (config.set(authTokenKey, token, 'user')inlib/utils/oidc.js). setup-node's own README says the same thing: "npm Trusted Publishing (OIDC) is not affected, since it does not useNODE_AUTH_TOKEN."The GitHub Packages publish is untouched and still uses
GITHUB_TOKEN.Provenance will be attached automatically once a publish does happen, since npm enables it by default for trusted publishing from a public repo. No
--provenanceflag needed.