Publish by OIDC trusted publishing instead of the expired npm token

Merged
UN
2mo ago
+783-24

The publish credential is dead, and nothing said so

pm-github last reached npm on 2026.8.18. main reads 2026.8.18, with no matching tag and nothing on the registry. Sixteen of the eighteen published fleet packages are in the same state, all stopping on 2026-08-16 or 2026-08-17.

The release job fails at Publish npm package:

npm error code E404
npm error 404 Not Found - PUT https://registry.npmjs.org/pm-github - Not found

A 404 on PUT is a rejected write credential, not a missing package. The registry answers 404 rather than 401 so it does not disclose package existence to an unauthorised caller — which is why this reads like a normal first publish rather than an outage. The repository secret was already rotated on 2026-08-22 and the failures continued unchanged; npm whoami against the maintainer host's stored auth returns 401. The credential is dead, not mis-stored.

Nothing else noticed, because the version bump and the release commit both land before the publish step — deliberately, so an interrupted release resumes the same version. So main advances whether or not the artifact ships and every other job stays green.

The change

Trusted publishing removes the credential that can expire: the registry mints a short-lived one from the workflow's own OIDC identity. Two changes had to go together, and the second is the easy one to miss:

  1. NODE_AUTH_TOKEN / secrets.NPM_TOKEN are gone from the publish step. id-token: write and registry-url stay — they are what make OIDC reachable.
  2. npm is raised to >=11.5.1 before the publish step. node 22 ships npm 10.x, which has no trusted-publishing support and would silently fall back to token auth — producing exactly the same E404 and looking like the migration simply failed.

Verification

Two guards fail closed, and both were mutation-checked rather than assumed:

MutationResult
Reintroduce NODE_AUTH_TOKENOIDC test fails
Delete the npm upgrade stepnpm-version test fails
Unmodifiedboth pass

Gates: build, check, test, docstring, changelog:full + changelog:check, and pm health --strict-exit against the pinned binary — all green.

Requires a one-time npmjs.com setup binding pm-github to unbraind/pm-github and release.yml before the next release can publish. Until that exists this workflow fails closed, which is correct but is not a release.

pm item

Summary by Sourcery

Migrate release publication to fail-closed npm OIDC trusted publishing and protect the release workflow from stale credentials, unsafe execution paths, and unverified registry access.

New Features:

  • Switch npm package publication to OIDC trusted publishing without long-lived npm tokens.
  • Preflight npm and registry acceptance before release mutations so rejected publishing identities fail closed.

Bug Fixes:

  • Prevent releases from advancing version and commit metadata when npm publishing credentials or trusted-publisher configuration are invalid.
  • Prevent non-main workflow runs from executing release operations with publishing permissions.

Enhancements:

  • Pin and verify a trusted-publishing-capable npm version at installation and publication time.
  • Remove legacy npm credentials from user and global configuration before publishing.
  • Harden release workflow handling of registry outages, rate limits, credential cleanup, permissions, and workflow-controlled inputs.

Deployment:

  • Require a one-time npmjs.com trusted-publisher binding for the package, repository, and release workflow.

Tests:

  • Add mutation-focused tests covering OIDC authentication, npm version enforcement, credential removal, workflow permissions, preflight ordering, and fail-closed behavior.

Chores:

  • Ignore runtime PM transaction journals from version control.
  • Add PM issue and history records for the release publishing migration.

Summary by cubic

Switches npm publishing from the expired stored NPM_TOKEN to OIDC trusted publishing, and makes the release fail before any version bump when npm rejects the workflow's identity.

  • Gates the release job on main at the job level, keeping a step-level check as defense in depth.
  • Pins npm to 11.19.0, fails closed on the effective version at install, re-verifies it immediately before publication, and rejects GITHUB_PATH writes in between.
  • Preflights the OIDC id-token exchange before any mutation, encoding scoped names per npm's escapedName, accepting any 2xx, and distinguishing a registry outage or rate limit from a misconfigured trusted publisher.
  • Scrubs every legacy credential from setup-node, a stray .npmrc, and npm's global config, rejecting any npm set or npm config set of a credential key.
  • Stores the exchange's short-lived publish credential in an mktemp file removed by exactly one EXIT trap; the success message notes the exchange proves identity, not what the binding permits.
  • Passes repository metadata to run scripts through env, so repository-controlled text can't be spliced into privileged shell commands.
  • Covers the workflow with an enumerated mutation suite of fail-closed cases, including the inline-permissions branch that used to report correctly declared permissions as missing.
  • Fixes six guards that could fail open: the audit's catch rethrows everything except a missing control, unspaced &&, ||, and ; split changelog commands, the release-job boundary accepts a leading underscore in job ids, both YAML block-scalar indicator orders are matched, and the changelog gate asserts the flag changes the heading instead of pinning today's date.
  • Adds an identity audit that fails any post-baseline commit with an unapproved author or committer or a new absolute home path.
  • Ignores .agents/pm/transactions/ journals, which are runtime state that would trip pm health if committed.

Migration

Bind pm-github to unbraind/pm-github and release.yml on npmjs.com once before the next release; until then publishing fails closed.