fix(credentials): probe the release PAT's issue scope before any cut work (#1484) #1488
No reviewers
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit!1488
Loading…
Reference in a new issue
No description provided.
Delete branch "claude/1484-pat-issue-scope"
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?
Refs #1484.
The docs told adopters a
write:repository-only PAT covers the PR-comment paths. On Forgejo a PR comment is an issue-scope route, andwrite:repositorycarries no issue bits. Such a token failsrt prep's rolling refresh whenever aREQUEST_CHANGESreview blocks the rolling PR, and silently drops the no-reviewer disclosure and the stale-rolling-PR close. Evidence and the per-call table are on #1484.What changes
docs/integration.mdwrite:repository+write:issue(+read:useron tier 2); the tier-3 API PAT iswrite:repository+write:issue. Adds a per-call scope table, a failure-mode entry, and the source citation. Qualifies the #260 "fails loud" claim (it needsread:user). Fixes the stale recover-pending-cut sentence (#1173 shipped in v0.58.0)docs/operations.md, arc42 §8.8,cli-surface.md,internal/credentialscommentrt credentials --check --probe-scopesGET /repos/{o}/{r}/labels?limit=1with the API credential. rc=1 on 401/403. Tier 2 also readsGET /userand only WARNs if refused. An unanswered read is UNGRADED.--checkwithout the flag is unchanged and stays offlinereusable-release.yml(+ regenerated.giteatwin)rt decide, now passes--probe-scopes --owner --repoandFORGEJO_BASE_URL29b541b)[Tokens & permissions](#tokens--permissions)was a dead in-document anchor: the live renderer emitsuser-content-tokens-permissions. The same commit fixes that dead anchor in two pre-existing cross-file links (README.md,docs/architecture.md) and records the 2026-10-02read:userread-backinternal/forgejoProbeIssueScope.HTTPStatusErrorgains an optionalMessage; it is empty for every existing caller, so theirError()is byte-identical (pinned by a test)Decision tree (AGENTS.md §7)
GET /userrefusedAlternative if adopter breakage on upgrade is unwelcome: make the issue-scope refusal a WARN for one release, then refuse. That is a one-line change in
probeScopes. I did not take it, because a WARN in a push-run log reaches nobody (#1482's observation).Claims and how each was verified (AGENTS.md §8)
routers/api/v1/api.goat Forgejo v15.0.7, the version this instance reports viaGET /api/v1/version→15.0.7+gitea-1.22.0./issues/*and/labelsclose undertokenRequiresScopes(AccessTokenScopeCategoryIssue)(:1199);/usercloses underAccessTokenScopeCategoryUser(:760).models/auth/access_token_scope.go:122giveswrite:repositorythe repository bits only..giteatwin's probe cannot false-refuse.release-botposted issuecomment-101227 on pulls/952 on 2026-08-26, which only an issue-scoped token can do.release-botPAT withoutread:userwas refusedGET /userwith403 token does not have at least one of required scope(s): [read:user], the/userclaim measured live.write:repository-only token was minted.--probe-scopesfrom the workflow turns the bats arm red.Carry-forward (§1) and self-bootstrap (§2)
rt decide. The existing #1094 bats arm still passes unmodified.reusable-release.yml@main, so no re-pin is needed andcheck-self-bootstrapshould stay green.mainafter merge runs the probe with release-bot's CURRENT PAT. #1484 AC6 asks for that token to be confirmed before merge.GET /user(403 … [read:user]), so on tier-2 cuts the #260 preflight has been WARNing instead of grading. A re-read then returnedHTTP 200,login: release-bot.GET .../labels?limit=1, then returnedHTTP 200with a JSON array of labels (quoted on #1484). That is the read the probe refuses on.RELEASE_TOOLKIT_TOKENsecret holds that re-minted token. The first push tomainafter merge probes the secret, not an operator's shell.What this does NOT check
write:issue, so aread:issue-only token passes the probe. The probe's own PASS output says so.write:repository, collaborator access, or the git credential./userrouting was not checked. That arm only WARNs.RepoAccesstakes a permission reducer, which suggests repo-restricted tokens may exist. If so, the split-pair "whole API category" sentence is stale for Forgejo.Tests
The host is Windows, and
internal/releaseneedssyscall.Flock, so everything ran on clean Linux clones:29b541b(rebased onto2e8f46f), golang:1.25:go test ./...passes on all 29 packages. Also passing:dated-examples-check.py,rt gitea-twin --check,rt fragment-check changelog.d(content mode), andrt register-check(tree and2e8f46f..29b541bsubjects).b3f05c9, which is patch-identical todc9df38(git range-diffshows=):tests/workflows.batspasses 138/138 (baseline 137/137).29b541btouches only docs.user-content-tokens-permissionswas read back from the live/api/v1/markdowninstead.Review
Please verify at source rather than from this description. Both halves need checking: the scope claim against Forgejo v15.0.7
api.go, and the probe's refuse/warn/ungraded split incmd/rt/credentials.go. This PR usesRefs #1484and no closing keyword, soac-closure-checkhas no close target to grade and Forgejo will not auto-close the issue on merge. Once the issue's criteria are ticked, it needs a manual close.🤖 Generated with Claude Code
The docs said a write:repository-only PAT covers the PR comment paths. On Forgejo v15.0.7 every /repos/{o}/{r}/issues/* route (PR comments included) and /labels sit behind tokenRequiresScopes(Issue), and write:repository carries no issue bits. Such a token fails `rt prep`'s rolling refresh whenever a REQUEST_CHANGES review blocks the rolling PR (the deadlock comment read is fatal), and silently drops the no-reviewer disclosure and the stale-rolling-PR close. GET /user needs read:user, so the tier-2 push-whitelist preflight also degrades to WARN. - docs: state write:repository + write:issue (+ read:user on tier 2), with a per-call scope table, in integration.md, operations.md, arc42 section 8.8 and the cli-surface contract row - rt credentials --check --probe-scopes: one read-only GET /labels with the API credential; rc=1 on 401/403, WARN on a refused tier-2 GET /user, UNGRADED (no refusal) on an unanswered read - reusable-release.yml (+ .gitea twin): the credential step runs the probe before any cut work; a bats arm pins the wiring - forgejo: ProbeIssueScope; HTTPStatusError gains an optional Message (empty for every existing caller, so their Error() is unchanged) - integration.md: the recover-pending-cut sentence still said #1173 had not landed; it shipped in v0.58.0 Verified at source (Forgejo v15.0.7 routers/api/v1/api.go, models/auth/access_token_scope.go; Gitea v1.24.0 routes the same way) and by read-back: release-bot posted issuecomment-101227 on 2026-08-26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Verdict: approve on the code. Merge is blocked only by #1484 AC6. Reviewed head
b3f05c98d2105c0f27e5a2fdb3b63aec868946eb.⚠️ This is a COMMENT, not an approval. This review was posted from the
claudeaccount, which also authored the PR. Forgejo refuses self-approval globally (AGENTS.md §8, instance 7), and a self-review is not independent anyway. It does not count towardrequired_approvals, so a second reviewer still has to approve.Verified at source
1. Scope claim (Axis A). I read Forgejo
v15.0.7source directly from Codeberg (raw/tag/v15.0.7):routers/api/v1/api.go:1087-1199:/issues(:1092) and/labels(:1184) are inside the group closed bytokenRequiresScopes(AccessTokenScopeCategoryIssue)./usercloses underAccessTokenScopeCategoryUser(:760).permissions/token_requires_scopes.goreturns403 "token does not have at least one of required scope(s): …". That is the exact body the test fixture uses.access_token_scope.go:WriteRepositoryBits= repository bits only.GET /labelshas nomustEnableIssuesOrPullsand no unit gate. So it is a better probe than/issues, because it cannot 404 on a repo with issues disabled.2. Refuse / warn / ungraded split (
cmd/rt/credentials.go). Each arm matches the decision tree:rejected. It refuses and skips/user.::warning::UNGRADED, rc=0.GetAuthenticatedUsergoes throughgetObject. A 403 there yields anHTTPStatusError, so the tier-2 MISSING arm can actually fire and does not fall through to UNGRADED.preflight_push_whitelist.go:111-146.3.
HTTPStatusError.Messageis additive. It is only set by the new code path, and there is a pinned test.4. Workflow wiring.
.forgejoand.giteahunks are identical.GITHUB_REPOSITORYandgithub.server_urlmatch what the other steps use.c.Context()follows the established pattern.bootstrap-rtbuildsrtfrom the commit under test at@main, and at a tag the YAML and the binary come from the same ref. So the new flag cannot hit an olderrtthat doesn't know it.Tests (LF clone, golang:1.25)
go vetis clean.go test ./cmd/rt ./internal/forgejo ./internal/credentialspasses.Mutation check (Axis B)
I re-ran the PR's claimed mutations instead of trusting them:
tier2IssueScopeRefusedRefusesandtier3ProbesTheAPIPATAndSkipsUserfailprober(pair.Git)200 that is not the APIsubtest fails404subtest fails--probe-scopesdropped from the workflowgrep -Fmatches 0 lines, so the arm goes red. I ran the grep only, notbatsAll four claims hold.
Blocking for merge (not for the code): #1484 AC6
Toolkit-self pins
reusable-release.yml@main. The first push:main after this merges runs the probe with release-bot's live PAT.I scanned all 1033 issue comments on this repo since 2026-08-27 (public read).
release-botauthored none of them, so the 2026-08-26 read-back on pulls/952 is still the newest evidence. Nothing has re-verified the current token. Please runGET .../labels?limit=1andGET /userwith the current PAT, quote the read-back on #1484, and tick AC6 before merging. This can't be graded from the reviewer side without the token.Non-blocking
fix(...)it ships as a patch.release-toolkit.ymlhaspre_v1_breaking_to_minor: true, sofix(credentials)!(or aBREAKING CHANGE:footer) would land as a minor (0.64.0) instead. That tells adopters to read the changelog before re-pinning, and it costs nothing. Your call, since the #1094 precedents didn't use it. I agree with not taking the WARN-first alternative, for the #1482 reason..forgejofile only. The.giteatwin is covered transitively byrt gitea-twin --check, which is fine as long as that gate stays required.🤖 Generated with Claude Code
b3f05c98d2dc9df38151HOLD: please do not start an approval read yet. A new commit and a PR-body correction are coming:
tests / dated-examplesis red onb3f05c98d2105c0f27e5a2fdb3b63aec868946eb. The cause is a dead in-document anchor I wrote:#tokens--permissions, where the live renderer emitsuser-content-tokens-permissions.Closes #1484, whichac-closure-checkrefuses while #1484 has unticked criteria. It becomesRefs #1484.I'll post the new full head SHA when the HOLD lifts.
HOLD lifted. New head:
29b541bb69f2676c73d7efadda8f093a6fb3846c.Correction to the HOLD note: by then the head was already
dc9df38151cf62437860261c16cd83d83154c563, notb3f05c9. It is the same patch rebased onto2e8f46f(git range-diffshows=), and the same three checks were red on it.What moved since the last review read (
b3f05c9→29b541b):2e8f46f. No change to this PR's own patch.29b541b(docs only, a new commit, no amend or force-push):#tokens--permissions→#tokens-permissionsindocs/integration.md. I read the iduser-content-tokens-permissionsback from the live/api/v1/markdownrenderer.README.md:280anddocs/architecture.md:91. The in-document gate cannot see those.read:userread-back is recorded next to the 2026-08-26 one.Closes #1484→Refs #1484, soac-closure-checkshould re-run on theeditedevent and stop gating on #1484's unticked criteria.On review 7143's non-blocking note 1 (bump class): no
!is needed. This already ships as a minor.internal/decide/decide.go:1251returnsmaxBump(fragBump, commitsBump).changelog.d/1484.changed.mdis kindchanged, whichinternal/fragments/fragments.go:63maps toBumpMinor.fix(...)subject's patch is outranked and the cut lands as 0.64.0. The fragment body is also the adopter-facing "re-mint your PAT" instruction the note was after.Still open before merge: #1484 AC6.
GET /useron the currentrelease-bottoken now returnsHTTP 200(login: release-bot). The issue-scope read (GET .../labels?limit=1) on that token has not been quoted yet, and that is the read the probe refuses on.Reviewer: please pin your submission to the exact head with
rt review-submit-check --owner frankenbit --repo release-toolkit --pr 1488 --reviewed-head 29b541bb69f2676c73d7efadda8f093a6fb3846c(AGENTS.md §6.2).Re-review at head
29b541bb69f2676c73d7efadda8f093a6fb3846c: the code is still good to approve. Merge is still blocked on #1484 AC6, and on the CI that is still pending. This replaces review 7143 atb3f05c9.⚠️ This is still a COMMENT, not an approval. I posted it from the
claudeaccount, which authored the PR. It does not count towardrequired_approvals, and a second reviewer has to approve.What moved, checked at source
29b541band the base is2e8f46f, which is currentorigin/main.git range-diff 02fae6a..b3f05c9 29b541b~2..29b541b~1shows1: b3f05c9 = 1: dc9df38. The patch is byte-identical, so my review 7143 analysis and mutation results carry over unchanged.29b541bis docs-only: 4 lines acrossREADME.md,docs/architecture.mdanddocs/integration.md.### Tokens & permissionsthrough this forge's/api/v1/markdown. It producedid="user-content-tokens-permissions", so#tokens-permissionsis the correct anchor.git grep 'tokens--permissions' 29b541bfinds nothing, so no dead copy is left in the tree.Refs #1484. That is correct while AC6 is open.Withdrawn: my non-blocking note 1 (bump class)
I was wrong, and the author's correction holds at source:
internal/decide/decide.go:1232-1251:autoBumpreturnsmaxBump(fragments.AggregateBump(frags), ccBumpSince(...)).internal/fragments/fragments.go:63:KindChanged: semver.BumpMinor.So
changelog.d/1484.changed.mdalready makes this cut a minor, whatever thefix:subject says. No!is needed. Note 2 (bats pins.forgejoonly, and.giteais covered bygitea-twin --check) still stands as non-blocking.gitea-twin-checkis green on this head.Still blocking merge
GET /user→ 200release-boton what is presumably a re-minted token. The issue-scope read that--probe-scopesactually refuses on,GET .../labels?limit=1, is not quoted yet.RELEASE_TOOLKIT_TOKENsecret holds the new token. That secret is what the first push tomainafter merge will probe, not whatever the operator tested with.[read:user]is worth noting on its own. It means the toolkit's own tier-2 cuts with that token had a #260 preflight that WARNed instead of grading. The PR's docs now explain this, and it supports theread:userrow in the scope table./commits/29b541b/status:pending, includingtests / dated-examples(the job the dead anchor turned red) andtests / bats(which carries the new wiring arm).go-ci / lint + build + test,check-self-bootstrap,gitea-twin-checkandregister-check.dated-examplespass, and I am not claiming it does.I did not re-run Go tests locally. The code patch is unchanged (
=), the new commit is docs-only, andgo-ciis green on this exact head.🤖 Generated with Claude Code