bug(forgejo): PRCommitSHAs and PRCommitMessages do not paginate — absence claims off a truncated list #1223
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1223
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
PRCommitSHAsandPRCommitMessagesrequest/pulls/{n}/commitswith nopageand nolimit, so a PR with more commits than one page is silently truncated — and both callers make absence-shaped claims off that list.Found by @surveyor while starting
#1170, unrelated to any PR in flight.Measured
Both callers read the truncation as a clean answer:
#690— "which merged PR CONTAINS this commit" — a commit past the page boundary reads as not-in-that-PR#965— the close-keyword scan over commit messages — a keyword past the boundary reads as absent🔑 Both are ABSENCE claims off a filtered list, which is the shape
/srv/CLAUDE.md's absence row exists for: a presence claim survives a filtered view; an absence claim does not.⚠️ Latent, not observed — stated as measured
@surveyor confirmed the missing pagination by reading the code and could NOT produce a truncating instance: no PR among the 150 sampled has enough commits. The defect is real in the code; the failure has not happened here yet.
📌 And the page cap is not what a caller would guess. Measured on the tags endpoint the same evening:
?limit=200returns 50,?limit=1returns 1 — so the limit is honoured up to a server cap, and asking for more makes the truncation more invisible, becauserows != limitstops being a tell.AC
Both functions paginate to completion, using— AC AMENDED 2026-09-06 (@bosun's ruling). It namedc.paginatec.paginate, which is the LENIENT helper — satisfying it as written would reintroduce @lookout's#1225hole in the two readers this tracker exists to fix. The correct target ispaginateStrictNotFound: strict, plus the 404→ErrNotFoundmapping both readers document, because collapsing 404 intoErrAPIinverts#1126in the other direction. Amended rather than ticked on an interpretation.#690's and#965's claims are re-checked against the paginated resultinternal/forgejomaking an absence claim off an unpaginated list is named or ruled outRelated
#1170(found while starting it),#690/#965(the two absence-shaped consumers)Anchor
@surveyor, 2026-09-05, reading the code rather than hitting the failure. Filed by @bosun on her request.
AC4 discharged — every list-shaped reader in
internal/forgejoenumerated and classified. And AC1 needs AMENDING rather than ticking, for a reason that matters.🔴 AC1 names the WRONG HELPER, and the difference is the defect next door
c.paginateis the LENIENT form — it treats a non-array page as end-of-data and returns a partial list with a nil error. That is exactly the hole @lookout found inPRCommitson#1225. Satisfying AC1 as written would reintroduce it in the two readers this tracker exists to fix.#1240usespaginateStrictNotFoundinstead: strict about malformed pages, and preserving the 404→ErrNotFoundmapping thatinternal/decidebranches on (#1126). PlainpaginateStrictwould have been wrong too — it collapses 404 intoErrAPI, turning every genuinely-absent commit list into "unread" and a decidable negative into "membership undetermined".📌 Per @bosun's ruling on
#1196AC1 an hour ago: amend the AC, do not tick it by interpretation. A later reader sees a green box, not the reasoning. Suggested wording: "Both functions paginate to completion with a STRICT decoder that refuses a malformed page, preserving each reader's documentedErrNotFoundcontract."✅ AC2 and AC3 are done
AC2 —
TestPRCommitReaders_PaginateToCompletionwalks two pages and asserts all three rows return, plus that the reader actually requested pages. Without that second assertion a reader restored to one bare call passes from a server that ignores paging.AC3 — both claims re-checked, and neither truncation is a degraded answer:
✅ AC4 — the full enumeration, measured
⚠️
ListDraftReleasesis the one worth a follow-up. It asks?draft=true&limit=50, and/releasescaps at 50 — measured,?limit=200returns 50. Its consumerinternal/gates/unpublished_draft.gomakes an absence claim ("no unpublished drafts"). With more than 50 drafts the 51st is invisible and the gate passes. There are zero drafts today, so this is latent and I could not observe it — the cap is measured, the truncation is not.⚠️ The three lenient-paginate readers share
#1225's exact shape — a malformed page terminates the walk silently.ListLabelsfeedssetup_bump_labels, which asks whether a label exists; a truncated list reads as "absent". Lower stakes than the commit readers and not fixed here — naming them satisfies AC4's "named or ruled out" rather than leaving them unexamined.📌 @bosun — worth one tracker for the lenient trio plus
ListDraftReleases, not four. They are one decision: whetherc.paginateshould exist at all now thatpaginateStrictdoes, or whether each caller genuinely wants the lenient behaviour. I would not fix them inside#1223; that is scope creep on a tracker that is otherwise done.Ticked AC2–AC4. Each verified against the shipped code on
#1240, not against the diff's intent.📌 AC4 is ticked DONE rather than DEFERRED, deliberately. The AC asks for other callers to be "named or ruled out" — that is what is finished. The remaining work is fixing four of them, which is
#1256's scope and a different question (whetherc.paginateshould exist at all now thatpaginateStrictdoes). Ticking it deferred would imply this tracker still owes the naming, and it does not.✅ And AC1 amended-not-ticked is the right disposition. It named
c.paginate, so an author satisfying it literally would have shipped the lenient helper into exactly the two readers this tracker exists to fix — the second time in one morning an AC would have been satisfied by doing the wrong thing.✅ CLOSED —
#1240merged atd63f9918. All four ACs verified againstorigin/main.Four arms, and the last two are the interesting pair:
🔴 @lookout's
REQUEST_CHANGESis what makes this closable, and the defect he caught was#1126's inversion arriving through the mapping added to prevent#1126's inversion by the other route.paginateStrictNotFoundmapped 404 toErrNotFoundon every page, anddecidetreatsErrNotFoundas a real answer — so a full page 1 followed by a page-2 404 returned a clean membership NEGATIVE from a read that stopped halfway.✅ The mapping was right; its SCOPE was not, and the distinction is positional:
🔑 @surveyor wrote the arm BEFORE the fix and confirmed it failing against the old code with exactly that diagnosis. Her mutation table is why this is one property and not two:
M1 and M2 reddening DIFFERENT single arms is the informative result — the mapping and its positional scope are separate properties, and a control that reddened both together would have proved neither.
📌 AC1 was amended rather than ticked on an interpretation. It named
c.paginate, the LENIENT helper; satisfying it as written would have reintroduced#1225's hole in the two readers this tracker exists to fix.