Skip to content

feat(release): zero-credential publishing via trusted publishing - #14

Merged
phall1 merged 1 commit into
mainfrom
zero-credential-release
Aug 9, 2026
Merged

feat(release): zero-credential publishing via trusted publishing#14
phall1 merged 1 commit into
mainfrom
zero-credential-release

Conversation

@phall1

@phall1 phall1 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Works backwards from the agreed end state: no long-lived credential anywhere in the release path, and a release is not green until the published package has been installed from the registry and executed on every platform it shipped for.

Single entry point (the npm caller-filename rule)

npm's trusted publishing validates the calling workflow's filename when the publish runs inside a reusable workflow. A publisher registered for fork-publish.yml would never match — npm sees release-please.yml in the OIDC claim.

So the manual re-publish path moves up into release-please.yml as a publish_tag input, and fork-publish.yml keeps only workflow_call. One filename to register on npmjs.com; no second entry point that silently cannot authenticate.

Auth preflight reports, not guesses

The verify job states which methods are available — OIDC, token, or both — and fails only when neither exists. The whoami/scope check becomes a warning, since under trusted publishing there is no token account to interrogate. (This preflight is what finally surfaced the 2FA-bypass cause after two blind 404 releases.)

verify-published matrix

npm publish exiting 0 says nothing about whether the registry serves the package, the wrapper resolves the right platform binary, or the binary runs — v0.14.1's registry read-path lagged the write by ~4 minutes. A final matrix job installs the release from the registry on all four runners, waits out that lag, and fails unless phui --version prints the released version.

Homebrew loses its credential entirely

The tap ignores dispatch payloads and re-verifies everything from the release itself — the dispatch only ever bought latency. With phall1/homebrew-tap#7 polling every 15 minutes, the dispatch job is deleted here and HOMEBREW_TAP_TOKEN becomes deletable.

Migration safety

NPM_TOKEN stays for one release as a fallback — npm auto-detects OIDC and prefers it when both are present — then gets deleted once a release is observed publishing without it.

Prerequisite (manual, npmjs.com)

Trusted publisher on all five packages: GitHub Actions / phall1 / phui / workflow release-please.yml / allowed action npm publish.

Also pins npm ≥ 11.5.1 in publish jobs (upgrade only when the bundled one is older).

…t shipped

Works backwards from a release pipeline that holds no long-lived credential.

npm's trusted publishing validates the *calling* workflow's filename when the
publish runs inside a reusable workflow, so `fork-publish.yml` can no longer be
entered directly — a second entry point would be a path that silently cannot
authenticate once the token is gone. The manual re-publish moves up to
`release-please.yml` as a `publish_tag` input, so automatic and manual releases
both come through the one filename a trusted publisher can name.

npm prefers OIDC over any token it finds, so `NPM_TOKEN` stays as a fallback and
this change is safe to land before the publishers are registered. It becomes
dead weight the moment they are, and gets deleted then rather than in a cutover.

The publish jobs now upgrade npm when the runner's bundled version predates
11.5.1, instead of trusting that whatever Node 24 ships stays new enough.

The old credential preflight assumed a token existed. It now reports which
mechanism will actually be used — OIDC, token, or neither — and only treats a
scope/account mismatch as fatal when there is no OIDC to fall back on. Three
releases were lost today to npm reporting "you cannot publish here" as a 404
that is indistinguishable from a typo, which is what this exists to prevent.

Drops the Homebrew dispatch and with it this repository's last credential. The
tap re-resolves every release and recomputes every digest itself and has always
ignored the payload, so the dispatch only ever bought latency in exchange for a
PAT that expires silently. The tap now polls every fifteen minutes instead.

Finally, a green `npm publish` is not evidence anyone can install the result: it
says nothing about whether the registry read path has caught up, whether the
wrapper resolves the right platform package, or whether the binary runs there.
`verify-published` installs the published version from the registry on all four
platforms and executes it. Today that gap nearly had a successful publish
reported as a failure, because the packages were not readable for four minutes
after the write succeeded.
@phall1
phall1 merged commit 6680a9d into main Aug 9, 2026
1 check passed
@phall1
phall1 deleted the zero-credential-release branch August 9, 2026 17:55
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