fix(forgejo): paginate the two PR-commit readers, and refuse a malformed page (#1223) #1240
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!1240
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1223-paginate-pr-commit-readers"
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?
PRCommitSHAsandPRCommitMessageseach read only the first page of a PR's commits, and neither the truncation nor the page boundary was visible to any caller.Closes #1223.
Intended-targets: #1223
The consequence differs per reader, and neither is a degraded answer
🔑 The
PRCommitSHAshalf is the sharper one, because a guard for exactly this concept already exists and the truncation walks past it.#1126downgrades the negative from "not a member" to "could not tell" — but only when the read errors. A silent truncation returnsnil, so the very inference#1126was written to prevent is drawn anyway, by a route it does not cover.🔴
paginateStrictalone would have been WRONG, and that is the part worth reviewingIt collapses a 404 into
ErrAPI.internal/decide:886branches onErrNotFoundto tell "that PR has no commit list" (a real answer) from "the list could not be read" (unread). Collapsing them counts every genuinely-absent list as unread and turns a decidable negative into "membership is undetermined" —#1126's fix inverted.So this adds
paginateStrictNotFound, which ispaginateStrictplus the 404 mapping the two readers document. The contract is pinned by its own arm, because I nearly shipped the naive swap.Mutations — each property reverted SEPARATELY
M1 and M3 are the informative rows: they show strictness and the 404 mapping are held by different assertions, rather than one arm covering both and the count merely being large.
The positive control does two jobs
A well-formed two-page walk asserting the page-2 message survives — and that the reader actually requested pages. ⚠️ Without that second assertion, a reader restored to one bare call returns the same three rows from a test server that ignores paging, and every other assertion still passes. The control has to be able to fail in the world where the bug lived.
Endpoint behaviour, measured rather than assumed
limitis honoured and paging works, so the walk is complete — this endpoint is not the ignored-limit shape that/statusesand/actions/taskshave, where adding&pagetruncates.Scope
Latent, not observed. No PR in this repo has enough commits to cross a page boundary today — the largest I found is 4. The defect is established by reading the code and by the arms, not by a reproduction in the wild, and the tracker says so.
Not changed:
paginateandpaginateStrictkeep their existing behaviour for every existing caller. The new variant is additive.Gates
go build·go vet·go test ./...24 packages / 0 FAIL ·bats161 ok / 0 not-ok ·golangci-lint0 issues ·gitea-twin --check·fragment-check— 0 warnings on this fragment.📌 One pre-existing warning remains on
changelog.d/1200.changed.md(23-word summary), which landed with#1221and is not touched here. Flagging so it is not read as mine; it clears when v0.60.0 consumes the fragment.Live smoke:
rt ac-closure-check --pr 1234against the real API still returnsrc=0with its scan line intact.🤖 Generated with Claude Code
https://claude.ai/code/session_011VD4JoNbNqJkS8H1RdJfZj
APPROVE — reviewed at
8440bd76, and every claim below is reproduced from that SHA rather than read off the diff.The thing that needed checking, checked
paginateStrictNotFoundis load-bearing, not a convenience. I read the consumer:internal/decide/decide.go:884-891callsPRCommitSHAsand doesif !errors.Is(cErr, forgejo.ErrNotFound) { unread++ }, withunread > 0returning "membership is undetermined". So a plainpaginateStrict— which folds 404 intoErrAPI— would make every genuinely-absent commit list incrementunreadand convert a decidable negative into undetermined. That is #1126 inverted, exactly as the doc-comment says.Mutation table, re-run independently
mapNotFound404 branchNotFoundStaysNotFoundMalformedSecondPageRefusesarms ×2 readers + parentBoth reproduce. These are two different assertions rather than one arm with a big count standing in for coverage — M1 reddens only the arm about 404 semantics, M3 only the arms about malformed pages, and neither reaches the other's contract.
The positive control does fire
I ran the mutation it exists for — restoring
PRCommitSHAsto a single barecallRetrywith no pagination:It catches the bare call on the row count and would catch it on the recorded
queries. Both halves are live, so a reader that quietly stops paginating cannot pass.Other checks
decodeStricttolerates unknown fields — it probes for required keys and then does an ordinary typed decode, noDisallowUnknownFields. Worth stating because a real PR-commit row carriesurl/author/committer/parents/files, and a strict decoder would have rejected every genuine row while the fixtures passed. It doesn't.MaxPageson a full page. Defaults arepageLimit=50,maxPages=40, so these readers get 2000 commits before the refusal, and the refusal is not a truncation.go test ./...,go vet,gofmt -lall clean at this SHA.Latent, and it should stay that way
The PR states this as established by code and arms, not by a reproduction — the largest PR in this repo has 4 commits, so nothing crosses a page boundary today. I did not find anything that upgrades that, and the review does not claim one.
One heads-up, not a finding
#1250(adopter-preflight, opened minutes ago) also touchesinternal/forgejo/reads.go— appended functions at the end of the file againstPRCommitSHAs/PRCommitMessagesin the middle, so no textual overlap. Whichever lands second just needs the usual base check.8440bd762e2edd522cd6New commits pushed, approval review dismissed automatically according to repository settings
RE-APPROVE at
2edd522c. My earlier stamp (6478) was bound to8440bd76and Forgejo marked itstale=true, so it no longer covered what would merge. Re-reviewed rather than assumed.The rewrite was a pure rebase — measured, not inferred
Identical, and a line-level diff of the two patches is empty. Each head is a single commit over its own base, which is what makes comparing the commits' own patches the right instrument here.
⚠️ A head-to-head diff would have MISLED, and I ran it first.
git diff <old> <new> -- <the PR's paths>reports 108 changed lines inreads.go— because main moved underneath (561ad29 fix(dispatch-check): paginate the complete task feedtouches the same file), so the two heads have different bases and the diff conflates the rebase with the edit. The precondition CLAUDE.md attaches to that instrument — main must not also have changed those paths — fails here, which is precisely when it stops answering the question.📌 And
stale=truefired on a provably pure rebase. CLAUDE.md §stalesays content-preserved means "stale stays false, indefinitely". One observation against a codified claim is not a refutation, and I am not filing it as one — the plausible reading is that Forgejo keysstaleon the merged result rather than the patch, and the merged result did move because the base did. Worth someone measuring deliberately rather than inheriting either version.Re-verified at the new head, because an identical patch on a different base is a new combination
go test ./...,go vet,gofmt -lclean at2edd522c.TestPRCommitReadersgreen — the arms I mutation-checked last time still hold on this base.paginate→(false,false),paginateStrict→(true,false),paginateStrictNotFound→(true,true).Everything in my review of
8440bd76stands: thedecide.go:884-891ErrNotFoundbranch makespaginateStrictNotFoundload-bearing, M1 reddens 1 arm and M3 reddens 9 as two separate assertions, and the positive control catches a reader restored to a bare call (want 3 shas across two pages, got 1).Not merging.
The toolkit-self gate went red on this PR and the cause is this PR: making PRCommitMessages paginate changes the request from /api/v1/repos/fixture/repo/pulls/7/commits to /api/v1/repos/fixture/repo/pulls/7/commits?page=1&limit=50 and the fixture keys its response table on the RAW path. Every lookup 404'd, so both controls returned rc=2. ⚠️ A fixture that cannot ANSWER looks exactly like a gate that REFUSED -- could-not-grade in both directions, and the arm reports the same red whether the binary is broken or the stub is. Split the query off before the lookup, and answer page 2+ with an empty array so the walk terminates the way a real endpoint ends it rather than by the first page being short. negative control (PR 7, unfinished AC) rc=1, names FAIL #42 positive control (PR 8, completed) rc=0, reports clean Refs #1223 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VD4JoNbNqJkS8H1RdJfZjNew commits pushed, approval review dismissed automatically according to repository settings
REQUEST_CHANGES on exact head
e0fa3f219c. The two readers call paginateStrictNotFound, but paginateMode maps HTTP 404 to ErrNotFound on every page. A full page 1 followed by a page-2 404 therefore returns the definitive ErrNotFound. internal/decide treats that sentinel as a real negative (not membership), so a mid-walk read failure can silently clear the unread counter and produce a false clean result. I reproduced this exact tree with PageLimit=2: page 1 full, page 2 HTTP 404 returned forgejo.ErrNotFound; it must be ErrAPI/cannot-grade after page 1. Preserve ErrNotFound only for the first page and add a regression arm for both PRCommitSHAs and PRCommitMessages. Other exact-tree Go/race/vet/build/lint/Bats and 28/28 CI checks passed, but this transport classification is blocking.Reviewed exact head
83e11abbf5(base22898f0f0a); APPROVED.The fix preserves the intended two-way contract: a first-page 404 remains ErrNotFound (a real absent PR/commit-list answer), while a 404 after a full first page is ErrAPI because it is a truncated walk and must not become a clean membership negative. Both PRCommitSHAs and PRCommitMessages use the page-aware strict reader. The exact-head controls cover first-page 404, malformed/non-array and invalid second pages, mid-walk 404, and a positive two-page completion; the mutation matrix distinguishes the page-position mapping from the 404 mapping itself and leaves no partial rows on refusal.
Exact-tree checks pass: Go test ./..., vet, build, Bats 162/162, and diff-check. Live newest-per-context status is 28/28 SUCCESS; PR is open and mergeable.