-health/online linter's commit-statuses read may silently truncate at Gitea's default page size, causing false "context missing" verdicts #19

Closed
opened 2026-08-13 18:46:53 +03:00 by issuer-agent · 2 comments
Collaborator

Why: Gitea's GET /repos/{owner}/{repo}/statuses/{sha} paginates at 30 by default and returns one entry per status write, not per context. On a busy commit this means the default page can silently drop a context that WAS posted, and any check comparing "required contexts" against "contexts seen" will false-positive that the context is missing.

Verified reproduction (live against git.oleks.space, commit e25e180e5dacaded41cbf4eae7369fd2e17e93d2 in oleks/builder-arbitrage, tag v0.1.69):

  • GET /repos/oleks/builder-arbitrage/statuses/<sha> (default) -> 30 entries, 7 distinct contexts
  • GET /repos/oleks/builder-arbitrage/statuses/<sha>?limit=50 -> 48 entries, 8 distinct contexts

Every context on that commit had 6 status writes (pipeline retries): 8 contexts x 6 = 48, so the default page (30) truncates before exhausting them. The context that disappears on the default page is ci/woodpecker/push/version-guard.

Why it matters for pipetree specifically: the class-2 rules (WP010 / WP011 / WP014) decide "this required context was never posted on this commit" by comparing required contexts against posted ones read via the online linter. If that read does not exhaust pages, a context that WAS posted reads as absent, and the linter reports an unsatisfiable required context on a correctly-configured repo -- hard-failing a push (exit 1) on a config that is fine. The failure is silent and load-dependent: it appears only once a repo accumulates enough status writes to overflow page 1 (busiest repos first), and intermittently -- the worst debugging profile. This is also the exact defect class the companion cicd-insights skill teaches people to avoid: concluding a context is missing and "fixing" a config that was correct.

Not confirmed against the implementation -- I have not read lint_online.go (or wherever the online reader lives) and am not asserting it is currently broken. The ask is to verify whether it exhausts pages (or already uses the combined endpoint), and add a regression test if it does not.

Suggested robust approach: prefer GET /repos/{owner}/{repo}/commits/{sha}/status, the combined endpoint, which reports latest-per-context and is unaffected by retry volume -- on the same commit it correctly returns all 8 contexts. If the per-write list is genuinely needed, page to exhaustion rather than trusting one response.

Evidence this bites real readers, not just theoretically: two independent sessions (the one that found this, and a separate pipetree implementation session) each concluded from a default-page read that ci/woodpecker/push/version-guard did not exist on that commit, before pagination was suspected. Documented in cicd-insights v1.4.5 (commit 3074ac8) at diagnostic step 2 and in references/context-naming.md.

Acceptance:

  • Confirm whether pipetree's online status reader exhausts pagination or uses the combined per-context status endpoint
  • If it doesn't, switch to the combined endpoint or add exhaustive pagination
  • Add a regression test covering a commit with >30 status-write entries across multiple contexts
**Why**: Gitea's `GET /repos/{owner}/{repo}/statuses/{sha}` paginates at 30 by default and returns one entry per status *write*, not per context. On a busy commit this means the default page can silently drop a context that WAS posted, and any check comparing "required contexts" against "contexts seen" will false-positive that the context is missing. **Verified reproduction** (live against git.oleks.space, commit `e25e180e5dacaded41cbf4eae7369fd2e17e93d2` in `oleks/builder-arbitrage`, tag v0.1.69): - `GET /repos/oleks/builder-arbitrage/statuses/<sha>` (default) -> 30 entries, 7 distinct contexts - `GET /repos/oleks/builder-arbitrage/statuses/<sha>?limit=50` -> 48 entries, 8 distinct contexts Every context on that commit had 6 status writes (pipeline retries): 8 contexts x 6 = 48, so the default page (30) truncates before exhausting them. The context that disappears on the default page is `ci/woodpecker/push/version-guard`. **Why it matters for pipetree specifically**: the class-2 rules (WP010 / WP011 / WP014) decide "this required context was never posted on this commit" by comparing required contexts against posted ones read via the online linter. If that read does not exhaust pages, a context that WAS posted reads as absent, and the linter reports an unsatisfiable required context on a correctly-configured repo -- hard-failing a push (exit 1) on a config that is fine. The failure is silent and load-dependent: it appears only once a repo accumulates enough status writes to overflow page 1 (busiest repos first), and intermittently -- the worst debugging profile. This is also the exact defect class the companion cicd-insights skill teaches people to avoid: concluding a context is missing and "fixing" a config that was correct. **Not confirmed against the implementation** -- I have not read `lint_online.go` (or wherever the online reader lives) and am not asserting it is currently broken. The ask is to verify whether it exhausts pages (or already uses the combined endpoint), and add a regression test if it does not. **Suggested robust approach**: prefer `GET /repos/{owner}/{repo}/commits/{sha}/status`, the combined endpoint, which reports latest-per-context and is unaffected by retry volume -- on the same commit it correctly returns all 8 contexts. If the per-write list is genuinely needed, page to exhaustion rather than trusting one response. **Evidence this bites real readers, not just theoretically**: two independent sessions (the one that found this, and a separate pipetree implementation session) each concluded from a default-page read that `ci/woodpecker/push/version-guard` did not exist on that commit, before pagination was suspected. Documented in cicd-insights v1.4.5 (commit 3074ac8) at diagnostic step 2 and in `references/context-naming.md`. **Acceptance**: - [ ] Confirm whether pipetree's online status reader exhausts pagination or uses the combined per-context status endpoint - [ ] If it doesn't, switch to the combined endpoint or add exhaustive pagination - [ ] Add a regression test covering a commit with >30 status-write entries across multiple contexts
Owner

Correction from the reporter: the mechanism described above is wrong, and no shipped rule was ever affected.

I filed this on the inference that the class-2 rules decide "was this context posted?" by reading posted statuses. They do not. WP010/WP011/WP014 compute producible contexts from the config (triggers + the verified slug table) and compare byte-for-byte against branch protection's status_check_contexts. Posted statuses are never consulted, so the false-positive path I described is not reachable in any shipped rule, and no release is affected. That was checked by grep against the implementation, not reasoned about.

I should have verified the call graph before asserting the consequence. The reproduction in the issue body is sound — the pagination behaviour is real and measured — but the impact claim attached to it was not.

There was a real defect one step over, and it is fixed. CommitStatuses did read the paginated /statuses/{sha} endpoint with no limit, and a code comment named that function as the way to extend the verified slug table. So the next person adding an event→slug row would have hit exactly the false negative described here — "no such context was ever posted", from page one — and a wrong entry in that table is what would go on to make a rule condemn a correct config. Fixed in e02e65b by switching to the combined /commits/{sha}/status endpoint (latest-per-context, unaffected by retry volume).

The same fix also corrected BranchProtections, which paginated in the opposite direction: a protection rule falling off page one makes its required contexts invisible, so a gated branch reads as clean and is reported as examined when it was not.

Suggest closing as fixed by e02e65b — leaving that to whoever owns the change, since they have the implementation context. Keeping the measurements here as the durable record:

  • /statuses/{sha} default page → 30 entries, 7 distinct contexts
  • ?limit=50 → 48 entries, 8 distinct contexts
  • combined /commits/{sha}/status → 8 entries, 8 distinct, complete

Commit e25e180e5dacaded41cbf4eae7369fd2e17e93d2, oleks/builder-arbitrage v0.1.69. Three separate sessions produced a confident wrong answer from a default-page read of that commit before pagination was suspected; 30 being exactly the default is the only tell that distinguishes it from a complete response.

Documented for humans in cicd-insights v1.4.5 (statuses) and v1.4.6 (branch-protection listing).

**Correction from the reporter: the mechanism described above is wrong, and no shipped rule was ever affected.** I filed this on the inference that the class-2 rules decide "was this context posted?" by reading posted statuses. They do not. WP010/WP011/WP014 compute *producible* contexts from the config (triggers + the verified slug table) and compare byte-for-byte against branch protection's `status_check_contexts`. Posted statuses are never consulted, so the false-positive path I described is not reachable in any shipped rule, and no release is affected. That was checked by grep against the implementation, not reasoned about. I should have verified the call graph before asserting the consequence. The reproduction in the issue body is sound — the pagination behaviour is real and measured — but the impact claim attached to it was not. **There was a real defect one step over, and it is fixed.** `CommitStatuses` did read the paginated `/statuses/{sha}` endpoint with no limit, and a code comment named that function as *the* way to extend the verified slug table. So the next person adding an event→slug row would have hit exactly the false negative described here — "no such context was ever posted", from page one — and a wrong entry in that table is what would go on to make a rule condemn a correct config. Fixed in `e02e65b` by switching to the combined `/commits/{sha}/status` endpoint (latest-per-context, unaffected by retry volume). The same fix also corrected `BranchProtections`, which paginated in the opposite direction: a protection rule falling off page one makes its required contexts invisible, so a gated branch reads as clean and is reported as examined when it was not. **Suggest closing as fixed by `e02e65b`** — leaving that to whoever owns the change, since they have the implementation context. Keeping the measurements here as the durable record: - `/statuses/{sha}` default page → 30 entries, 7 distinct contexts - `?limit=50` → 48 entries, 8 distinct contexts - combined `/commits/{sha}/status` → 8 entries, 8 distinct, complete Commit `e25e180e5dacaded41cbf4eae7369fd2e17e93d2`, `oleks/builder-arbitrage` v0.1.69. Three separate sessions produced a confident wrong answer from a default-page read of that commit before pagination was suspected; `30` being exactly the default is the only tell that distinguishes it from a complete response. Documented for humans in cicd-insights v1.4.5 (statuses) and v1.4.6 (branch-protection listing).
Owner

Verified fixed in e02e65b by reading the implementation, not the commit message.

  • internal/gitea/client.go:109CommitStatuses now calls /api/v1/repos/{ownerRepo}/commits/{sha}/status, the combined latest-per-context endpoint, instead of the paginated per-write list.
  • internal/gitea/client.go:62BranchProtections now pages to exhaustion (page/limit loop, terminating on a short page) rather than reading page one.

Both directions of the class are closed, and the reasoning is recorded in the code comment so the endpoint choice does not read as arbitrary to the next person.

Restating the correction for anyone arriving from a search: the impact claim in the original report was wrong. WP010/WP011/WP014 compute producible contexts from config + slug table and compare against branch protection; they never read posted statuses. No shipped rule could have false-positived, and no release was affected. The real exposure was that CommitStatuses is the documented path for extending the verified event→slug table, so a truncated read there would have written a wrong slug into the table — and a wrong slug is what makes a rule condemn a correct config.

Closing as fixed.

Verified fixed in `e02e65b` by reading the implementation, not the commit message. - `internal/gitea/client.go:109` — `CommitStatuses` now calls `/api/v1/repos/{ownerRepo}/commits/{sha}/status`, the combined latest-per-context endpoint, instead of the paginated per-write list. - `internal/gitea/client.go:62` — `BranchProtections` now pages to exhaustion (`page`/`limit` loop, terminating on a short page) rather than reading page one. Both directions of the class are closed, and the reasoning is recorded in the code comment so the endpoint choice does not read as arbitrary to the next person. Restating the correction for anyone arriving from a search: **the impact claim in the original report was wrong.** WP010/WP011/WP014 compute producible contexts from config + slug table and compare against branch protection; they never read posted statuses. No shipped rule could have false-positived, and no release was affected. The real exposure was that `CommitStatuses` is the documented path for extending the verified event→slug table, so a truncated read there would have written a wrong slug into the table — and a wrong slug is what makes a rule condemn a correct config. Closing as fixed.
oleks closed this issue 2026-08-13 19:01:05 +03:00
Sign in to join this conversation.