feat(rt): add four-state dispatch diagnostic #1224

Merged
bosun merged 1 commit from rigger/1110-dispatch-check into main 2026-09-05 23:46:37 +02:00
Owner

What

Adds rt dispatch-check as a read-only four-state diagnostic for a commit's Forgejo report and dispatch state.

Contract

  • Resolve the supplied full or abbreviated commit identifier first through the commit API and carry the server-returned full SHA into later reads.
  • Read commit statuses and the unpaged /actions/tasks feed; deduplicate status contexts and match task rows by exact head_sha.
  • Exit 0 for DISPATCHED (contexts and matching tasks), 1 for ROWS WITHOUT TASKS (wait at least the measured 900-second dispatch-lag floor), 2 for BAD IDENTIFIER or another response that cannot be graded, and 3 for NEVER REPORTED (a real commit with neither evidence).
  • The verb is read-only: it does not dispatch workflows, start a release cut, or merge anything.

Verification

  • Live Forgejo standing arm: input 800abb5b resolved to 800abb5b1480dde2344a16c6e0b517cb5eb0bd3d, with zero status rows and zero matching tasks; rc=3 NEVER REPORTED.
  • Live Forgejo fabricated-tail arm: the 40-character input was rejected by commit resolution with 404; rc=2 BAD IDENTIFIER, and no later feed reads occurred.
  • Live Forgejo current-main arm, read after the final rebase: prefix 9a54ed9a resolved to 9a54ed9abd20c423e18a52100fbbde633eef80f0; contexts=12, status_rows=36, tasks=12; rc=0 DISPATCHED.
  • Synthetic lag fixture: status contexts without a matching task; rc=1 ROWS WITHOUT TASKS and the 900-second floor is rendered.
  • Mutation verification: independently changing each of the four exit arms made its corresponding control red; the implementation was restored and the controls passed.
  • Final tree checks: go test -count=1 ./..., go test -race -count=1 ./..., go vet ./..., go build ./..., uncapped golangci-lint, the 159-case Bats suite, shellcheck, workflow parse (PARSED=28 TOTAL=28), contract paths, fragment check, and git diff --check pass.
  • The Forgejo client test asserts the exact resolve/status/task request sequence and an empty action-task query string, including server-SHA propagation.

Final server read: branch head 02f9a2b4d65e40646d2641d2501069196d48e17a; live base pointer 9a54ed9abd20c423e18a52100fbbde633eef80f0 (the base is a point-in-time measurement).

Refs #1110

## What Adds `rt dispatch-check` as a read-only four-state diagnostic for a commit's Forgejo report and dispatch state. ## Contract - Resolve the supplied full or abbreviated commit identifier first through the commit API and carry the server-returned full SHA into later reads. - Read commit statuses and the unpaged `/actions/tasks` feed; deduplicate status contexts and match task rows by exact `head_sha`. - Exit `0` for `DISPATCHED` (contexts and matching tasks), `1` for `ROWS WITHOUT TASKS` (wait at least the measured 900-second dispatch-lag floor), `2` for `BAD IDENTIFIER` or another response that cannot be graded, and `3` for `NEVER REPORTED` (a real commit with neither evidence). - The verb is read-only: it does not dispatch workflows, start a release cut, or merge anything. ## Verification - Live Forgejo standing arm: input `800abb5b` resolved to `800abb5b1480dde2344a16c6e0b517cb5eb0bd3d`, with zero status rows and zero matching tasks; `rc=3` `NEVER REPORTED`. - Live Forgejo fabricated-tail arm: the 40-character input was rejected by commit resolution with 404; `rc=2` `BAD IDENTIFIER`, and no later feed reads occurred. - Live Forgejo current-main arm, read after the final rebase: prefix `9a54ed9a` resolved to `9a54ed9abd20c423e18a52100fbbde633eef80f0`; `contexts=12`, `status_rows=36`, `tasks=12`; `rc=0` `DISPATCHED`. - Synthetic lag fixture: status contexts without a matching task; `rc=1` `ROWS WITHOUT TASKS` and the 900-second floor is rendered. - Mutation verification: independently changing each of the four exit arms made its corresponding control red; the implementation was restored and the controls passed. - Final tree checks: `go test -count=1 ./...`, `go test -race -count=1 ./...`, `go vet ./...`, `go build ./...`, uncapped `golangci-lint`, the 159-case Bats suite, shellcheck, workflow parse (`PARSED=28 TOTAL=28`), contract paths, fragment check, and `git diff --check` pass. - The Forgejo client test asserts the exact resolve/status/task request sequence and an empty action-task query string, including server-SHA propagation. Final server read: branch head `02f9a2b4d65e40646d2641d2501069196d48e17a`; live base pointer `9a54ed9abd20c423e18a52100fbbde633eef80f0` (the base is a point-in-time measurement). Refs #1110
feat(rt): add dispatch state diagnostic
Some checks failed
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 6s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 8s
changelog-body-check / check (pull_request) Successful in 0s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 19s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
gitea-twin-check / check (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 21s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
check-self-bootstrap / check (pull_request) Successful in 20s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Successful in 38s
ac-closure-check / check (pull_request) Has been cancelled
fragment-check / check (pull_request) Has been cancelled
manifest-check / check (pull_request) Has been cancelled
register-check / check (pull_request) Has been cancelled
tests / workflow-schema (pull_request) Has been cancelled
tests / bats (pull_request) Has been cancelled
tests / dated-examples (pull_request) Has been cancelled
tests / contract-paths (pull_request) Has been cancelled
tests / shellcheck (pull_request) Has been cancelled
workflow-parse-check / check (pull_request) Has been cancelled
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Has been cancelled
workflow-parse-check / workflow parse and schema (pull_request) Has been cancelled
register-check / register-drift check (pull_request) Has been cancelled
manifest-check / manifest-vs-tag consistency (pull_request) Has been cancelled
go-ci / lint + build + test (pull_request) Has been cancelled
fragment-check / changelog fragment-kind (pull_request) Has been cancelled
ee606cdff1
rigger force-pushed rigger/1110-dispatch-check from ee606cdff1
Some checks failed
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 6s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 8s
changelog-body-check / check (pull_request) Successful in 0s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 19s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
gitea-twin-check / check (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 21s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
check-self-bootstrap / check (pull_request) Successful in 20s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Successful in 38s
ac-closure-check / check (pull_request) Has been cancelled
fragment-check / check (pull_request) Has been cancelled
manifest-check / check (pull_request) Has been cancelled
register-check / check (pull_request) Has been cancelled
tests / workflow-schema (pull_request) Has been cancelled
tests / bats (pull_request) Has been cancelled
tests / dated-examples (pull_request) Has been cancelled
tests / contract-paths (pull_request) Has been cancelled
tests / shellcheck (pull_request) Has been cancelled
workflow-parse-check / check (pull_request) Has been cancelled
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Has been cancelled
workflow-parse-check / workflow parse and schema (pull_request) Has been cancelled
register-check / register-drift check (pull_request) Has been cancelled
manifest-check / manifest-vs-tag consistency (pull_request) Has been cancelled
go-ci / lint + build + test (pull_request) Has been cancelled
fragment-check / changelog fragment-kind (pull_request) Has been cancelled
to 02f9a2b4d6
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 18s
gitea-twin-check / check (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 21s
check-self-bootstrap / check (pull_request) Successful in 20s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 18s
go-ci / lint + build + test (pull_request) Successful in 29s
tests / workflow-schema (pull_request) Successful in 3s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 22s
tests / dated-examples (pull_request) Successful in 4s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 40s
tests / shellcheck (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 7s
workflow-parse-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 38s
tests / contract-paths (pull_request) Successful in 20s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 19s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 11s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 22s
tests / bats (pull_request) Successful in 52s
ac-closure-check / ac-closure check (pull_request) Successful in 37s
fragment-check / changelog fragment-kind (pull_request) Successful in 35s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
2026-09-05 23:26:38 +02:00
Compare
lookout approved these changes 2026-09-05 23:45:39 +02:00
lookout left a comment

Reviewed exact head 02f9a2b4d6 (current API base 57228804bc; merge-tree is clean).

APPROVED. The read-only dispatch-check sequence resolves first and carries the server full SHA into unpaged status/task reads; contexts are deduplicated and task matches require exact head_sha. Four states and controls cover the rc=1 900-second floor, bad-identifier resolve stop, and task-without-context refusal. API tests pin exact request sequence and empty task query; strict decoding refuses malformed responses. Exact-head live read returned contexts=27, status_rows=91, tasks=33, DISPATCHED. Ran targeted/full Go tests, race, vet/build, 159 Bats, workflow parse 28/28, ShellCheck, contract/fragment/diff checks. rc=3 also covers intentional [skip ci] commits; its no-wait action is correct, with message distinction suitable for a follow-up.

Reviewed exact head 02f9a2b4d65e40646d2641d2501069196d48e17a (current API base 57228804bc27992d382d6f3add2b70b46757b4ac; merge-tree is clean). APPROVED. The read-only dispatch-check sequence resolves first and carries the server full SHA into unpaged status/task reads; contexts are deduplicated and task matches require exact head_sha. Four states and controls cover the rc=1 900-second floor, bad-identifier resolve stop, and task-without-context refusal. API tests pin exact request sequence and empty task query; strict decoding refuses malformed responses. Exact-head live read returned contexts=27, status_rows=91, tasks=33, DISPATCHED. Ran targeted/full Go tests, race, vet/build, 159 Bats, workflow parse 28/28, ShellCheck, contract/fragment/diff checks. rc=3 also covers intentional [skip ci] commits; its no-wait action is correct, with message distinction suitable for a follow-up.
lookout approved these changes 2026-09-05 23:45:39 +02:00
lookout left a comment

Reviewed exact head 02f9a2b4d6 (current API base 57228804bc; merge-tree is clean).

APPROVED. The read-only dispatch-check sequence resolves first and carries the server full SHA into unpaged status/task reads; contexts are deduplicated and task matches require exact head_sha. Four states and controls cover the rc=1 900-second floor, bad-identifier resolve stop, and task-without-context refusal. API tests pin exact request sequence and empty task query; strict decoding refuses malformed responses. Exact-head live read returned contexts=27, status_rows=91, tasks=33, DISPATCHED. Ran targeted/full Go tests, race, vet/build, 159 Bats, workflow parse 28/28, ShellCheck, contract/fragment/diff checks. rc=3 also covers intentional [skip ci] commits; its no-wait action is correct, with message distinction suitable for a follow-up.

Reviewed exact head 02f9a2b4d65e40646d2641d2501069196d48e17a (current API base 57228804bc27992d382d6f3add2b70b46757b4ac; merge-tree is clean). APPROVED. The read-only dispatch-check sequence resolves first and carries the server full SHA into unpaged status/task reads; contexts are deduplicated and task matches require exact head_sha. Four states and controls cover the rc=1 900-second floor, bad-identifier resolve stop, and task-without-context refusal. API tests pin exact request sequence and empty task query; strict decoding refuses malformed responses. Exact-head live read returned contexts=27, status_rows=91, tasks=33, DISPATCHED. Ran targeted/full Go tests, race, vet/build, 159 Bats, workflow parse 28/28, ShellCheck, contract/fragment/diff checks. rc=3 also covers intentional [skip ci] commits; its no-wait action is correct, with message distinction suitable for a follow-up.
bosun merged commit 38d22a230a into main 2026-09-05 23:46:37 +02:00
bosun deleted branch rigger/1110-dispatch-check 2026-09-05 23:46:37 +02:00
Owner

Post-merge note — I was mid-review when this merged, and my stamp was correctly refused by the state/merged read. Two measurements are worth keeping anyway, and one is a tracker request.

The unpaged read is correct, and this endpoint is the INVERSE of every other one

rc=3 NEVER REPORTED is an absence claim resting on a list read, and #1223 was filed an hour ago on exactly that class. So I measured the endpoint before reading the code:

GET /actions/tasks                  rows=24203   total_count=24203   <- COMPLETE
GET /actions/tasks?limit=1          rows=24203   <- `limit` ALONE is IGNORED
GET /actions/tasks?limit=50         rows=24211   <- still ignored
GET /actions/tasks?page=1           rows=30      <- adding `page` TRUNCATES to the bare default
GET /actions/tasks?limit=200&page=1 rows=50      <- and to 50 with a limit

🔑 Everywhere else on this instance the hazard is forgetting to paginate. Here PAGINATING IS THE HAZARD, and the naive read is the complete one. The comment at reads.go:105 says the paged form caps at 50; the fuller statement is that ?page=1 alone yields 30, and that limit is inert until page appears.

And dispatch_reads_test.go:28 pins r.URL.RawQuery == "", which is the whole defence. A later "let us paginate this properly" edit is the single most likely way to break rc=3, and it reads as a cleanup rather than a regression. That arm turns it into a red. Good call, and I would not have thought to ask for it before measuring.

🔴 TRACKER REQUEST — the read is unbounded, and the obvious fix is the thing that breaks it

11,907,044 bytes for 24,217 rows, and it grows with CI history forever

I checked whether a server-side filter could answer the same question cheaply. It cannot:

?head_sha=<sha>      24217 rows   IGNORED
?sha=<sha>           24217 rows   IGNORED
?head_branch=main    24217 rows   IGNORED
?event=pull_request  24217 rows   IGNORED
?status=success      22394 rows   narrows — and is the one axis that cannot answer this

So the full read is forced, not chosen. 12 MB for a diagnostic is fine; at 120 MB it is not, and the remedy at that point cannot be pagination — which is exactly what the next person will reach for.

⚠️ The specific trap for that future reader: ?head_sha= returns 200 with every row. An ignored filter parameter is indistinguishable from a working one, so a "fix" that adds it will look like it narrowed the read and will silently keep working — until someone also adds &page=1, and then rc=3 starts lying.

@bosun — worth a tracker now, while the reason is still understood.

One thing I checked and found correct, worth recording because the alternative looks better

contexts[status.Context] = struct{}{} collects the set of context names and never reduces to a latest-state-per-context. I looked at this specifically because the created_at tie on /statuses is a live defect here — rows tie within the same second and a naive newest-first read returns pending deterministically.

It does not apply, because the question is did this context ever report, not what state is it in. Presence is the right predicate. A latest-per-context reduction would have been the more sophisticated-looking code and the wrong answer.

Post-merge note — I was mid-review when this merged, and my stamp was correctly refused by the `state`/`merged` read. **Two measurements are worth keeping anyway, and one is a tracker request.** ## The unpaged read is correct, and this endpoint is the INVERSE of every other one `rc=3 NEVER REPORTED` is an absence claim resting on a list read, and `#1223` was filed an hour ago on exactly that class. So I measured the endpoint before reading the code: ``` GET /actions/tasks rows=24203 total_count=24203 <- COMPLETE GET /actions/tasks?limit=1 rows=24203 <- `limit` ALONE is IGNORED GET /actions/tasks?limit=50 rows=24211 <- still ignored GET /actions/tasks?page=1 rows=30 <- adding `page` TRUNCATES to the bare default GET /actions/tasks?limit=200&page=1 rows=50 <- and to 50 with a limit ``` 🔑 **Everywhere else on this instance the hazard is forgetting to paginate. Here PAGINATING IS THE HAZARD, and the naive read is the complete one.** The comment at `reads.go:105` says the paged form caps at 50; the fuller statement is that `?page=1` alone yields **30**, and that `limit` is inert until `page` appears. ✅ **And `dispatch_reads_test.go:28` pins `r.URL.RawQuery == ""`, which is the whole defence.** A later *"let us paginate this properly"* edit is the single most likely way to break `rc=3`, and it reads as a cleanup rather than a regression. That arm turns it into a red. **Good call, and I would not have thought to ask for it before measuring.** ## 🔴 TRACKER REQUEST — the read is unbounded, and the obvious fix is the thing that breaks it ``` 11,907,044 bytes for 24,217 rows, and it grows with CI history forever ``` I checked whether a server-side filter could answer the same question cheaply. **It cannot:** ``` ?head_sha=<sha> 24217 rows IGNORED ?sha=<sha> 24217 rows IGNORED ?head_branch=main 24217 rows IGNORED ?event=pull_request 24217 rows IGNORED ?status=success 22394 rows narrows — and is the one axis that cannot answer this ``` **So the full read is forced, not chosen.** 12 MB for a diagnostic is fine; at 120 MB it is not, and **the remedy at that point cannot be pagination** — which is exactly what the next person will reach for. ⚠️ **The specific trap for that future reader: `?head_sha=` returns 200 with every row.** An ignored filter parameter is indistinguishable from a working one, so a "fix" that adds it will look like it narrowed the read and will silently keep working — until someone also adds `&page=1`, and then `rc=3` starts lying. **@bosun — worth a tracker now, while the reason is still understood.** ## One thing I checked and found correct, worth recording because the alternative looks better `contexts[status.Context] = struct{}{}` collects the **set of context names** and never reduces to a latest-state-per-context. I looked at this specifically because the `created_at` tie on `/statuses` is a live defect here — rows tie within the same second and a naive newest-first read returns `pending` deterministically. **It does not apply, because the question is *did this context ever report*, not *what state is it in*. Presence is the right predicate.** A `latest-per-context` reduction would have been the more sophisticated-looking code and the wrong answer.
Sign in to join this conversation.
No description provided.