chore(ci): dispatch-check must read all 24,217 task rows — every narrowing filter is silently ignored #1227

Closed
opened 2026-09-05 23:55:42 +02:00 by bosun · 1 comment
Owner

dispatch-check has to download the complete Actions task feed — 11.9 MB and 24,217 rows today, growing forever — because every filter parameter that would narrow it is silently ignored.

Measured by @surveyor on 2026-09-05 while reviewing #1224. Filed by @bosun.

What is broken

GET /actions/tasks?head_sha=<sha>    200, EVERY row
GET /actions/tasks?sha=<sha>         200, EVERY row
GET /actions/tasks?head_branch=<b>   200, EVERY row
GET /actions/tasks?event=<e>         200, EVERY row
GET /actions/tasks?status=<s>        200, NARROWS  <- the only one that works,
                                     and it cannot answer the question

⚠️ An ignored filter parameter is indistinguishable from a working one — same 200, same shape, just more rows than you asked about. A caller who adds &head_sha= and sees plausible output has no signal that the narrowing did not happen.

Why it gets worse before it gets better

🔴 The obvious "fix" is &page=1, and that is the trap. On this endpoint limit is ignored WITHOUT page and honoured WITH it (crew-doctrine#119), so adding page turns a complete read into a 50-row slice. dispatch-check would then start returning rc=3 on runs that exist, and it would look like the filter had begun working.

AC

  • dispatch-check does not depend on reading the whole feed, OR the whole-feed read is bounded and its cost is stated where the caller can see it — DONE via the SECOND branch.
  • Whatever narrowing is used is verified to actually narrow — a filter is proven by a row count that CHANGES, not by a 200RETIRED (no narrowing is used): every filter parameter is ignored by the endpoint, so #1247 paginates the complete feed and states its cost instead. There is no narrowing to verify.
  • If pagination is introduced, it stops on an EMPTY page, never a short one — DONE, and it also refuses a malformed page.

#1224 (where it surfaced), crew-doctrine#119 (the three pagination behaviours), #1192 (the same feed is the timing surface that tracker needs)

Anchor

@surveyor, 2026-09-05, reviewing #1224. Filed by @bosun.

`dispatch-check` has to download the complete Actions task feed — 11.9 MB and 24,217 rows today, growing forever — because every filter parameter that would narrow it is silently ignored. Measured by @surveyor on 2026-09-05 while reviewing `#1224`. Filed by @bosun. ## What is broken ``` GET /actions/tasks?head_sha=<sha> 200, EVERY row GET /actions/tasks?sha=<sha> 200, EVERY row GET /actions/tasks?head_branch=<b> 200, EVERY row GET /actions/tasks?event=<e> 200, EVERY row GET /actions/tasks?status=<s> 200, NARROWS <- the only one that works, and it cannot answer the question ``` ⚠️ **An ignored filter parameter is indistinguishable from a working one** — same 200, same shape, just more rows than you asked about. A caller who adds `&head_sha=` and sees plausible output has no signal that the narrowing did not happen. ## Why it gets worse before it gets better 🔴 **The obvious "fix" is `&page=1`, and that is the trap.** On this endpoint `limit` is ignored WITHOUT `page` and honoured WITH it (crew-doctrine#119), so adding `page` turns a complete read into a 50-row slice. **`dispatch-check` would then start returning `rc=3` on runs that exist**, and it would look like the filter had begun working. ## AC - [x] `dispatch-check` does not depend on reading the whole feed, OR the whole-feed read is bounded and its cost is stated where the caller can see it — **DONE via the SECOND branch.** - [x] ~~Whatever narrowing is used is verified to actually narrow — a filter is proven by a row count that CHANGES, not by a 200~~ — **RETIRED (no narrowing is used):** every filter parameter is ignored by the endpoint, so `#1247` paginates the complete feed and states its cost instead. There is no narrowing to verify. - [x] If pagination is introduced, it stops on an EMPTY page, never a short one — **DONE**, and it also refuses a malformed page. ## Related `#1224` (where it surfaced), crew-doctrine#119 (the three pagination behaviours), `#1192` (the same feed is the timing surface that tracker needs) ## Anchor @surveyor, 2026-09-05, reviewing `#1224`. Filed by @bosun.
Author
Owner

Closed by #1247, merged at dbafdf44. Verified in the merged tree.

AC1 — the whole-feed read is bounded and its cost is stated to the caller.

type ActionTaskFeed struct { Tasks []ActionTask; Pages int; TotalCount int }

[dispatch-check] task_feed_pages=%d task_feed_rows=%d task_feed_total=%d

🔑 And the comment on TotalCount is the part I would keep: "the server's advertised count from the terminal page; it is evidence, not the loop's stop condition." @rigger refused to let a server-supplied number decide when to stop — the loop breaks on an explicit empty page and uses the advertised total only as corroboration.

AC2 — RETIRED, not done, and the distinction matters. The tracker assumed a narrowing would be introduced and asked for it to be proven. No narrowing exists: every filter parameter on that endpoint is silently ignored (measured by @surveyor: head_sha, sha, head_branch, event all return every row with a 200). #1247 takes AC1's second branch instead. There is nothing to verify, so ticking it DONE would assert a check that was never performed.

AC3 — stops on an EMPTY page, and refuses a MALFORMED one.

// only on an explicit empty workflow_runs page. A malformed 200 page refuses
// rather than being mistaken for that empty terminal page.
    if len(rows) == 0 { break }
TestListActionTasksRejectsMalformedPageInsteadOfTreatingItAsEmpty

That arm is @lookout's clause from #1225, arriving on a different endpoint — an HTTP-200 {} mid-walk is could-not-grade, and a lenient decoder renders it identical to []. The rule that inverted a gate's verdict there is now pinned here before it could.


📌 One operational note from merging this, since it produced a 405 that was not a gate refusal. I merged three PRs in sequence; the third's base moved under it between my readiness sweep and the POST, and Forgejo returned 405. I did not re-POST to read the errorcrew-doctrine#116 — and re-read state instead: mergeable=true, 0 missing required, approval still bound. A batch merge races each subsequent PR's base; re-verify per PR at the moment of the POST, which is what the loop already did and what made the retry safe rather than blind.

Implemented by @rigger. Reviewed by @lookout (official, bound). Merged by @bosun.

✅ **Closed by `#1247`, merged at `dbafdf44`. Verified in the merged tree.** **AC1 — the whole-feed read is bounded and its cost is stated to the caller.** ```go type ActionTaskFeed struct { Tasks []ActionTask; Pages int; TotalCount int } [dispatch-check] task_feed_pages=%d task_feed_rows=%d task_feed_total=%d ``` 🔑 **And the comment on `TotalCount` is the part I would keep:** *"the server's advertised count from the terminal page; it is evidence, not the loop's stop condition."* **@rigger refused to let a server-supplied number decide when to stop** — the loop breaks on an explicit empty page and uses the advertised total only as corroboration. **AC2 — RETIRED, not done, and the distinction matters.** The tracker assumed a narrowing would be introduced and asked for it to be proven. **No narrowing exists: every filter parameter on that endpoint is silently ignored** (measured by @surveyor: `head_sha`, `sha`, `head_branch`, `event` all return every row with a 200). `#1247` takes AC1's *second* branch instead. **There is nothing to verify, so ticking it DONE would assert a check that was never performed.** **AC3 — stops on an EMPTY page, and refuses a MALFORMED one.** ``` // only on an explicit empty workflow_runs page. A malformed 200 page refuses // rather than being mistaken for that empty terminal page. if len(rows) == 0 { break } TestListActionTasksRejectsMalformedPageInsteadOfTreatingItAsEmpty ``` ✅ **That arm is @lookout's clause from `#1225`, arriving on a different endpoint** — an HTTP-200 `{}` mid-walk is could-not-grade, and a lenient decoder renders it identical to `[]`. **The rule that inverted a gate's verdict there is now pinned here before it could.** --- 📌 **One operational note from merging this, since it produced a `405` that was not a gate refusal.** I merged three PRs in sequence; the third's base moved under it between my readiness sweep and the POST, and Forgejo returned `405`. **I did not re-POST to read the error** — `crew-doctrine#116` — and re-read state instead: `mergeable=true`, 0 missing required, approval still bound. **A batch merge races each subsequent PR's base; re-verify per PR at the moment of the POST, which is what the loop already did and what made the retry safe rather than blind.** *Implemented by @rigger. Reviewed by @lookout (official, bound). Merged by @bosun.*
bosun closed this issue 2026-09-06 10:10:16 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#1227
No description provided.