bug(tokens): the documented release PAT scope omits write:issue — a write:repository-only token fails the rolling refresh on a REQUEST_CHANGES deadlock #1484
Labels
No labels
bump
major
bump
minor
bump
patch
kind
bug
kind
chore
kind
docs
kind
epic
kind
feature
loop/paused
mirror-drift
priority
critical
priority
high
priority
low
priority
medium
size
L
size
M
size
S
size
XL
status
blocked
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit#1484
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?
The docs tell adopters to mint the release PAT with only
write:repository, and claim it "covers the manifest push + the PR-create/comment paths" (docs/integration.mddecision matrix, hazard note, bot provisioning step 3, at-a-glance table, path-α step 2;docs/operations.mdrotation step 1; arc42 §8.8;cmd/rt/credentials.gotier-3 output). On Forgejo, a PR comment is an issue-scope route, andwrite:repositorycarries no issue bits.Evidence
Source, Forgejo v15.0.7 — the version this instance reports (
GET /api/v1/version→15.0.7+gitea-1.22.0):The check runs before the handler (
permissions/token_requires_scopes.go), so the refusal is403 token does not have at least one of required scope(s): [read:issue]regardless of collaborator rights. Gitea v1.24.0 groups the same routes the same way.Live read-back:
release-botposted pulls/952#issuecomment-101227 (2026-08-26, thert post-cutstale-rolling-PR note). That POST passes only the issue-scope check, so release-bot's PAT carried issue scope as of that date — which is why no frankenbit adopter has hit this. Not graded: a throwawaywrite:repository-only token was not minted; the claim rests on source + this read-back.What a
write:repository-only PAT does todayinternal/prep/pr.go:100)GET /issues/{n}/comments— read:issuecmd/rt/prep.go:284returns it, so the rolling refresh fails whenever a liveREQUEST_CHANGESblocks the rolling PRpr.go:316)POST /issues/{n}/comments— write:issueclose_stale_rolling_pr.go:88)PATCH /issues/{n},POST .../comments— write:issuepreflight_push_whitelist.go:190)GET /user— read:user (tier 2 only)Not on a PAT path:
manifest-pr(path γ, auto token),ac-closure-check(GITHUB_TOKEN),setup-bump-labels/review-eligibility(operator's own token).Also stale
docs/integration.md(recover-pending-cut section) saysreusable-recover-pending-cut.yml"is not yet ininternal/bake/marker.go's canonical file set … until release-toolkit#1173 lands". #1173 landed and first shipped in v0.58.0; v0.57.3/v0.57.4 still bake'v0.57.0'for that workflow.Acceptance criteria
write:repository+write:issue(+read:useron tier 2; the tier-3 API PAT needswrite:repository+write:issue), with a table of which release-path call needs which scope and what fails without it — DONE @ #1488 (dc9df38).docs/integration.md: decision matrix, hazard-note scope table (scope / call / what fails without it), failure-mode entry, bot provisioning step 3, at-a-glance table, split-pair bound and path-α step 2. Alsodocs/operations.mdrotation step 1, arc42 §8.8, thecli-surface.mdcontract row and theinternal/credentialspackage comment.29b541bfixes the section's anchor (#tokens-permissions).rt credentials --check --probe-scopesmakes one read-only issue-scope read with the API credential and exits 1 when the forge refuses it (401/403); a refused tier-2GET /userWARNs and does not refuse; an unanswered read is UNGRADED, not a pass and not a refusal; its PASS names that a read cannot showwrite:issue— DONE @ #1488 (dc9df38).probeScopesincmd/rt/credentials.go, withTestCredentialsProbe_*arms incmd/rt/credentials_test.gocovering: the tier-2 refusal (rc=1); a 401 named as a dead token, not a scope; a tier-2GET /userrefusal WARNing at rc=0; an unanswered read UNGRADED and not PASS; tier 1 skipped; and the PASS disclosure that a read cannot showwrite:issue. Client side:internal/forgejo/probe_issue_scope_test.go.reusable-release.yml(and its.giteatwin) runs the probe in the credential step, before any cut work, and a bats arm pins that wiring — DONE @ #1488 (dc9df38). The credential step, which still precedesrt decide, passes--probe-scopes --owner --repoandFORGEJO_BASE_URL. The.giteatwin was regenerated (gitea-twin --checkgreen). The bats arm isworkflows: the credential grade probes the API credential's issue scope (#1484).prober(pair.Git), the non-array guard removed) and each turned an arm red. The bats wiring arm went red when--probe-scopeswas removed. Review 7143 independently re-ran the three, plus an extra 404 mutation, against the code rather than trusting the claim.dc9df38). The recover-pending-cut section now names v0.58.0 as the floor. It explains that v0.57.3 and v0.57.4 contain the fold/restore change but bake'v0.57.0', so they run the older recovery binary.release-bot's CURRENT PAT is confirmed to pass the probe before the re-pin that ships it — the read-back above is from 2026-08-26, and a rotation since then without issue scope would redden the toolkit's own next release at the credential step — DONE 2026-10-02, before #1488 merged. The token then in use was refusedGET /user(403 … [read:user]) and was re-minted. The new token answeredGET /userwithHTTP 200asrelease-bot(comment 113683) andGET .../labels?limit=1withHTTP 200(comment 113688). The operator confirmed the org-levelRELEASE_TOOLKIT_TOKENsecret holds it. Toolkit-self pins@main, so the merge itself ships the probe; there is no separate re-pin.Leads, not claims
permissions/repo_access.goresolves permissionsWithReducer(ctx.Reducer()), which suggests Forgejo v15 may support repo-restricted tokens. If so, the "each scope is a whole API category, so the split bounds the API surface only as far as the owning account's reach" sentence in the split-pair section is stale for Forgejo. Unverified.ClosePRusesPATCH /issues/{n};PATCH /pulls/{n}withstate: closedwould need only repository scope. The comments would still need issue scope, so it does not remove the requirement.claude referenced this issue2026-10-02 09:56:39 +02:00
AC6 read-back, 2026-10-02. These are
release-bottoken reads run by the operator. I quote the responses only; no token value is involved.GET /api/v1/user: This is the live confirmation of the/user→read:userclaim. It also means that, on tier-2 cuts using that token, the #260 push-whitelist preflight could not resolve the push identity, so it WARNed and proceeded instead of grading (cmd/rt/preflight_push_whitelist.go:190-193). That is inferred from source plus this read-back; the run logs were not read.GET /api/v1/user:HTTP 200,"login":"release-bot". Forgejo v15.0.7 has no route to add a scope to an existing token (web: new/delete/regenerate; API: create/delete), so this is presumably a newly minted token.release-botshowslast_login2026-10-02T10:26:48+02:00.AC6 stays open: the issue-scope read on the current token,
GET /api/v1/repos/frankenbit/release-toolkit/labels?limit=1, has not been quoted yet. That is the readrt credentials --probe-scopesrefuses on. If the token was re-minted, also confirm the org-levelRELEASE_TOOLKIT_TOKENsecret holds the new token, because that is what the first push tomainafter #1488 merges will probe.AC6 issue-scope read-back, 2026-10-02. This is the same
release-bottoken that answeredGET /userwithHTTP 200above. It was run by the operator; I quote the response only.GET /api/v1/repos/frankenbit/release-toolkit/labels?limit=1→HTTP 200, body a JSON array beginning[{"id":276,"name":"bump/major",…}. So the forge let the token through the issue-scope check.rt credentials --check --probe-scopesrefuses on, so this token passes the probe:issue scope: PASS.GET /user200 above,user scope: PASStoo.read:issue, notwrite:issue. The 2026-08-26 comment POST is the only write-level evidence, and it was made with an earlier token.One item left for AC6: confirm that the org-level
RELEASE_TOOLKIT_TOKENActions secret holds this token. The first push tomainafter #1488 merges probes the secret, not the operator's shell.