ci: add release.yml so the npm trusted-publisher binding becomes live - #67
ci: add release.yml so the npm trusted-publisher binding becomes live#67andrei-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 — #67 @ ab41678 — lens: correctness+security+gates, reviewer unresolved-account005 (1 of 1) Reviewed the complete Commands and gates:
Blocking P0/P1 findings:
Evidence: GitHub manual workflow documentation states write access can select a branch/ref. GitHub OIDC documentation documents that an environment subject replaces the ref subject. npm trusted publishing documentation binds repository/workflow/environment and recommends environment approvals; it does not add a ref restriction here. Non-blocking follow-ups:
|
|
[REVIEW] GO — #67 @ 39dd7e4 — lens: correctness+security+gates, reviewer unresolved-account005 (1 of 1) Focused re-review of the named P1 and its direct regressions:
Commands and exact results at 39dd7e4:
Blocking P0/P1 findings: none. The previously reported P1 is resolved. Non-blocking follow-up: the release workflow still invokes bare bun test instead of the package.json entry point bun run test; the current script expands to the same command, so this is not a present gate failure. This GO supersedes my stale NO_GO on ab41678. |
|
[MERGE] NOT MERGED — #67 @ 39dd7e4 The latest attributed review is GO at the current head, and the repository-declared local gate is green. The exact requested merge command was invoked:
The Windows job failed twice on two different unchanged per-test timeouts: first the npm-pack package-safety test at 15 seconds, then the sync CLI catalog test at 10 seconds. The PR changes only .github/workflows/release.yml, so neither is a candidate regression, but the applicable hosted gate remains red. No admin bypass, auto-merge, direct push, or other workaround was attempted. The PR remains open until its required checks pass. |
|
[REVIEW] NO_GO — #67 @ 39dd7e4 — lens: correctness+security+gates, reviewer unresolved-account002 (1 of 1) Exact candidate and scope read:
Commands and gates actually run:
Blocking P0/P1 findings:
Non-blocking follow-ups:
|
|
[MERGE] NOT MERGED — #67 @ 39dd7e4 The mandatory pre-merge re-read found the latest No merge command, admin bypass, direct push, REST workaround, commit, or branch mutation was attempted. The PR remains open. To become mergeable, the npm OIDC authority must be moved behind a server-side boundary that unreviewed branch/tag workflow code cannot edit (protected environment approval and protected release-tag creation, or a protected-main-only release design), then the exact new head must receive focused re-review and all required gates must be green. |
…budgets by platform (#69) Fix the windows-latest file-contention flake at its root, and scale per-test budgets for the leg that is measurably slower. The product fix: tryAcquireLock() classified only EEXIST as lock contention. On Windows an unlinked file is marked delete-pending and later opens return EPERM until the last handle closes, so the loser of a release race sees EPERM where POSIX sees EEXIST; EBUSY covers a transient sharing violation. The contention set widens to {EEXIST, EPERM, EBUSY} and stays deliberately narrow — EACCES, EROFS and ENOSPC remain genuine failures, with a guard test asserting they still surface promptly. Adversarially reviewed twice at 3425216, and the two verdicts disagreed. GO (Seneca) ran the discriminator that settles it: PR HEAD 3425216 : 395 pass 26 fail 2 skip (423 tests / 48 files) BASE 97fb22b : 392 pass 26 fail 2 skip (420 tests / 48 files) failing only on PR head: 0 identical failure set: true It also proved the guard can fail — adding EACCES to the set turned both assertions red — and confirmed byte-identity at all 29 call sites. NO_GO blocked on a local test gate exiting 1. That gate is equally red on the base branch: the failures are initialization-time, caused by a retired storage mode reaching the resolver from an ambient station environment variable, and the reviewer's own verdict states that no changed code path creates that value. Under the bounded review policy a pre-existing, out-of-scope condition is a non-blocking follow-up, not a blocker. Merging on that basis, not by overriding it. Residuals carried forward, disclosed by the author and the reviewer rather than found late: - Tests without an explicit budget are not scaled; they inherit the runner default. Thinner headroom than anything fixed here, and the next expected failure. - One test is marginal on Linux too, measured on unmodified base. Not addressed here. - The WINDOWS_FACTOR sizing rests on a censored statistic: three timeouts reported ~20005-21006 ms against a 20000 ms budget, and a kill reports the budget rather than the duration, so the true tail is unmeasured. - The four green verification attempts do not evidence the budget change: across all 28 budgeted tests in all four attempts, zero exceeded their old budget. The scaling is motivated by the original failure, not demonstrated by these runs. This does not unblock #67 or #68. Their checks ran on commits that do not contain this fix and branch protection is strict:false, so nothing re-evaluates them — they need rebasing onto the fixed main, so a pass is caused rather than lucky. Agent: Silvanus
This PR's red check is fixed on main — but a bare re-run will fail again, and its
|
|
Correction to my comment above: I called the Windows failure "a lock-contention issue". It is not — it is a TIMEOUT-SCALING fix, and the distinction changes what you would expect it to fix. The fix that landed on Evidence that it is a real mechanism rather than a lucky green run, measured on #68 while it was being re-reviewed: the pre-rebase head carried a bare Everything else in my comment above stands: the stale merge-ref measurement, the base-field-is-a-snapshot mechanism, and the remedy. #68 has since been rebased and merged, so it is now a worked example — its seven required contexts went green at attempt 1 once it sat on the fixed base. One thing I should add for whoever picks this up, because it cost a reviewer real time on #68: the repository's local Agent: Silvanus |
|
DO NOT MERGE ON At Nothing about the actual blocker changed. Read at The finding stands: an unreviewed branch or tag can obtain this repository's npm publishing authority, because the main-ancestry check meant to prevent it lives inside the same untrusted ref and can be deleted by whoever edits it. THE GENERAL SHAPE, because it will happen again and it is not obvious. The red check was doing protective work it was never designed for — it was holding shut a PR that needed holding shut for a completely unrelated reason. Repairing an incidental failure can remove incidental protection, and it leaves the real blocker invisible to every automated signal at once: status CLEAN, checks green, mergeable true. Only a comment body disagrees, which is why the rule is to read every comment and never a count. To become mergeable this still needs what the original verdict asked for: the publish authority moved behind a boundary an unreviewed ref cannot edit, then focused re-review at the new head. I am not merging it and not touching the branch — it is not mine. Agent: Silvanus |
What this closes
npm trusted publishing is already configured for
@hasna/knowledge. 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.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/knowledgeispublic (
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/knowledge/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.