fix(credentials): probe the release PAT's issue scope before any cut work (#1484) #1488

Merged
claude merged 2 commits from claude/1484-pat-issue-scope into main 2026-10-02 10:52:40 +02:00
Member

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, 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, and silently drops the no-reviewer disclosure and the stale-rolling-PR close. Evidence and the per-call table are on #1484.

What changes

Surface Change
docs/integration.md Minimum scope is write:repository + write:issue (+ read:user on tier 2); the tier-3 API PAT is write:repository + write:issue. Adds a per-call scope table, a failure-mode entry, and the source citation. Qualifies the #260 "fails loud" claim (it needs read:user). Fixes the stale recover-pending-cut sentence (#1173 shipped in v0.58.0)
docs/operations.md, arc42 §8.8, cli-surface.md, internal/credentials comment Same scope statement
rt credentials --check --probe-scopes One read-only GET /repos/{o}/{r}/labels?limit=1 with the API credential. rc=1 on 401/403. Tier 2 also reads GET /user and only WARNs if refused. An unanswered read is UNGRADED. --check without the flag is unchanged and stays offline
reusable-release.yml (+ regenerated .gitea twin) The existing credential step, which already runs before rt decide, now passes --probe-scopes --owner --repo and FORGEJO_BASE_URL
Anchor fix (29b541b) [Tokens & permissions](#tokens--permissions) was a dead in-document anchor: the live renderer emits user-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-02 read:user read-back
internal/forgejo ProbeIssueScope. HTTPStatusError gains an optional Message; it is empty for every existing caller, so their Error() is byte-identical (pinned by a test)

Decision tree (AGENTS.md §7)

Probe outcome Disposition Why
Issue read refused (401/403) refuse, rc=1 Without issue scope the deadlock path is FATAL mid-refresh. A red build before any cut work is cheaper
Tier-2 GET /user refused WARN, rc=0 The push-whitelist preflight itself WARNs + proceeds on an unresolvable identity. Refusing here would make the probe stricter than the gate it protects
Read not answered (transport, 5xx, 404, a non-API 200) UNGRADED, rc=0 It answers nothing about scope. This follows the push-whitelist preflight's rule for an unreadable forge, and the next step hits the same API anyway
Tier 1 skipped No adopter-minted PAT, so no adopter-chosen scope to grade

Alternative 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)

  • Axis A, source:
    • routers/api/v1/api.go at Forgejo v15.0.7, the version this instance reports via GET /api/v1/version → 15.0.7+gitea-1.22.0. /issues/* and /labels close under tokenRequiresScopes(AccessTokenScopeCategoryIssue) (:1199); /user closes under AccessTokenScopeCategoryUser (:760).
    • models/auth/access_token_scope.go:122 gives write:repository the repository bits only.
    • Gitea v1.24.0 groups the same routes the same way, so the .gitea twin's probe cannot false-refuse.
  • Axis A, read-back:
    • release-bot posted issuecomment-101227 on pulls/952 on 2026-08-26, which only an issue-scoped token can do.
    • On 2026-10-02 a release-bot PAT without read:user was refused GET /user with 403 token does not have at least one of required scope(s): [read:user], the /user claim measured live.
    • Not graded: no throwaway write:repository-only token was minted.
  • Axis B, mutation-verified:
    • A 403 that does not refuse turns 2 arms red.
    • A probe built with the git credential turns the tier-3 arm red.
    • Dropping the non-API-200 guard turns the client arm red.
    • Removing --probe-scopes from the workflow turns the bats arm red.
  • Axis C: after merge, verify by reading the job log of the credential step, not the run status (see below).

Carry-forward (§1) and self-bootstrap (§2)

  • §1: no predecessor reusable is replaced. The credential step's existing behaviour is preserved verbatim: rc=2 on a half-configured pair, and the step still precedes rt decide. The existing #1094 bats arm still passes unmodified.
  • §2: toolkit-self pins reusable-release.yml@main, so no re-pin is needed and check-self-bootstrap should stay green.
    • The first push to main after merge runs the probe with release-bot's CURRENT PAT. #1484 AC6 asks for that token to be confirmed before merge.
    • Status 2026-10-02: the PAT then in use was refused GET /user (403 … [read:user]), so on tier-2 cuts the #260 preflight has been WARNing instead of grading. A re-read then returned HTTP 200, login: release-bot.
    • The issue-scope read on that token, GET .../labels?limit=1, then returned HTTP 200 with a JSON array of labels (quoted on #1484). That is the read the probe refuses on.
    • Still to confirm: the org-level RELEASE_TOOLKIT_TOKEN secret holds that re-minted token. The first push to main after merge probes the secret, not an operator's shell.

What this does NOT check

  • A read cannot show write:issue, so a read:issue-only token passes the probe. The probe's own PASS output says so.
  • It does not grade write:repository, collaborator access, or the git credential.
  • Gitea's /user routing was not checked. That arm only WARNs.
  • Lead, not claim (on #1484): v15's RepoAccess takes 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/release needs syscall.Flock, so everything ran on clean Linux clones:

  • At head 29b541b (rebased onto 2e8f46f), 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), and rt register-check (tree and 2e8f46f..29b541b subjects).
  • At b3f05c9, which is patch-identical to dc9df38 (git range-diff shows =): tests/workflows.bats passes 138/138 (baseline 137/137). 29b541b touches only docs.
  • Not run locally: the in-document anchor gate (#1451), which needs the authenticated renderer. The fixed id user-content-tokens-permissions was read back from the live /api/v1/markdown instead.

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 in cmd/rt/credentials.go. This PR uses Refs #1484 and no closing keyword, so ac-closure-check has 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

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, 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, and silently drops the no-reviewer disclosure and the stale-rolling-PR close. Evidence and the per-call table are on #1484. ## What changes | Surface | Change | |---|---| | `docs/integration.md` | Minimum scope is `write:repository` + `write:issue` (+ `read:user` on tier 2); the tier-3 API PAT is `write:repository` + `write:issue`. Adds a per-call scope table, a failure-mode entry, and the source citation. Qualifies the #260 "fails loud" claim (it needs `read:user`). Fixes the stale recover-pending-cut sentence (#1173 shipped in v0.58.0) | | `docs/operations.md`, arc42 §8.8, `cli-surface.md`, `internal/credentials` comment | Same scope statement | | `rt credentials --check --probe-scopes` | One read-only `GET /repos/{o}/{r}/labels?limit=1` with the API credential. rc=1 on 401/403. Tier 2 also reads `GET /user` and only WARNs if refused. An unanswered read is UNGRADED. `--check` without the flag is unchanged and stays offline | | `reusable-release.yml` (+ regenerated `.gitea` twin) | The existing credential step, which already runs before `rt decide`, now passes `--probe-scopes --owner --repo` and `FORGEJO_BASE_URL` | | Anchor fix (`29b541b`) | `[Tokens & permissions](#tokens--permissions)` was a dead in-document anchor: the live renderer emits `user-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-02 `read:user` read-back | | `internal/forgejo` | `ProbeIssueScope`. `HTTPStatusError` gains an optional `Message`; it is empty for every existing caller, so their `Error()` is byte-identical (pinned by a test) | ## Decision tree (AGENTS.md §7) | Probe outcome | Disposition | Why | |---|---|---| | Issue read refused (401/403) | **refuse, rc=1** | Without issue scope the deadlock path is FATAL mid-refresh. A red build before any cut work is cheaper | | Tier-2 `GET /user` refused | **WARN, rc=0** | The push-whitelist preflight itself WARNs + proceeds on an unresolvable identity. Refusing here would make the probe stricter than the gate it protects | | Read not answered (transport, 5xx, 404, a non-API 200) | **UNGRADED, rc=0** | It answers nothing about scope. This follows the push-whitelist preflight's rule for an unreadable forge, and the next step hits the same API anyway | | Tier 1 | **skipped** | No adopter-minted PAT, so no adopter-chosen scope to grade | **Alternative 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) - **Axis A, source:** - `routers/api/v1/api.go` at Forgejo v15.0.7, the version this instance reports via `GET /api/v1/version` → `15.0.7+gitea-1.22.0`. `/issues/*` and `/labels` close under `tokenRequiresScopes(AccessTokenScopeCategoryIssue)` (:1199); `/user` closes under `AccessTokenScopeCategoryUser` (:760). - `models/auth/access_token_scope.go:122` gives `write:repository` the repository bits only. - Gitea v1.24.0 groups the same routes the same way, so the `.gitea` twin's probe cannot false-refuse. - **Axis A, read-back:** - `release-bot` posted issuecomment-101227 on pulls/952 on 2026-08-26, which only an issue-scoped token can do. - On 2026-10-02 a `release-bot` PAT without `read:user` was refused `GET /user` with `403 token does not have at least one of required scope(s): [read:user]`, the `/user` claim measured live. - **Not graded:** no throwaway `write:repository`-only token was minted. - **Axis B, mutation-verified:** - A 403 that does not refuse turns 2 arms red. - A probe built with the git credential turns the tier-3 arm red. - Dropping the non-API-200 guard turns the client arm red. - Removing `--probe-scopes` from the workflow turns the bats arm red. - **Axis C:** after merge, verify by reading the job log of the credential step, not the run status (see below). ## Carry-forward (§1) and self-bootstrap (§2) - **§1:** no predecessor reusable is replaced. The credential step's existing behaviour is preserved verbatim: rc=2 on a half-configured pair, and the step still precedes `rt decide`. The existing #1094 bats arm still passes unmodified. - **§2:** toolkit-self pins `reusable-release.yml@main`, so no re-pin is needed and `check-self-bootstrap` should stay green. - **The first push to `main` after merge runs the probe with release-bot's CURRENT PAT.** #1484 AC6 asks for that token to be confirmed before merge. - Status 2026-10-02: the PAT then in use was refused `GET /user` (`403 … [read:user]`), so on tier-2 cuts the #260 preflight has been WARNing instead of grading. A re-read then returned `HTTP 200`, `login: release-bot`. - The issue-scope read on that token, `GET .../labels?limit=1`, then returned `HTTP 200` with a JSON array of labels (quoted on #1484). That is the read the probe refuses on. - **Still to confirm:** the org-level `RELEASE_TOOLKIT_TOKEN` secret holds that re-minted token. The first push to `main` after merge probes the secret, not an operator's shell. ## What this does NOT check - A read cannot show `write:issue`, so a `read:issue`-only token passes the probe. The probe's own PASS output says so. - It does not grade `write:repository`, collaborator access, or the git credential. - Gitea's `/user` routing was not checked. That arm only WARNs. - Lead, not claim (on #1484): v15's `RepoAccess` takes 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/release` needs `syscall.Flock`, so everything ran on clean Linux clones: - **At head `29b541b`** (rebased onto `2e8f46f`), 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), and `rt register-check` (tree and `2e8f46f..29b541b` subjects). - **At `b3f05c9`**, which is patch-identical to `dc9df38` (`git range-diff` shows `=`): `tests/workflows.bats` passes 138/138 (baseline 137/137). `29b541b` touches only docs. - **Not run locally:** the in-document anchor gate (#1451), which needs the authenticated renderer. The fixed id `user-content-tokens-permissions` was read back from the live `/api/v1/markdown` instead. ## 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 in `cmd/rt/credentials.go`. This PR uses `Refs #1484` and no closing keyword, so `ac-closure-check` has 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](https://claude.com/claude-code)
fix(credentials): probe the release PAT's issue scope before any cut work (#1484)
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Failing after 7s
ac-closure-check / check (pull_request) Failing after 0s
base-divergence-check / check (pull_request) Successful in 6s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / coverage (pull_request) Successful in 7s
fragment-check / density (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 6s
go-ci / lint + build + test (pull_request) Successful in 29s
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
prep-order-check / check (pull_request) Successful in 6s
readme-pin-check / mirror (pull_request) Successful in 7s
readme-pin-check / digest (pull_request) Successful in 6s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
tests / bats (pull_request) Successful in 32s
tests / dated-examples (pull_request) Failing after 4s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 4s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / page landing-tree failure (pull_request) Has been skipped
b3f05c98d2
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>
claude left a comment

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 claude account, 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 toward required_approvals, so a second reviewer still has to approve.

Verified at source

1. Scope claim (Axis A). I read Forgejo v15.0.7 source 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 by tokenRequiresScopes(AccessTokenScopeCategoryIssue).
  • /user closes under AccessTokenScopeCategoryUser (:760).
  • permissions/token_requires_scopes.go returns 403 "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 /labels has no mustEnableIssuesOrPulls and no unit gate. So it is a better probe than /issues, because it cannot 404 on a repo with issues disabled.
  • I did not check Gitea v1.24 routing.

2. Refuse / warn / ungraded split (cmd/rt/credentials.go). Each arm matches the decision tree:

  • 401 → rejected. It refuses and skips /user.
  • 403 → refuses.
  • Anything else → ::warning:: UNGRADED, rc=0.
  • GetAuthenticatedUser goes through getObject. A 403 there yields an HTTPStatusError, so the tier-2 MISSING arm can actually fire and does not fall through to UNGRADED.
  • The tier-3 "GET /user never called" claim matches preflight_push_whitelist.go:111-146.

3. HTTPStatusError.Message is additive. It is only set by the new code path, and there is a pinned test.

4. Workflow wiring.

  • The .forgejo and .gitea hunks are identical.
  • GITHUB_REPOSITORY and github.server_url match what the other steps use.
  • c.Context() follows the established pattern.
  • No binary/YAML skew: bootstrap-rt builds rt from 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 older rt that doesn't know it.

Tests (LF clone, golang:1.25)

go vet is clean. go test ./cmd/rt ./internal/forgejo ./internal/credentials passes.

Mutation check (Axis B)

I re-ran the PR's claimed mutations instead of trusting them:

Mutation Result
403 does not refuse 🔴 tier2IssueScopeRefusedRefuses and tier3ProbesTheAPIPATAndSkipsUser fail
prober(pair.Git) 🔴 the tier-3 arm fails
Non-array guard removed 🔴 the 200 that is not the API subtest fails
404 path removed (extra) 🔴 the 404 subtest fails
--probe-scopes dropped from the workflow the bats line's grep -F matches 0 lines, so the arm goes red. I ran the grep only, not bats

All 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-bot authored 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 run GET .../labels?limit=1 and GET /user with 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

  1. Bump class. This newly refuses tier-2/3 configs that cut successfully today, on every run and not only on the deadlock path. As fix(...) it ships as a patch. release-toolkit.yml has pre_v1_breaking_to_minor: true, so fix(credentials)! (or a BREAKING 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.
  2. Coverage. The bats arm pins the .forgejo file only. The .gitea twin is covered transitively by rt gitea-twin --check, which is fine as long as that gate stays required.

🤖 Generated with Claude Code

**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 `claude` account, 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** toward `required_approvals`, so a second reviewer still has to approve. ## Verified at source **1. Scope claim (Axis A).** I read Forgejo `v15.0.7` source 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 by `tokenRequiresScopes(AccessTokenScopeCategoryIssue)`. - `/user` closes under `AccessTokenScopeCategoryUser` (:760). - `permissions/token_requires_scopes.go` returns `403 "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 /labels` has no `mustEnableIssuesOrPulls` and no unit gate. So it is a better probe than `/issues`, because it cannot 404 on a repo with issues disabled. - I did not check Gitea v1.24 routing. **2. Refuse / warn / ungraded split (`cmd/rt/credentials.go`).** Each arm matches the decision tree: - 401 → `rejected`. It refuses and skips `/user`. - 403 → refuses. - Anything else → `::warning::` UNGRADED, rc=0. - `GetAuthenticatedUser` goes through `getObject`. A 403 there yields an `HTTPStatusError`, so the tier-2 MISSING arm can actually fire and does not fall through to UNGRADED. - The tier-3 "GET /user never called" claim matches `preflight_push_whitelist.go:111-146`. **3. `HTTPStatusError.Message` is additive.** It is only set by the new code path, and there is a pinned test. **4. Workflow wiring.** - The `.forgejo` and `.gitea` hunks are identical. - `GITHUB_REPOSITORY` and `github.server_url` match what the other steps use. - `c.Context()` follows the established pattern. - **No binary/YAML skew:** `bootstrap-rt` builds `rt` from 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 older `rt` that doesn't know it. ## Tests (LF clone, golang:1.25) `go vet` is clean. `go test ./cmd/rt ./internal/forgejo ./internal/credentials` passes. ## Mutation check (Axis B) I re-ran the PR's claimed mutations instead of trusting them: | Mutation | Result | |---|---| | 403 does not refuse | 🔴 `tier2IssueScopeRefusedRefuses` and `tier3ProbesTheAPIPATAndSkipsUser` fail | | `prober(pair.Git)` | 🔴 the tier-3 arm fails | | Non-array guard removed | 🔴 the `200 that is not the API` subtest fails | | 404 path removed (extra) | 🔴 the `404` subtest fails | | `--probe-scopes` dropped from the workflow | the bats line's `grep -F` matches 0 lines, so the arm goes red. I ran the grep only, not `bats` | All 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-bot` authored 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 run `GET .../labels?limit=1` and `GET /user` with 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 1. **Bump class.** This newly refuses tier-2/3 configs that cut successfully today, on every run and not only on the deadlock path. As `fix(...)` it ships as a patch. `release-toolkit.yml` has `pre_v1_breaking_to_minor: true`, so `fix(credentials)!` (or a `BREAKING 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. 2. **Coverage.** The bats arm pins the `.forgejo` file only. The `.gitea` twin is covered transitively by `rt gitea-twin --check`, which is fine as long as that gate stays required. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
alex force-pushed claude/1484-pat-issue-scope from b3f05c98d2
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Failing after 7s
ac-closure-check / check (pull_request) Failing after 0s
base-divergence-check / check (pull_request) Successful in 6s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / coverage (pull_request) Successful in 7s
fragment-check / density (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 6s
go-ci / lint + build + test (pull_request) Successful in 29s
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
prep-order-check / check (pull_request) Successful in 6s
readme-pin-check / mirror (pull_request) Successful in 7s
readme-pin-check / digest (pull_request) Successful in 6s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
tests / bats (pull_request) Successful in 32s
tests / dated-examples (pull_request) Failing after 4s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 4s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / page landing-tree failure (pull_request) Has been skipped
to dc9df38151
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Failing after 7s
ac-closure-check / check (pull_request) Failing after 0s
base-divergence-check / check (pull_request) Successful in 6s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / coverage (pull_request) Successful in 7s
fragment-check / density (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 6s
go-ci / lint + build + test (pull_request) Successful in 30s
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
prep-order-check / check (pull_request) Successful in 6s
readme-pin-check / mirror (pull_request) Successful in 7s
readme-pin-check / digest (pull_request) Successful in 6s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
tests / bats (pull_request) Successful in 32s
tests / dated-examples (pull_request) Failing after 4s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 4s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 4s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / page landing-tree failure (pull_request) Has been skipped
2026-10-02 10:22:59 +02:00
Compare
Author
Member

HOLD: please do not start an approval read yet. A new commit and a PR-body correction are coming:

  • tests / dated-examples is red on b3f05c98d2105c0f27e5a2fdb3b63aec868946eb. The cause is a dead in-document anchor I wrote: #tokens--permissions, where the live renderer emits user-content-tokens-permissions.
  • The body says Closes #1484, which ac-closure-check refuses while #1484 has unticked criteria. It becomes Refs #1484.

I'll post the new full head SHA when the HOLD lifts.

**HOLD:** please do not start an approval read yet. A new commit and a PR-body correction are coming: - `tests / dated-examples` is red on `b3f05c98d2105c0f27e5a2fdb3b63aec868946eb`. The cause is a dead in-document anchor I wrote: `#tokens--permissions`, where the live renderer emits `user-content-tokens-permissions`. - The body says `Closes #1484`, which `ac-closure-check` refuses while #1484 has unticked criteria. It becomes `Refs #1484`. I'll post the new full head SHA when the HOLD lifts.
docs(tokens): link the Tokens & permissions section by its rendered id (#1484)
Some checks failed
tests / bats (pull_request) Successful in 32s
tests / dated-examples (pull_request) Successful in 5s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 4s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 4s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / page landing-tree failure (pull_request) Has been skipped
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / coverage (pull_request) Successful in 7s
fragment-check / density (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
check-self-bootstrap / check (push) Successful in 5s
gitea-twin-check / check (push) Successful in 5s
go-ci / lint + build + test (push) Successful in 30s
go-ci / record reviewed vs landed commit (push) Failing after 3s
prepared-uncut-check / toolkit-self prepared-uncut controls (push) Successful in 5s
prepared-uncut-check / prepared-but-uncut release (push) Successful in 7s
prepared-uncut-check / check (push) Successful in 0s
release / decide + act (push) Successful in 9s
release / release (push) Successful in 0s
tests / workflow-schema (push) Successful in 3s
tests / bats (push) Successful in 31s
tests / dated-examples (push) Successful in 5s
tests / contract-paths (push) Successful in 4s
tests / shellcheck (push) Successful in 3s
go-ci / page landing-tree failure (push) Has been skipped
release / fire-cut (push) Has been skipped
29b541bb69
`### Tokens & permissions` renders as `user-content-tokens-permissions`:
the renderer drops the `&` and collapses the surrounding spaces into ONE
hyphen (docs/conventions.md, "In-document anchors"). Read back through
the live /api/v1/markdown renderer before changing it.

- docs/integration.md: the path-alpha step-2 link added in this PR used
  `#tokens--permissions`; the in-document anchor gate (#1451, in the
  `tests / dated-examples` job) refused it.
- README.md, docs/architecture.md: the same dead anchor in two
  pre-existing cross-file links. The in-document gate cannot see those,
  which is how they survived; fixed here because they point at the
  section this PR rewrites.
- docs/integration.md: record the 2026-10-02 read-back -- a release-bot
  PAT without read:user was refused GET /user with
  `403 token does not have at least one of required scope(s): [read:user]`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Member

HOLD lifted. New head: 29b541bb69f2676c73d7efadda8f093a6fb3846c.

Correction to the HOLD note: by then the head was already dc9df38151cf62437860261c16cd83d83154c563, not b3f05c9. It is the same patch rebased onto 2e8f46f (git range-diff shows =), and the same three checks were red on it.

What moved since the last review read (b3f05c9 → 29b541b):

  1. Rebased onto 2e8f46f. No change to this PR's own patch.
  2. 29b541b (docs only, a new commit, no amend or force-push):
    • #tokens--permissions → #tokens-permissions in docs/integration.md. I read the id user-content-tokens-permissions back from the live /api/v1/markdown renderer.
    • The same dead anchor in two pre-existing cross-file links, README.md:280 and docs/architecture.md:91. The in-document gate cannot see those.
    • The 2026-10-02 read:user read-back is recorded next to the 2026-08-26 one.
  3. PR body: Closes #1484 → Refs #1484, so ac-closure-check should re-run on the edited event 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:1251 returns maxBump(fragBump, commitsBump).
  • changelog.d/1484.changed.md is kind changed, which internal/fragments/fragments.go:63 maps to BumpMinor.
  • So the 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 /user on the current release-bot token now returns HTTP 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).

**HOLD lifted.** New head: `29b541bb69f2676c73d7efadda8f093a6fb3846c`. **Correction to the HOLD note:** by then the head was already `dc9df38151cf62437860261c16cd83d83154c563`, not `b3f05c9`. It is the same patch rebased onto `2e8f46f` (`git range-diff` shows `=`), and the same three checks were red on it. **What moved since the last review read (`b3f05c9` → `29b541b`):** 1. Rebased onto `2e8f46f`. No change to this PR's own patch. 2. `29b541b` (docs only, a new commit, no amend or force-push): - `#tokens--permissions` → `#tokens-permissions` in `docs/integration.md`. I read the id `user-content-tokens-permissions` back from the live `/api/v1/markdown` renderer. - The same dead anchor in two pre-existing cross-file links, `README.md:280` and `docs/architecture.md:91`. The in-document gate cannot see those. - The 2026-10-02 `read:user` read-back is recorded next to the 2026-08-26 one. 3. PR body: `Closes #1484` → `Refs #1484`, so `ac-closure-check` should re-run on the `edited` event 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:1251` returns `maxBump(fragBump, commitsBump)`. - `changelog.d/1484.changed.md` is kind `changed`, which `internal/fragments/fragments.go:63` maps to `BumpMinor`. - So the `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 /user` on the current `release-bot` token now returns `HTTP 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).
claude left a comment

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 at b3f05c9.

⚠️ This is still a COMMENT, not an approval. I posted it from the claude account, which authored the PR. It does not count toward required_approvals, and a second reviewer has to approve.

What moved, checked at source

  • Rebase. The live head is 29b541b and the base is 2e8f46f, which is current origin/main. git range-diff 02fae6a..b3f05c9 29b541b~2..29b541b~1 shows 1: b3f05c9 = 1: dc9df38. The patch is byte-identical, so my review 7143 analysis and mutation results carry over unchanged.
  • 29b541b is docs-only: 4 lines across README.md, docs/architecture.md and docs/integration.md.
    • I rendered ### Tokens & permissions through this forge's /api/v1/markdown. It produced id="user-content-tokens-permissions", so #tokens-permissions is the correct anchor.
    • git grep 'tokens--permissions' 29b541b finds nothing, so no dead copy is left in the tree.
    • Fixing the two pre-existing cross-file links in the same commit is right: they point at the section this PR rewrites.
  • The new read:user source note matches the 403 body quoted on #1484 (comment 113683), and it is dated.
  • Body is now 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: autoBump returns maxBump(fragments.AggregateBump(frags), ccBumpSince(...)).
  • internal/fragments/fragments.go:63: KindChanged: semver.BumpMinor.

So changelog.d/1484.changed.md already makes this cut a minor, whatever the fix: subject says. No ! is needed. Note 2 (bats pins .forgejo only, and .gitea is covered by gitea-twin --check) still stands as non-blocking. gitea-twin-check is green on this head.

Still blocking merge

  1. #1484 AC6. Comment 113683 confirms GET /user → 200 release-bot on what is presumably a re-minted token. The issue-scope read that --probe-scopes actually refuses on, GET .../labels?limit=1, is not quoted yet.
    • Also confirm that the RELEASE_TOOLKIT_TOKEN secret holds the new token. That secret is what the first push to main after merge will probe, not whatever the operator tested with.
    • The first 403 [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 the read:user row in the scope table.
  2. CI is not finished. Read from /commits/29b541b/status:
    • Still pending: combined pending, including tests / dated-examples (the job the dead anchor turned red) and tests / bats (which carries the new wiring arm).
    • Green so far: go-ci / lint + build + test, check-self-bootstrap, gitea-twin-check and register-check.
    • I have not seen dated-examples pass, 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, and go-ci is green on this exact head.

🤖 Generated with Claude Code

**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 at `b3f05c9`. ⚠️ **This is still a COMMENT, not an approval.** I posted it from the `claude` account, which authored the PR. It does not count toward `required_approvals`, and a second reviewer has to approve. ## What moved, checked at source - **Rebase.** The live head is `29b541b` and the base is `2e8f46f`, which is current `origin/main`. `git range-diff 02fae6a..b3f05c9 29b541b~2..29b541b~1` shows `1: b3f05c9 = 1: dc9df38`. The patch is byte-identical, so my review 7143 analysis and mutation results carry over unchanged. - **`29b541b` is docs-only:** 4 lines across `README.md`, `docs/architecture.md` and `docs/integration.md`. - I rendered `### Tokens & permissions` through this forge's `/api/v1/markdown`. It produced `id="user-content-tokens-permissions"`, so `#tokens-permissions` is the correct anchor. - `git grep 'tokens--permissions' 29b541b` finds nothing, so no dead copy is left in the tree. - Fixing the two pre-existing cross-file links in the same commit is right: they point at the section this PR rewrites. - **The new read:user source note** matches the 403 body quoted on #1484 (comment 113683), and it is dated. - **Body is now `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`: `autoBump` returns `maxBump(fragments.AggregateBump(frags), ccBumpSince(...))`. - `internal/fragments/fragments.go:63`: `KindChanged: semver.BumpMinor`. So `changelog.d/1484.changed.md` already makes this cut a minor, whatever the `fix:` subject says. No `!` is needed. Note 2 (bats pins `.forgejo` only, and `.gitea` is covered by `gitea-twin --check`) still stands as non-blocking. `gitea-twin-check` is green on this head. ## Still blocking merge 1. **#1484 AC6.** Comment 113683 confirms `GET /user` → 200 `release-bot` on what is presumably a re-minted token. The issue-scope read that `--probe-scopes` actually refuses on, `GET .../labels?limit=1`, is **not quoted yet**. - Also confirm that the `RELEASE_TOOLKIT_TOKEN` secret holds the new token. That secret is what the first push to `main` after merge will probe, not whatever the operator tested with. - The first 403 `[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 the `read:user` row in the scope table. 2. **CI is not finished.** Read from `/commits/29b541b/status`: - Still pending: combined `pending`, including `tests / dated-examples` (the job the dead anchor turned red) and `tests / bats` (which carries the new wiring arm). - Green so far: `go-ci / lint + build + test`, `check-self-bootstrap`, `gitea-twin-check` and `register-check`. - I have not seen `dated-examples` pass, 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, and `go-ci` is green on this exact head. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
alex approved these changes 2026-10-02 10:42:55 +02:00
claude merged commit 29b541bb69 into main 2026-10-02 10:52:40 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
frankenbit/release-toolkit!1488
No description provided.