Skip to content

Publish releases through npm trusted publishing - #40

Merged
MaxGhenis merged 4 commits into
mainfrom
fix/npm-trusted-publishing
Oct 26, 2026
Merged

MaxGhenis merged 4 commits into
mainfrom
fix/npm-trusted-publishing

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Why

PolicyEngine/policyengine-ui-kit#39 merged as 7ad64d8, and its release run (40339204428) bumped the version to 0.13.0 and built the package, but npm publish failed:

npm error code E404
npm error 404 Not Found - PUT https://registry.npmjs.org/@policyengine%2fui-kit - Not found

npm returns a 404 on a publish when the credentials can't publish the package. The repo's NPM_TOKEN secret was last set on 2026-06-10, and npm still shows 0.12.1 as latest. Because the failure came before "Commit and push", main is unchanged: the three changelog fragments from #39 are still there, and the next successful run will release them as 0.13.0.

What

The same change PolicyEngine/policyengine-household-wizard#4 made, which has worked since its npm side was set up (its 0.2.0 went out on 2026-10-22 with a signed provenance statement):

  • The release job gets id-token: write, so npm can accept the workflow's GitHub OIDC identity (npm trusted publishing). The NODE_AUTH_TOKEN / NPM_TOKEN use is gone.
  • Node is pinned to 24.21.0, and a step checks it bundles npm 11.19.0. Trusted publishing needs npm 11.5.1 or later; the job used Node 20 before.
  • workflow_dispatch lets someone retry a failed release without pushing a commit. On a manual run head_commit is empty, so the job's skip-the-release-commit condition passes.
  • The job runs only on main. npm trusts the workflow from any branch, and a manual run can pick one, so without this a manual run on a feature branch would publish it as latest.
  • Releases run one at a time: the job is in a release concurrency group. The group is on the job, not the workflow, so a skipped non-main run never takes a main run's place in the queue.
  • A run releases only the commit it was started for, and only if that commit is main's tip when the run checks, before the build. npm records the run's commit as the package's source commit, so the published code must be that commit's. If main has moved on by then, the run releases nothing and starts a fresh run on the tip instead (gh workflow run, which needs actions: write). That covers a later merge and the release commit of a run this one waited for. The fresh run releases whatever fragments are still unreleased on the tip, and its provenance names the code it publishes. A merge that lands after the check doesn't change what this run publishes: it releases its own commit, and the merge's fragments go out in the next release.
  • commit_and_push.sh rebases the release commit onto main before pushing it, so a merge that lands during a release doesn't stop the release commit from reaching main. Two cases are left: a rebase conflict, or a merge landing between the pull and the push. Either stops the script with the version on npm but no release commit on main. Later runs can then keep picking that same version and fail at publish, since npm refuses a version it already has, until someone writes the release commit by hand.

Version bumping, changelog building, the build, the publish command and the release commit keep their order.

Needs Max: npm's side

This PR doesn't publish anything or touch npm. Before a release can succeed, someone with publish rights on @policyengine/ui-kit has to add a trusted publisher on npmjs.com (package settings → Trusted publishing):

  • GitHub Actions, organization PolicyEngine, repository policyengine-ui-kit, workflow push.yml, no environment;
  • under "Allowed actions", npm publish ticked. This workflow publishes directly, and per npm's docs a configuration created after 2026-09-03 allows only npm stage publish until you tick it. household-wizard's release guide asks for the same.

Then run the release by hand:

gh workflow run push.yml -R PolicyEngine/policyengine-ui-kit

Until then, any run that reaches the publish step, including the one this PR's merge starts, will fail there the same way, leaving main unchanged. Once a release succeeds, the NPM_TOKEN repo secret can be deleted.

Not in this PR, from the review: an npm-publish environment limited to main, named in npm's trusted-publisher form, would also stop a pushed branch with an edited workflow from publishing (true of the old token too); and splitting the build from the publish would keep id-token: write away from bun install and the build (household-wizard has the same single job).

Verification

  • actionlint .github/workflows/push.yml and shellcheck .github/commit_and_push.sh: clean.
  • No workflow refers to NPM_TOKEN or NODE_AUTH_TOKEN any more.
  • The toolchain pins and the publish step match household-wizard's working workflow.
  • Not run: a publish. That needs npm's side first.

🤖 Generated with Claude Code

The release for #39 built 0.13.0 but npm refused the publish with E404
on the PUT, its reply when a token can't publish the package. NPM_TOKEN
was last set on 2026-06-10. Like policyengine-household-wizard (#4),
publish with the workflow's OIDC identity instead of a stored token, on
Node 24.21.0 with npm 11.19.0, and allow a manual run to retry a release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policyengine-ui-kit Ready Ready Preview Oct 26, 2026 4:56pm UTC

Request Review

MaxGhenis and others added 2 commits October 26, 2026 07:42
The review of #40 found that npm trusts the workflow from any branch and
a manual run can pick one, so a manual run on a feature branch would
publish it. Restrict the job to main. Add a concurrency group so two
releases don't race, and rebase the release commit onto main before
pushing it, so a version npm has always lands on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run that waited behind another release would otherwise check out its
own pushed commit and bump from the version before that release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second review of #40 found that checking out main's tip could
publish a later merge's code while npm's provenance names the triggering
commit. Check out the triggering commit again, and release it only if
it is still main's tip; otherwise start a fresh run on the tip, which
carries its fragments. Move the concurrency group onto the job, so a
skipped non-main run can't displace a queued main run, and say what the
rebase before the push still can't cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 26, 2026 17:13
@MaxGhenis
MaxGhenis merged commit 6e6630d into main Oct 26, 2026
4 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged at reviewed head 5fb12ee as 6e6630d.

  • Checks: gh pr checks exit 0 on run 40363868249 (head 5fb12ee); MERGEABLE / CLEAN; not a draft; no change requests.
  • Independent review (GPT-6.1 Sol via Subfleet): r1 at 568e806 and r2 at bd8b871 requested changes, all fixed; r3 at 5fb12ee approved, with three wording fixes to this description, applied before merge.
  • Still needed before 0.13.0 publishes: the npm trusted publisher for @policyengine/ui-kit (GitHub Actions, PolicyEngine / policyengine-ui-kit / push.yml, no environment, Allowed actions: npm publish), then gh workflow run push.yml -R PolicyEngine/policyengine-ui-kit. Queued for Max as d2048.

@MaxGhenis
MaxGhenis deleted the fix/npm-trusted-publishing branch October 26, 2026 17:22

This branch was successfully deployed

1 active deployment
Preview — 5fb12eeb Deployed Oct 26, 2026 by vercel[bot]
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.

1 participant