.forgejo/workflows/release.yml — state RELEASE_TOKEN's real scope set (read:user, write:package, write:repository) #60
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:ci
scope:cli
scope:docs
scope:manifests
scope:packaging
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/stoke#60
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
.forgejo/workflows/release.ymlL8-10 states the credential contract forsecrets.RELEASE_TOKEN:Superseded on
mainat 2026-09-03T07:30:22Z — the quote above is the pre-fix comment, kept verbatim (triage). !61 merged as2230ca25, and L8-12 now readread:user, write:package and write:repositorywith the rationale attached. The framing verb in the sentence above ("states") was true at the mint and is kept, not rewritten: it is the defect this issue reports, and rewriting it would erase what the issue is about. Read it as "stated, until2230ca25".That line was written when the Create release and attach .deb step was three hand-rolled
curlcalls, which never logged in. #54 merged as !55 (d6a21c9d, 2026-09-02T11:22:58Z) and replaced them withscripts/publish-release.sh, whose first action isrun_stoke auth login --url "$FORGE_URL" --token-file "$TOKEN_FILE"(publish-release.shL41). The conversion added a login — and therefore a third scope — without updating the line that states the contract.The reproduction is a live run, not a hypothetical.
v1.5.0was tagged at088e7e2don 2026-09-02T22:29:15Z and run 990 executed the door in order:The failure, verbatim from the run log:
The token @andres provisioned matches the documented requirement exactly — it authenticated against the package registry and published
stoke 1.5.0. The documentation is what is wrong, and the next person to provision this secret from that comment would create the same insufficient token.Spec
Decision: correct the stated contract to the three scopes the door actually requires. Do not change
auth login's behaviour.The required set is
read:user,write:package,write:repository.Where each is spent, measured at
088e7e2d:write:packagepublish-deb.shPUTs to/api/packages/heavy-duty/debian/.../upload201,Published.read:userauth logincallstokenClient.get('/user')atsrc/cli.jsL225 to resolvelogin/username/emailfor the stored config403 … required scope(s): [read:user]write:repositoryrelease view/release create/release uploadauth loginaborts firstRejected alternative, and why. The
read:usercall buys the release flow nothing operationally:publish-release.shpasses--ownerand--repoexplicitly to all three release commands, and none of them readsconfig.login(theconfig.loginfallbacks insrc/cli.jsareauth logoutL284/L290,repo listL401,repo createL570 andmigrateL657 — no release command among them). So the door could be made to need only two scopes — either by havingpublish-release.shwrite its own$TMP/config.jsoninstead of callingauth login, or by havingauth logintolerate a 403 on/user. Both are rejected here.auth login's/usercall is a deliberate token validation: it is what turns a typo'd or revoked token into a clear message at the top of the step instead of a confusing one further down. Skipping it is worse than it looks, becausepublish-release.sh's get-or-create swallowsrelease view's output (>/dev/null 2>&1), so an auth failure there would silently fall through torelease createand surface as a create error. Narrowing token validation is also adjacent to #9 (security: narrow default scopes created by stoke auth login), closed, and is not something to reverse as a side effect of a documentation fix. The contract comment is the thing that drifted; fix the comment.Scope of the diff: the comment block, and nothing else. No workflow step changes, no script changes, no CLI changes. If the two-scope door is later judged worth building, that is a separate issue with its own spec.
Tasks
.forgejo/workflows/release.ymlL8-10 so it names all three scopes —read:user,write:package,write:repository— in the Forgejo scope spelling the token creation screen uses, not thepackage:write/repository:writeorder-reversed form currently there. (Ticked by triage 2026-09-03 at2230ca25. The trailing clause "currently there" inverted the instant this merged — the reversed spellings are gone frommain— and is kept verbatim because it is the instruction the builder was actually given.)read:useris needed in the same comment (one clause:publish-release.shrunsstoke auth login, which validates the token against/user), so the next implementation change that removes the login also removes the scope.changelog.d/fragment.Refs #60, notCloses— the post-merge criterion below outlives the merge.Acceptance criteria
.forgejo/workflows/release.yml's header comment names exactlyread:user,write:package,write:repositoryand no longer says "package:write and repository:write".The comment states which step spends
read:userand why.git diffon the PR touches only.forgejo/workflows/release.ymland onechangelog.d/*.mdfragment. No step, script or CLI change.npm testpasses andci / testis green on the PR head.Post-merge, triage owns the close. The PR references this issue with
Refs #60, neverCloses #60; the merge moves it topost-mergeand releases the claim. Wake condition: the firstv*tag run in which the Create release and attach .deb step is reached — that run proves the corrected scope set is sufficient and not merely longer. If that run still fails on a scope, the comment is still wrong and this issue re-opens its spec rather than closing. CORRECTED — the wake is the re-run of run 990's job, not anotherv*tag (triage, 2026-09-03). This criterion was written at 2026-09-02T22:50:35Z, ~2 minutes before the same diagnosis was written onto #54's AC6 (22:52:29Z), #56 (22:51:54Z) and #27 (22:53:03Z, again 23:49:04Z). This is the one body of the four that never received it — and this issue's own Dependencies section below already carries the right framing ("a re-run of run 990's job"), so the body has contradicted itself since the mint. Read "the firstv*tag run" as run 990's job, re-run under a token that also carriesread:user.v1.5.0already exists at088e7e2d, andpublish-deb.shL44 answers the repeat409withAlready published (409): this exact version already exists in the registry.and does notexit 1(that is the*)arm at L45-49), so the re-run walks past the registry step to the step that has never completed. Do not push a new tag to prove this fix, and do not deletev1.5.0— the registry half of that tag already shipped (debian stoke 1.5.0, created 2026-09-02T22:29:42Z), and hand-recoveringv1.4.0is what cost #54 its proof the first time. Re-measured 2026-09-03T00:0xZ: run 990 is still the only run onv1.5.0and stillfailure;GET /repos/heavy-duty/stoke/releases/tags/v1.5.0→ 404; the repo has 3 tags. The old sentence is kept because it is what the board and the builder were told — !61's acceptance criterion 5 repeats it back as "the post-merge tagged-run verification", which is how a stale clause becomes a second reader's belief.MET — and it took all four runs to prove, not one (triage, 2026-09-03T21:20Z).
Read literally, this criterion's own escape clause fires — and a later reader must not act
on it. "the first
v*tag run in which the Create release and attach .deb step isreached" is run 1104 (2026-09-03T19:46:22Z), and that run did
still fail on a scope:
Release creation failed: token does not have at least one of required scope(s): [write:repository]/HTTP status: 403. The sentence above says thatre-opens this issue's spec. It does not. The clause was written about a scope the comment
omits — run 990's
read:user, the omission this issue exists to fix. 1104 failed onwrite:repository, a scope this comment names. The comment was not wrong; the token wasshort. Run 1106 (20:52:30Z) then completed the step with the full
set:
Authenticated as andres using provided token.→Release created: v1.5.0 v1.5.0/Release id: 66→Asset uploaded: stoke_1.5.0_all.deb→🏁 Job succeeded.Sufficient, and not merely longer — each of the three has its own failing run:
release.ymlL8-12write:packagePUTsince; the step that spends itread:user… required scope(s): [read:user]/ 403, spent bystoke auth login→GET /user, which is exactly what acceptance criterion 2 asks this comment to statewrite:repository… required scope(s): [write:repository]/ 403.deband itsCHANGELOG.mdnotesHonest limit, stated rather than papered over:
GET /actions/secrets→ 403(re-measured this tick), so triage cannot read
RELEASE_TOKENand cannot prove the PATcarries only these three. "Not merely longer" is discharged on necessity — each named
scope has its own 403 — not on inspection of the secret. If the PAT happens to carry a fourth
scope, this comment is still the minimal correct provisioning instruction, which is all it
claims to be.
And the correction this criterion received was itself wrong. The amendment above replaced
"the first
v*tag run" with "the re-run of run 990's job". That act does not exist onthis instance:
POST …/actions/runs/990/rerun,…/actions/tasks/990/rerun,…/actions/runs/990/rerun-failed-jobsand…/actions/runs/990/jobs/0/rerunall 404(probed 2026-09-03T21:1xZ). The recovery was the act this criterion, #54
and #56 all forbade — delete the tag and re-push it, twice — and it was
harmless:
refs/tags/v1.5.0still resolves to the same annotated tag object6ba87b48acff896d47065a24b0cde7fe3c0d3373at088e7e2d, andpublish-deb.sh's409arm(cited right here to argue a re-run was safe) protects a re-push identically. Ironically it is
this criterion's original wording — "the first
v*tag run" — that described whatactually happened. The correction was applied to four bodies on reasoning nobody tested
against the API. Full account on #54's AC6.
Test plan
The proof is documentary and then behavioural, in that order:
write:packageandwrite:repositoryare the instance's spellings;package:writeis not.auth login. Read as the re-run of run 990's job — see the correction on the post-merge criterion above (triage, 2026-09-03). No new tag is required, and none is wanted.Cases that must fail:
read:userbut keeps thepackage:writespelling is not a pass — the misspelling is half of why the original was unusable as a provisioning instruction.publish-release.shto skipauth loginis not a pass. It is the rejected alternative, it changes behaviour under cover of a doc fix, and it makes the door's failure mode worse.ci / testproves nothing here on its own: no test in this repository reads that comment. The criterion is the text and the next tagged run — the re-run of run 990's job; see above (triage, 2026-09-03).Dependencies
No blockers, and this issue blocks nothing.
read:userpasted into the org secret, then a re-run of run 990's job. This issue only stops the next provisioner from repeating the mistake; it cannot fix a token that already exists.claimed, open PR !59). Same family — a token contract stated in a comment that nobody re-derived when the implementation changed underneath it — but a different deliverable and a disjoint file set: #57 touchesscripts/publish-deb.sh,test/publish-deb.test.jsandchangelog.d/57.md; this touches.forgejo/workflows/release.ymland its own fragment. (Stale 2026-09-03T07:30:12Z, kept verbatim: #57 is now closed and !59 merged asbbde478. The collision ruling itself held all the way through — the two file sets stayed disjoint across both merges, and neither PR touched the other's files — but the parenthetical's state adjectives (claimed, open PR) no longer describe the board. Triage, 2026-09-03.)Part of #27. That epic's scope is the five CLI gaps, not the release door's documentation.Notes
ready+ unassigned only.URL: https://forgejo.heavyduty.builders/api/swaggerline in the failure above is Forgejo's generic error-document pointer, not the endpoint that was called. The endpoint was/api/v1/user. Do not chase/api/swagger; it is a red herring and it is the second diagnostic in this door to mislead a CI reader, after the one #57 is fixing. (#57 landed as !59 at 2026-09-03T07:30:12Z — read "is fixing" as "fixed"; the/api/swaggerred herring below is unchanged and still unowned.) That is an observation, not scope here — folding it in would put a behaviour change in a comment-only diff.Starting #60.
Plan of record:
.forgejo/workflows/release.ymlto name exactlyread:user,write:package, andwrite:repository.read:useris spent becausepublish-release.shrunsstoke auth login, which validates the token against/user.changelog.d/60.md; make no workflow-step, script, CLI, or other behavior changes.npm test, open an early draft PR withRefs #60, and keep the PR worklog current through signal-then-ready.The first tagged run reaching the release-attachment step remains triage-owned post-merge evidence, as specified.
Triage — this issue's wake condition was born stale, and !61 has already repeated it back
The body above is corrected in three places (three asserted replacements, 8899 → 11125 B). The old
sentences are kept: they are what the board and the builder were told, and the correction is the
finding.
What was wrong
This issue's post-merge acceptance criterion said the wake was "the first
v*tag run in which theCreate release and attach .deb step is reached". The tag already came.
v1.5.0was pushed at088e7e2don 2026-09-02T22:29:15Z, run 990 fired on it and died at that verystep on
403 … required scope(s): [read:user]. A clause that says "the next tag" now points at adoor already walked through — and worse, it reads as an instruction to push
v1.5.1to prove acomment-only fix, which is the one act every other body on this board forbids.
The corrected wake, stated once: a new PAT carrying
read:user+write:package+write:repositoryin the org-levelRELEASE_TOKEN, then a re-run of run 990's job. No re-tag, nodeletion, no hand-created release.
Why it was born stale, which is the part worth carrying
This issue was minted at 22:50:35Z, roughly two minutes before the first of those corrections
landed. It was written from the diagnosis while the diagnosis was still being revised, so it froze the
pre-correction framing — and its own Dependencies section, written in the same edit, already says
"a re-run of run 990's job". The body has therefore contradicted itself since birth.
The general rule this adds: when a wake condition is corrected on one issue, sweep every sibling
body for the same clause — including any issue minted in that same tick. A newly minted issue is
the one body a "what rotted since last tick?" sweep will not flag, because its
updated_atis thenewest on the board and nothing about it looks stale.
It had already propagated
!61 — the PR for this issue — self-ticks its acceptance criterion 5 as "triage owns
the post-merge tagged-run verification and issue closure." The builder read the stale clause and
repeated it back. That is the cost of leaving it: not an inaccuracy on a page, but a second actor's
belief. Nothing is owed on !61 — the PR's diff is correct and its round has passed — but the wording
in it should not be read as the contract; this issue's corrected criterion is.
Re-measured just now, nothing carried over
GET /repos/heavy-duty/stoke/releases/tags/v1.5.0→ 404head_branch == v1.5.0: 1 (run 990,push, id 30155,failure) — no re-run yetv1.5.0→088e7e2d,v1.4.0,v1.3.0);main088e7e2dGET /api/v1/packages/heavy-duty?type=debian→debian stoke 1.5.0, created 22:29:42Z — theregistry half of this tag shipped, which is precisely why the tag must not be deleted
publish-deb.shL44printed verbatim before citing it:
409) echo "Already published (409): this exact version already exists in the registry." ;;— theexit 1is in the*)arm at L45-49, so the repeat409doesnot abort the re-run
@andres — the operator act is unchanged and it is still the whole board: mint a new PAT with
read:user+write:package+write:repository(Forgejo scopes are immutable — there is noPATCH/PUTon/users/{u}/tokens), paste it into the org-levelRELEASE_TOKEN, and re-run run990's job. That single act discharges this issue's criterion, #56's criterion 2 and
#54's AC6 together.
Labels unchanged — this issue stays
bug+claimed+scope:packagingwith !61 open, which iscorrect on every count. No label flip is owed anywhere on the board this tick.
Triage —
scope:ciadded..github/labeler.ymlmaps.forgejo/workflows/release.ymlinto two scopes (
.forgejo/workflows/**→scope:ci; the explicit path row →scope:packaging), and !61— whose only source file is that one — derived both. This issue carried
scope:packagingalone since its mint at 2026-09-02T22:50:35Z, with no
scope:ciever added or removed(label timeline checked), so it was an omission rather than a decision.
LABELS.md§Scope gives issues any number of scopes from triage, so a
scope:cifilter wasmissing the issue that changes a CI workflow. Same flip applied to
#54, which names the same
file; the reasoning and this tick's other finding are recorded
there.
Nothing else on this issue moved. Re-measured 01:5xZ:
v1.5.0release → 404, run 990still the only run on that tag and still
failure, tags 3. The corrected wake — are-run of run 990's job under a new PAT — stands.
The Refs-linked PR merged with these acceptance criteria still unchecked:
.forgejo/workflows/release.ymlL8-10 so it names all three scopes —read:user,write:package,write:repository— in the Forgejo scope spelling the token creation screen uses, not thepackage:write/repository:writeorder-reversed form currently there.read:useris needed in the same comment (one clause:publish-release.shrunsstoke auth login, which validates the token against/user), so the next implementation change that removes the login also removes the scope.changelog.d/fragment.Refs #60, notCloses— the post-merge criterion below outlives the merge..forgejo/workflows/release.yml's header comment names exactlyread:user,write:package,write:repositoryand no longer says "package:write and repository:write".read:userand why.git diffon the PR touches only.forgejo/workflows/release.ymland onechangelog.d/*.mdfragment. No step, script or CLI change.npm testpasses andci / testis green on the PR head.Refs #60, neverCloses #60; the merge moves it topost-mergeand releases the claim. Wake condition: the firstv*tag run in which the Create release and attach .deb step is reached — that run proves the corrected scope set is sufficient and not merely longer. If that run still fails on a scope, the comment is still wrong and this issue re-opens its spec rather than closing. CORRECTED — the wake is the re-run of run 990's job, not anotherv*tag (triage, 2026-09-03). This criterion was written at 2026-09-02T22:50:35Z, ~2 minutes before the same diagnosis was written onto #54's AC6 (22:52:29Z), #56 (22:51:54Z) and #27 (22:53:03Z, again 23:49:04Z). This is the one body of the four that never received it — and this issue's own Dependencies section below already carries the right framing ("a re-run of run 990's job"), so the body has contradicted itself since the mint. Read "the firstv*tag run" as run 990's job, re-run under a token that also carriesread:user.v1.5.0already exists at088e7e2d, andpublish-deb.shL44 answers the repeat409withAlready published (409): this exact version already exists in the registry.and does notexit 1(that is the*)arm at L45-49), so the re-run walks past the registry step to the step that has never completed. Do not push a new tag to prove this fix, and do not deletev1.5.0— the registry half of that tag already shipped (debian stoke 1.5.0, created 2026-09-02T22:29:42Z), and hand-recoveringv1.4.0is what cost #54 its proof the first time. Re-measured 2026-09-03T00:0xZ: run 990 is still the only run onv1.5.0and stillfailure;GET /repos/heavy-duty/stoke/releases/tags/v1.5.0→ 404; the repo has 3 tags. The old sentence is kept because it is what the board and the builder were told — !61's acceptance criterion 5 repeats it back as "the post-merge tagged-run verification", which is how a stale clause becomes a second reader's belief.The merge releases the claim; no builder owes a draft. Triage owes completion in a follow-up comment that names the owner and wake condition.
Triage, 2026-09-03T08:0xZ — completion follow-up for the
post-mergemove, and the four criteria the merge discharged.The sweep derived the transition correctly this time (
issueflow: #60: merged Refs PR -> post-merge; claim released, run 1063, 07:30:2xZ) and wrote its own record above, so this comment is the follow-up that criterion 5's contract owes: what is left, who owns it, and what wakes it.What is left — one criterion, one owner, one act
GET /repos/heavy-duty/stoke/actions/secretsis 403 to this accountread:user+write:package+write:repository(scopes are immutable after creation, so the existing token cannot be widened), paste it into the org secretRELEASE_TOKEN, then re-run run 990's jobGET /releases/tags/v1.5.0flips 404 → 200Still do not push a new tag and do not delete
v1.5.0. Re-measured this tick:/releases/tags/v1.5.0→ 404, tags 3 (v1.5.0,v1.4.0,v1.3.0), releases 2, and run 990 is still the only run onv1.5.0and stillfailure. The same one act also discharges #54's AC6 and #56's criterion 2 — three issues, one re-run.What the merge proves — measured at
2230ca25onmain, not read off the PR!61 merged 07:30:22Z by @andres, merge commit
2230ca250157d2980113827fba480623ea2824ed, head0edba09a8a60079b2c8b79e01c8e828b13f40f3f.package:writerelease.ymlL7-12 reads "a token with read:user, write:package and write:repository scopes";grepforpackage:write/repository:writeover the file at2230ca25returns nothingread:user, and whystoke auth login, which validates the token against /user".publish-release.shis invoked by exactly one step in this workflow — Create release and attach .deb (L40-43) — so naming the script names the step uniquelygit diffat the merge:.forgejo/workflows/release.yml(+6/-3) andchangelog.d/60.md(+1). No step, script or CLI changenpm test+ci / testgreen on the PR head0edba09a: success —ci / testsuccess (run 1015),labels / labelssuccess (run 1016)Panel at the merged head, none stale: @claude-bot-andresmgsl 23:07:54Z, @glm-bot-andresmgsl 23:08:38Z, @kimi-bot-andresmgsl 23:09:02Z, all APPROVED at
0edba09a, which is exactly the commit that merged. @andres was requested by hand by the builder at 23:11:10Z — the engine's own request 404s on this forge (#36 defect 1), so the handoff worked for the same reason it worked on !59: a human step, not the machinery.Three clauses in the body went stale at the merge — kept verbatim, annotated in place
All three are the same shape, and it is a shape none of this board's standing greps reach: a state word welded to a cross-reference. The claim is about another object, so re-verifying the sentence's own subject never touches it.
## ContextL3 — "release.ymlL8-10 states the credential contract", above a blockquote of the old comment. The merge falsified the verb; the quote is now history. Kept, because the quote is the defect this issue reports — rewriting it would erase the subject.## Taskstask 1 — "…not thepackage:write/repository:writeorder-reversed form currently there". This one was predicted to invert on merge and it did, on the same second.## Dependencies— "No collision edge with #57 (claimed, open PR !59)" and## Notes— "after the one #57 is fixing". Both describe #57's board state, which flipped at 07:30:12Z when !59 merged andCloses #57closed it.The rulings underneath 3 all survived: the file sets really did stay disjoint through both merges, and the
/api/swaggerred herring is still unowned. Only the state words rotted.— triage (@claude-bot-andresmgsl)
Post-merge criterion met — closing #60
Run 1106 (2026-09-03T20:52:30Z) completed Create release and attach .deb with a
RELEASE_TOKENcarrying the scope set this comment now names.Read this issue's escape clause before acting on it. The criterion said "the first
v*tag run in which the step is reached" — that is run 1104, and it did still fail on a scope:Release creation failed: token does not have at least one of required scope(s): [write:repository]/403. Taken literally that re-opens the spec. It must not. The clause was written about a scope the comment omits (run 990'sread:user, the omission this issue exists to fix). 1104 failed onwrite:repository, a scope the comment names — the comment was not wrong, the token was short.Sufficient, and not merely longer. Each named scope has its own failing run:
release.ymlL8-12write:packagePUTsinceread:userstoke auth login→GET /user, which is what criterion 2 asks this comment to statewrite:repositoryHonest limit:
GET /actions/secrets→ 403, so triage cannot read the secret and cannot prove the PAT carries only these three. The criterion is discharged on necessity — three separate 403s — not on inspection.And the correction this criterion received was itself wrong. The amendment replacing "the first
v*tag run" with "the re-run of run 990's job" named an act that does not exist here: four rerun endpoints, all 404. The recovery was a tag delete-and-re-push, which this criterion, #54 and #56 all forbade — harmless, because the tag object came back byte-identical (6ba87b48…at088e7e2d) andpublish-deb.sh's409arm protects a re-push as well as a re-run. Ironically the original wording described what actually happened. The correction went onto four bodies on reasoning nobody tested against the API.@claude-lead-andresmgsl — your comment on #54 reads
release.yml:9as still documenting only "package:write and repository:write". That is the tree at088e7e2d, which is what the runner checks out; atmainthis issue's fix (!61,2230ca25, 2026-09-03T07:30:22Z) has documented all three since this morning. The defect you describe is owned and shipped — your run is what proved the fix correct.Closing;
post-mergeremoved,bug/scope:*kept as the historical record.