Moving Act's CD publish path off long-lived npm tokens.
This is no longer preventative work. Publishing broke on 2026-08-18, well ahead of the ~January 2027 deadline this checklist was written for. The token stopped being accepted mid-day: the same pipeline published
@rotorsoft/act-pg@1.15.1successfully on 2026-08-17, and the next run (32141894683) failed with404 Not Found - PUT https://registry.npmjs.org/@rotorsoft%2factfor every package. npm answers 404 rather than 403 on a scoped package when the caller is not authorized. There is no working fallback to migrate away from — completing this checklist is the fix.
| Piece | Status |
|---|---|
| Publish job | .github/workflows/ci-cd.yml → cd matrix → npx semantic-release |
| Publish command | pnpm publish --no-git-checks --access public via @semantic-release/exec |
| Auth | ❌ secrets.NPM_TOKEN no longer accepted by npm |
| OIDC permission | ✅ Already set: id-token: write on the cd job |
| Provenance | ✅ Already set: NPM_CONFIG_PROVENANCE: true |
| Trusted Publisher config | ❌ Not registered for any package — this is the gap |
| Install path | pnpm install (npm v12 install-time defaults do not gate CI) |
Provenance OIDC is not the same as trusted-publishing auth — both need
id-token: write, but trusted publishing also needs a Trusted Publisher config
on npmjs.com.
GET [secure]&audience=npm%3Aregistry.npmjs.org 200 256ms
[WARN] Skipped OIDC: ERR_PNPM_AUTH_TOKEN_EXCHANGE ... (status code 404)
[E404] 404 Not Found - PUT https://registry.npmjs.org/@rotorsoft%2fact
The 200 is GitHub minting the OIDC identity token — that half works today.
pnpm then offered it to npm, which returned 404 because no Trusted Publisher
is registered for the package. Registering one turns that exchange into a
short-lived publish token. The Skipped OIDC line is not new and was never the
problem: the successful 2026-08-17 run logged it too, then fell back to a token
that still worked.
pnpm attempts the exchange whether or not a static token is present, so
removing NPM_TOKEN is hygiene, not the enabler.
| When | Change | Impact here |
|---|---|---|
npm v12 latest |
Install-time defaults: scripts / git / remote deps opt-in | Low — CI uses pnpm |
| ~early August 2026 | 2FA-bypass GAT cannot do account / org / package management | Do those ops interactively with 2FA |
| 2026-08-18 (actual) | Token rejected; every publish 404s | CD is broken now |
| ~January 2027 | 2FA-bypass GAT cannot publish directly | Moot once migrated |
Add a Trusted Publisher on npmjs.com for each published @rotorsoft/* package:
-
@rotorsoft/act -
@rotorsoft/act-pg -
@rotorsoft/act-sqlite -
@rotorsoft/act-patch -
@rotorsoft/act-http -
@rotorsoft/act-sse -
@rotorsoft/act-pino -
@rotorsoft/act-notify -
@rotorsoft/act-otel -
@rotorsoft/act-crypto -
@rotorsoft/act-ops -
@rotorsoft/act-tck -
@rotorsoft/act-diagram
For each package → Settings → Trusted Publisher → GitHub Actions:
| Field | Value |
|---|---|
| Organization / user | Rotorsoft (match npm’s case rules) |
| Repository | act-root |
| Workflow filename | ci-cd.yml (basename only) |
| Environment | leave empty unless the job gains an environment: |
| Allowed actions | npm publish (and staged publish only if you opt into that flow) |
-
pnpmattempts the OIDC exchange natively — repo pinspackageManager: pnpm@11.21.0(the checklist previously said 11.17.0), and the 2026-08-18 log shows it trying the exchange unprompted. - Node stays ≥
22.14.0— workflow pins22.23.2 - GitHub-hosted runners (self-hosted OIDC for npm is not supported yet)
- Escape hatch is not usable as written. Switching
publishCmdtonpm publishneeds npm CLI ≥11.5.1, and Node 22.23.2 ships npm 10.x — that route needs an explicitnpm i -g npm@lateststep added first. Worth knowing before reaching for it mid-incident.
The original plan validated one package while keeping the token as a safety net. The token is dead, so there is no net: registering the publisher is the recovery, and the first successful publish is the validation.
- Register the Trusted Publisher for
@rotorsoft/act-patchfirst (small, easy to release on its own) - Land the workflow change dropping
NPM_TOKEN/NODE_AUTH_TOKEN— safe to merge on its own: it touches nolibs/path, socddoes not run - Trigger a release with a trivial
fix(act-patch):commit on master - Confirm the log shows a successful exchange rather than
Skipped OIDC - Confirm the published version shows provenance on npmjs.com
The first trusted publish got past auth and failed provenance verification instead:
[E422] Error verifying sigstore provenance bundle: Failed to validate repository
information: package.json: "repository.url" is
"git+https://github.com/rotorsoft/act-root.git", expected to match
"https://github.com/Rotorsoft/act-root" from provenance
The attestation records GitHub's canonical owner casing (Rotorsoft); every
package.json in the repo said rotorsoft. npm compares them exactly.
Each package needs the one-word fix before its first trusted publish, and
it must ride a commit that touches only that package — the changes job keys
the release matrix on libs/<lib>/**, so fixing all thirteen at once would
fire a release for every library, and each one without a registered publisher
fails and strands a tag.
-
@rotorsoft/act-patch+ rootpackage.json - The other twelve, in one commit once every publisher was registered
- Register Trusted Publishers for the remaining twelve packages
- Delete the
NPM_TOKENrepository secret - Keep
NPM_CONFIG_PROVENANCE: true— trusted publishing enables provenance by default, but the explicit flag survives a change of publish path
semantic-release commits the version bump and pushes the tag before the publish step, so four releases exist in git and not on npm:
| Package | git tag | on npm |
|---|---|---|
@rotorsoft/act |
1.28.0 | 1.27.0 |
@rotorsoft/act-pg |
1.16.0 | 1.15.1 |
@rotorsoft/act-sqlite |
1.16.0 | 1.15.0 |
@rotorsoft/act-tck |
1.33.0 | 1.32.0 |
@rotorsoft/act-patch |
1.2.8 | 1.2.7 |
No GitHub releases were created either — that plugin also runs after publish.
- Decide: leave the gap (recommended — nobody can have pinned a version
that never existed, and the next merge takes the following numbers), or
publish the four by hand. Note a manual publish needs an interactive
npm loginwith 2FA, since trusted publishing only works from inside the workflow, and it produces no provenance. - Re-running the failed jobs does not republish them: semantic-release sees the tags already present and concludes there is nothing to release.
- Keep
@semantic-release/npmwithnpmPublish: falseand publish via@semantic-release/exec→pnpm publish(current pattern) - If OIDC fails under pnpm, temporary escape hatch: switch
publishCmdtonpm publish --access public(npm CLI OIDC is the reference path), then revisit pnpm once confirmed - Watch for
actions/setup-noderegistry-url+.npmrcinteractions if auth errors appear after removing the token — adjust only if a real failure shows up
- Stop using any 2FA-bypass GAT for: creating/deleting tokens, changing maintainers / access, editing trusted-publisher config, org/team grants
- Perform those actions interactively with 2FA
- Prefer a read-only token (if any) for private-package installs; never a publish-capable bypass token in CI after cutover
- npm v12 install defaults — CI installs with pnpm. Revisit only if something
in the toolchain starts calling
npm installdirectly, or localnpmworkflows need anapprove-scriptsallowlist. - Staged publishing + human 2FA — valid fallback, but incompatible with unattended semantic-release. Prefer trusted publishing.
- Every published
@rotorsoft/*package has a Trusted Publisher forRotorsoft/act-root/ci-cd.yml - A master release publishes without
NPM_TOKEN/NODE_AUTH_TOKEN - The GitHub
NPM_TOKENsecret is deleted - Provenance still appears on new publishes
- This checklist’s cutover items are all checked