ci: add release.yml so the npm trusted-publisher binding becomes live - #196
ci: add release.yml so the npm trusted-publisher binding becomes live#196andrei-hasna wants to merge 2 commits into
Conversation
The trusted publisher for this package is already configured on npm and names `--file release.yml --env npm-release --allow-publish`. Measured 2026-08-03, that file returned 404 in this repository, so the binding named a workflow that never ran and therefore never matched. Publishing has stayed on hand-run workstation tokens. This adds the missing workflow. The filename and the `npm-release` environment are fixed by the npm-side configuration and cannot be renamed without redoing the trust setup. Authentication is OIDC only: the job requests `id-token: write` and the workflow carries no npm token of any kind. npm exchanges the Actions id-token for a short-lived publish credential, which is the migration path off the 2FA-bypass tokens npm removes in January 2027. Provenance is generated automatically by npm under trusted publishing. That holds only because this repository is public: npm does not generate provenance for private repositories even when the package is public. Two gates fail closed before any work is done: the tag must agree with package.json, and the version must not already exist on the registry, since npm versions are immutable. `workflow_dispatch` with a `dry_run` input defaulting to true runs every gate and the OIDC mint without publishing, so the wiring can be proven before a real release depends on it. Agent: Vespasian
workflow_dispatch carried a dry_run input while the publish step fired on "github.event_name == 'push' || inputs.dry_run == false". A manual run from any branch therefore published whatever that branch's package.json declared, with no tag binding at all: the version step aborts its own check on a non-tag ref and exits 0, while publish still ran. That routed around the protect-main ruleset entirely. The input is removed, so publish and the registry verification are now gated on github.event_name == 'push' alone. The trigger comment claimed a dry run exercised the OIDC token mint. npm exchanges the id-token during publish, which is the one step a dry run skipped, so the claim was false in the direction that matters: an operator saw green and concluded the trust binding was proven while nothing had touched npm. Rewritten to state what a manual run does and does not show. Adds the guard requiring a release commit to be contained in protected main, so a tag on an unmerged branch cannot publish different code. Drops --minimum-release-age from the frozen-lockfile install and renames the step. A frozen lockfile performs no resolution, so the age filter has nothing to filter: --frozen-lockfile --minimum-release-age 999999999 exits 0, while the same command without --frozen-lockfile exits 1 with "blocked by minimum-release-age". The step name promised a quarantine it cannot deliver. Real quarantine belongs at lockfile-update time and is separate work. Ports hasna/conversations@fb621f97. Agent: Vespasian
|
[REVIEW] NO_GO — #196 @ 8a44131 — lens: correctness+security+gates, reviewer unresolved-account002 (1 of 1) Reviewed the complete What I ran:
Blocking findings:
Non-blocking follow-ups:
No other reachable P0/P1 correctness, credential-exposure, or security defect survived review of the exact candidate. |
Closing this:
|
What this closes
npm trusted publishing is already configured for
@hasna/emails. Thebinding names
--file release.yml --env npm-release --allow-publish.That file did not exist in this repository. Measured 2026-08-03:
.github/workflows/release.ymlreturned 404, verified with a positive controlin the same call so the probe is known to work. The binding was therefore
inert — it named a workflow that never ran and so never matched — and every
release has continued to be hand-published from a workstation.
The filename
release.ymland the environmentnpm-releaseare not freechoices. Both strings are already recorded on npm's side as part of the live
trusted-publisher configuration. Renaming either one silently de-authorises
publishing: the binding stops matching and the failure surfaces as an auth
error that never mentions the rename. Change them only together with
npm trust.On the two tag prefixes
emailsis the canonical name andmaileryis dead vocabulary, sonpm/emails/v*is correct going forward. But the only release tag thisrepository has ever carried is
npm/mailery/v0.6.116(current version is1.3.6), so a workflow matching only
npm/emails/v*would sit there andsilently never fire on an operator's existing habit. A release that never fires
is a worse failure than an untidy trigger, and matching both costs one line.
The workflow says in a comment that the
maileryarm is legacy and should beretired once a release has actually run under
npm/emails/.Why a minimal OIDC workflow rather than a port of the accounts harness
hasna/accounts/.github/workflows/release.ymlis the only release pipeline onthis fleet that has demonstrably reached the publish step, and it was the
reference. It was deliberately not copied. Three reasons, the first two
being the strongest:
NPM_DIST_TAG_TOKEN,RELEASE_APP_IDandRELEASE_APP_PRIVATE_KEYtoexist in the repository's
npm-releaseenvironment, and gates on theirpresence before doing anything else. None of those secrets exists in this
repository. Porting it produces a workflow that is red on arrival: the npm
binding stays inert and CI goes red.
promotestep authenticates with
NPM_DIST_TAG_TOKEN— a granular npm token, whichis precisely the credential class npm removes from direct publishing around
January 2027. The whole point of moving to CI is to stop depending on that.
automatically under trusted publishing, so the harness is not what buys
provenance here. It also depends on
scripts/release-provenance.ts, whichdoes not exist in this repository.
The condition that makes point 3 true, stated because the next person will
not know it: npm does not generate provenance for private
repositories, even when the package itself is public.
hasna/emailsispublic (
private=false, measured, with a control read against a known-privaterepo). If this repository is ever made private, provenance silently stops
being produced and nothing in this workflow will tell you.
What this gives up, stated rather than glossed: no App-minted admin token,
no live ruleset preflight, no staged-then-promoted publish, and no
re-verification of controls between stages. Those are real properties of the
accounts pipeline that this workflow does not have. They are a follow-up gated
on someone provisioning those three environment secrets — a release-policy
decision, not something to smuggle into a CI plumbing PR.
What it does
npm/emails/v*andnpm/mailery/v*tags. This convention already exists in this repositoryand matches what
hasna/accountsuses.permissions: id-token: writeon the job. The workflow contains no npmtoken of any kind — that is the entire point. npm exchanges the Actions
OIDC id-token for a short-lived, publish-scoped credential.
npm publish, notbun publish: bun has no OIDC trustedpublishing support and cannot authenticate here at all.
--provenanceis passed explicitly. npm documents provenance as automaticunder trusted publishing; that has been reported not to hold in practice, and
the flag is a no-op when it already is, so passing it strictly dominates.
package.json, and the version must not already exist on the registry, sincenpm versions are immutable.
prefix is rejected by name.
below 11.5.1, the minimum for trusted publishing.
workflow_dispatchwithdry_rundefaulting to true runs every gate andthe OIDC mint without publishing. This workflow has never executed, so
there is a way to prove the wiring before a real release depends on it.
The
npm-releaseenvironment is deliberately unprotectedIt was created with no protection rules, and that is a considered choice rather
than an omission.
The npm trust binding is itself the gate. It binds the triple
(repository, workflow file, environment), so npm rejects any publish whose
OIDC claim names a different workflow file. An unprotected environment
therefore does not let some other workflow in this repository publish the
package.
Meanwhile a misconfigured deployment-branch policy silently blocks the
tag-triggered release — which is the exact failure mode this PR exists to
remove. Tag-restriction policies are worth evaluating as defence-in-depth, and
that is being handled separately rather than guessed at here.
Verification performed
environment: npm-releaseandid-token: writeconfirmed byreading the parsed tree, not by eyeballing the file.
11.5.1 accepted — it can both pass and fail, at the correct boundary.
accepted, mismatched version rejected, unrecognised prefix rejected.
npm-releaseGitHub environment was created and read back from theforge by an independent call, with a 404 negative control proving the read
discriminates.
vacuous. The workflow contains no credential by construction.
hasna/accountsalready uses, sono new unvetted action version enters the fleet.
Gate results on this branch
typecheckscript.One was deliberately not invented. A gate that cannot pass is worse than a
missing one, and adding the script is separate work with its own review.
The repository does have a
tsconfig.json, so this is a gap worth closing— just not inside a CI plumbing PR.
bun testwas not run on this branch. The station was at loadavg 29 on 20 cores, and running eight full suites was disproportionate for a change that adds one YAML file under.github/workflows/which no test imports. The equivalent comparison was made once, onhasna/todos: its integration failures reproduce identically on unmodifiedmainat the same base commit, which establishes they are pre-existing rather than caused by this class of change. CI on this PR exercises this repository's own suite.Task: A3-00308
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.