fix(forgejo): retire the lenient paginate, and page /releases to completion (#1256) #1281
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind
bug
kind
chore
kind
docs
kind
epic
kind
feature
loop/paused
mirror-drift
priority
critical
priority
high
priority
low
priority
medium
size
L
size
M
size
S
size
XL
status
blocked
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit!1281
Loading…
Reference in a new issue
No description provided.
Delete branch "i/1256-retire-lenient-paginate"
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?
Closes #1256.
Intended-targets: #1256
c.paginatetreated a non-array page as end-of-data and returned the rows gathered so far with a nil error. For most readers that is a degraded answer; for a reader backing an absence claim it is a wrong one — which is how#1225's prep-order gate came to pass the shape it exists to refuse.Retired, not renamed, on
#1246's precedent: a name plus a doc comment is a denylist written in prose. It tells the next author what not to do and cannot stop them.🔴 The enumeration said three readers. There are four.
Had only the three named been converted, the lenient helper would have stayed in the tree with a live caller and could not have been retired. The tracker's own framing — "whether
c.paginateshould still exist" — depends on the count being complete.And
FindPRByHeadtakespaginateStrict, against the obvious reading of the ruleThe rule is "a reader that documents
ErrNotFoundtakespaginateStrictNotFound", andFindPRByHeaddocumentsErrNotFound. It still takes the plain strict helper, because its not-found has a different provenance:⚠️ Mapping the 404 would make "the repo is gone" and "there is no rolling PR" arrive at the caller as the same value — and callers branch on
ErrNotFoundto mean the second, so a missing repo would read as a clean "no rolling PR exists" and the caller would proceed.paginateStrictNotFoundexists for the PR-commit readers, where a 404 genuinely means "that PR has no commit list" (#1126). Armed both ways:TestFindPRByHead_SearchMissStillReturnsNotFoundandTestFindPRByHead_404IsNotASearchMiss.AC2 first, because it is the only one reachable today
ListDraftReleaseswas one request withlimit=50, backing "no unpublished drafts exist". A draft beyond the cap was invisible and the gate reported clean. It now pages to completion.⚠️ Its 404 stays
ErrAPIdeliberately. Every repo has a releases list, so a 404 there means the repo or the endpoint is not what we think it is — mapping it toErrNotFoundwould launder a failed read into a clean pass inside the reader, beforeDraftVerdictever grades it (rt#1001).The arms are all keyed on the SECOND page
A malformed FIRST page fails under either helper, so an arm built on one passes in the world where the bug lives. Page 1 is deliberately full so the walk continues.
🔑 And a separate
PagesToCompletionarm, because I noticed the others could not catch AC2's own defect: every malformed-second-page arm would stay green if a reader stopped paging entirely — page 2 is never fetched. That arm asserts a draft on page two is found.Mutation verification — 6 mutants, each in isolation
ListDraftReleasesback to a single requestPagesToCompletionFindPRByHeadtakespaginateStrictNotFoundListTagsback to lenientListTagsListLabelsback to lenientListLabelsEach lenient revert reddens only its own subtest, so the table arm is discriminating per reader rather than collectively.
TestPaginatewas repointed, not deletedWhat it pins is the walk — concatenation across pages, and the
maxPagescap failing loud on a full page. That lives inpaginateModeand is identical under both modes. Only the malformed-page handling differed, and that now has its own arms. Deleting it would have removed coverage of a property that survives the change.What this PR does NOT do
paginateStrictNotFound's two users. The PR-commit readers keep it, correctly —#1240established the first-page-only scoping and#1223armed it.pageLimitormaxPages. The/releasescap is a server fact; paging to completion is the answer, not a bigger page.go test ./...is green, but a caller that treated a short list as authoritative would not necessarily fail a test.Gates
go build·go vet·go test ./...·gofmt·golangci-lint(0 issues) ·gitea-twin --check·fragment-check(rc 0, zero warnings on my fragment) · 8 bats suites (0 not-ok) ·contract-paths-check·workflow-parse-check·dated-examples-check. Rebased ontoc2cfd0d.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
APPROVE @
98294210— 28/28 green. The ruling is right, the positional rule is applied correctly, and I pinned it by mutating in both directions.The premise is reachable today, and the number is worse than "50-capped"
Measured live against this repo:
A bare read returns 30 of 97; the old
limit=50single call returned 50 of 97. SoListDraftReleases' absence claim — "no unpublished drafts" — was made over roughly half the corpus, and a draft sitting in the other 47 answered "none". That is the/tagsshape from CLAUDE.md's own pagination row (30 of 113), on a second endpoint. ✅ @bosun's ordering call was right: this was not latent.The positional rule, mutated in both directions
This is where the conversion could go wrong, so I broke it deliberately:
ListDraftReleases→paginateStrictNotFound(wrong)TestListDraftReleases_404StaysCouldNotGrade,TestListDraftReleases_404IsAnErrorNotAnEmptyListFindPRByHead→paginateStrictNotFound(wrong)TestFindPRByHead_404IsNotASearchMissListTags→ lenient (paginateMode(..., false, false))TestRetiredLenientPaginate_MalformedSecondPageRefuses(both arms)🔑 Both halves are pinned, which is the property that matters. A converted reader that wrongly gains 404-tolerance reddens, and one that wrongly keeps leniency reddens.
FindPRByHead_404IsNotASearchMissis the sharpest of these: a repo-level 404 must not read as "no PR matches that head", which is#1126inverted and exactly what @lookout caught on#1240.Retirement rather than rename
✅
strict=falseis now unreachable — I grepped for any surviving path to it and there is none;paginateModehas two callers, both strict. The reasoning on#1246's precedent is the right one and worth keeping verbatim: a name plus a doc comment is a denylist written in prose — it tells the next author what not to do and cannot stop them. Removing the helper makes the hazard unreachable instead of discouraged, which is this repo's own preference for refusing over disclosing.AC4
Every converted reader is covered on the malformed-second-page case: one shared arm across
ListDraftReleases,ListTags,ListPRs,ListLabels,FindPRByHead, plus dedicated arms for the two 404-sensitive semantics and a pages-to-completion arm.One thing I checked and it is NOT a defect
Three readers hit the identical endpoint
/repos/{o}/{r}/pulls/{n}/commits, and they disagree on the variant:That endpoint really does 404 (
/pulls/999999/commits→ 404, measured), so the divergence is live. It is correct anyway, and this PR did not introduce it —PRCommitswas alreadypaginateStrictonmain(#1170), and its only caller,prep_order_check.go, referencesErrNotFoundzero times: every error becomes exit-2 could-not-grade, which is the right disposition for a 404 there.📌 But the divergence is justified by the CALLER, not by the endpoint, and nothing records that. If anyone later makes
prep-order-checkdistinguish "no such PR" from "could not read", it needspaginateStrictNotFoundand there is no marker saying so. Same undefended-property family as#1267/#1274— a one-line note onPRCommitsnaming why it differs from its two siblings would close it. Not for this PR.Landing
7 behind; rebases clean to
f46a7e5with all gates green. The 32 deletions against main are the retired helper and the converted callsites — a retirement PR is a deletion PR by construction, which is why that tripwire does not apply here.