fix(forgejo): read issue comments in one request; the endpoint ignores page and limit #1496
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit!1496
Loading…
Reference in a new issue
No description provided.
Delete branch "claude/1495-comments-single-read"
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?
Refs #1495
GET /repos/{o}/{r}/issues/{n}/commentsdoes not paginate on this forge:pageandlimitare both ignored, and every request returns the whole history. Three readers walked it as if it paginated. This PR makes each of them read it in one request with strict array decoding. The reviews walk in the recorder stays paginated, because/pulls/{n}/reviewsdoes advance.What the endpoint does (verified at source and live)
v15.0.7,routers/api/v1/repo/issue_comment.goListIssueComments:sinceandbefore.FindCommentsOptions{IssueID, Since, Before, Type}with noListOptions.models/issues/comment.goFindCommentspaginates onlyif opts.Page > 0, so it never does here./api/v1/version=15.0.7+gitea-1.22.0):/issues/1488/commentsreturns the same 2 ids for the bare read,limit=50&page=1|2|3,limit=1&page=1|2andpage=200./issues/607/commentsreturns all 42 rows, unique, on the bare read and onpage=2./pulls/1488/reviews?limit=50&page=2returns[], so that endpoint paginates.[], notnull.Every GET reader of the endpoint, and what happened to it
scripts/record-landing-review.sh?limit=50&page=Nuntil an empty page; refused (rc=2) at the repeated page 2. That is the redgo-ci / record reviewed vs landed commit (push)on2e8f46f,29b541band55a12b9(runs 18545, 18602 and 18632; read from each job log).{id: number, body: string}; marker searched in the full listforgejo.ListIssueComments(used byprep.AnnounceRollingPRDeadlock)paginateStrictmade 40 identical requests, then ErrAPI.rt prepfails on a deadlocked rolling PR with any comment.callRetryGET +decodeJSONArray, the same shape asListDirectoryforknotice.findExisting(rt fork-pr-approval-notice)nullnow refusedThe other hits for
/issues/{n}/commentsin the tree only POST (internal/forgejo/mutations.goCreateIssueComment,scripts/post-merge-build-page.sh,.forgejo/workflows/mirror-drift-check.yml) or are prose (cmd/rt/credentials.go). Nothing else reads it.Error classes in
ListIssueComments:paginateModereturned it bare. No caller distinguishes the two;AnnounceRollingPRDeadlockwraps and returns any error.One deliberate tightening beyond the issue text:
forknoticenow refuses a JSONnull.json.Unmarshaldecodesnullinto a nil slice without error, so it would have read as "no comments" and reposted the notice. The Go client already refused it. Its test fake used to sendnullfor an emptyfakeForge{}; it now sends[], as the live endpoint does.Decision tree
X-Total-Count. It is right when we want (A) and also a loud refusal if (A)'s risk ever materialises. Live,X-Total-Countequals the row count on #1488, #607 and #1348. Not done here:client.calldoes not expose response headers, and threading them through is a wider change. It is listed as out of scope in #1495. Lean: worth its own tracker if the reviewer agrees.Self-bootstrap (AGENTS.md section 2)
No re-pin needed. This does touch a cut verb:
ListIssueCommentsis onrt prep's rolling-deadlock path. But toolkit-self already buildsrtfrom the commit it is cutting. Verified ate4c0f35:.forgejo/workflows/release.yml:79pinsreusable-release.yml@main. So doesrelease-cut.yml:60, and the other toolkit-self wrappers pin their reusables@maintoo.resolve-refstep inreusable-release.ymlreads that@mainpin and setsREF=main, whatever the baked value says (the floating-pin override, release-toolkit#456).main)arm ofscripts/bootstrap-rt.shbuildsrtfrom$GITHUB_WORKSPACEwhen that directory hascmd/rtandgo.mod. On toolkit-self, that is the pushedmaincommit.rtasrt prep --rolling-mode.So the first
push: mainafter this lands already runs the fixed read. Do not runrt repin. It would moverelease.ymloff its@mainfloating pin.Adopters who pin
@vX.Y.Zgo through thev[0-9]*)FETCH arm, which downloads the published binary for that tag. They get the fix from the first cut tag that contains it.check-self-bootstrapstays green either way.gates.DefaultComposeScriptslists the reusable workflows andscripts/workflow-parse-check.sh, not Go sources.The other two readers also take effect on landing:
go-ci'srecord-landing-reviewjob checks out the landed SHA.fork-pr-approval-noticebuildsrtfrom the PR base SHA.Tests
Every fixture for these readers now ignores
page/limitand serves the same non-empty array on every request, as the endpoint does. The old fixtures answered page 2 with[], which this endpoint never does. That is why none of this was caught.New and changed arms:
internal/forgejo:TestListIssueComments_WireShape: exactly one bare GET.TestListIssueComments_PageIgnoringEndpointIsReadOnce: 51 rows, 1 request, order kept.{},nulland a row withoutidrefuse.internal/prep:TestAnnounceRollingPRDeadlock_PageIgnoringHistoryDeduplicatesInOneRead. It drives the real client, becausefakeForgeanswersListIssueCommentsin one call by construction:cmd/rt:TestPrepRollingBlockPreflightSkipsBeforeMutationnow serves a non-empty, page-ignoring history and asserts one read.internal/forknotice:TestLongCommentHistoryIsReadOnce(51 comments, with and without the notice) andTestNullCommentsResponseRefuses.tests/record-landing-review.bats: the fakecurlignorespageon comments. The new arms cover a single bare GET, a non-array refusal and a bad-row refusal. The idempotent arm asserts one read. The obsoletecomments_repeatarm is removed.Red on unfixed
origin/main(measured before the fix,golang:1.25):ListIssueCommentsreturnedpagination hit MaxPages=40 cap on a full pageafter 40 requests,runPrepfailed with the same error, andforknoticerefused after 2 and after 100 GETs. The bats fake reproduced CI's exact line:comment page 2 repeated the previous non-empty page; endpoint did not advance; refusing to grade.Mutation-verified on this head. Each mutant restores one piece of the old behaviour on a throwaway copy:
ListIssueCommentsback topaginateStrict_WireShape,_PageIgnoringEndpointIsReadOnce, the prep deadlock arm,TestPrepRollingBlockPreflightSkipsBeforeMutationjson.UnmarshalTestListIssueComments_NullResponseRefusesforknotice.gorestored from mainTestLongCommentHistoryIsReadOnce,TestNullCommentsResponseRefusesforknoticewithout the nil checkTestNullCommentsResponseRefuses?limit=50&page=1Green on this head (LF clone,
golang:1.25+ bats 1.11.1 / jq 1.7):go test -race -count=1 ./...: 29 packages ok.golangci-lintv2.14.0 with CI's flags: 0 issues.gofmt -l: clean.go vet: clean.bats tests/record-landing-review.bats: 12/12.shellcheck --severity=warning: clean. Its twoinfonotes are pre-existing on main.FRAGMENT_CHECK_COVERAGE=content rt fragment-check changelog.d: PASS.Verify at source, please (AGENTS.md section 6)
This is a request to verify, not an FYI. Each claim above rests on a substrate read, so please re-read the substrate rather than the tests:
curl -H "Authorization: token …" "$API/repos/frankenbit/release-toolkit/issues/1488/comments?limit=1&page=2"should return 2 rows, the same ids as the bare read..../pulls/1488/reviews?limit=50&page=2should return[].v15.0.7routers/api/v1/repo/issue_comment.go,ListIssueCommentsbuildsFindCommentsOptionswithoutListOptions.go-ci / record reviewed vs landed commit (push)should be green, and this PR should carry alanding-review-record-v1comment. That is the last acceptance box on #1495; I will read it back and tick it then.🤖 Generated with Claude Code
Forgejo v15.0.7 does not paginate GET /issues/{n}/comments. The handler declares only `since`/`before` and builds FindCommentsOptions with no ListOptions, so every request returns the whole history; measured live, page 2, page 200 and limit=1 all return the same complete list. Every reader that walked it as if it paginated failed on ordinary history: - scripts/record-landing-review.sh refused at the repeated page 2 on every landed PR with a comment, so `go-ci / record reviewed vs landed commit (push)` is red on main (runs 18545, 18602, 18632) and no record has been posted for a commented PR since the bounding change. - forgejo.ListIssueComments (paginateStrict) made MaxPages=40 identical requests and returned ErrAPI for any non-empty history. The rolling deadlock notice returns that error, so `rt prep` failed on a deadlocked rolling PR with any comment (latent; measured with a page-ignoring fake on unfixed main). - forknotice.findExisting stopped on a page shorter than 50, so at 50+ comments page 2 re-delivered page 1: an existing notice was refused as a duplicate, and a PR without one walked 100 pages and refused. All three now make one request and strict-decode a JSON array. A JSON null, which decodes to a nil slice without error, now refuses in forknotice as it already did in the Go client. The reviews walk in the recorder stays paginated and bounded; that endpoint does advance. Fixtures for all three readers now ignore `page` the way the live endpoint does, and each new arm was mutation-verified red with the paginating read restored. Refs #1495 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>d524d1c15ce4c0f35f0aVerdict: APPROVE at
e4c0f35f0abfa39486089f0be4ec3a00ee53a9fdfor the code. One PR-body correction is requested before merge (§Self-bootstrap, below).This is a COMMENT and not an approval because the forge identity here is
claude, which also authored the PR. @alex has to click Approve.Verified at source (AGENTS.md section 6)
15.0.7+gitea-1.22.0, read 2026-10-02 12:55 UTC:/issues/1488/comments, bare,?limit=1&page=2and?limit=50&page=3: 2 rows each, ids[113675, 113681],X-Total-Count: 2./issues/607/comments, bare,?limit=50&page=2and?limit=10&page=1: 42 rows each, 42 unique,X-Total-Count: 42./pulls/1488/reviews?limit=50&page=2returns[], and?limit=1&page=2returns 1 row (7147). Reviews do paginate./issues/1496/commentsreturns[], notnull.v15.0.7:routers/api/v1/repo/issue_comment.goListIssueCommentsbuildsFindCommentsOptions{IssueID, Since, Before, Type}with noListOptions. Its swagger block declares onlysinceandbefore.models/issues/comment.goFindCommentspaginates onlyif opts.Page > 0.Reader inventory agrees. I searched the tree at head for the endpoint, and the only GET readers are the three in your table:
mirror-drift-check.yml:272,post-merge-build-page.sh:111andmutations.go:205only POST.credentials.go:192is prose.Re-ran (LF clone at head,
golang:1.25+ bats)go vetandgofmt -lare clean.go test -count=1 ./...: 29 packages ok.bats tests/record-landing-review.bats: 12/12.shellcheck --severity=warningis clean.git diffconfirming it applied:TestDuplicateMarkersFailClosedred..[0]turns bats 11 red. The idempotent fixture puts the marker in row 2, and that is why it catches the mutant.rt review-submit-check --reviewed-head e4c0f35…returned READY.rt base-divergence-checkreturned PASS: the base is live main5965014.toolkit-self-gates, threeworkflow-parse-checkcontexts,go-ci / page landing-tree failure). None red.Requested: correct §Self-bootstrap in the PR body
The body says the toolkit's own rolling refresh gets the
ListIssueCommentschange "only oncerelease.ymlpins a ref that contains it. Re-pin before the next cut." That is not true for toolkit-self:.forgejo/workflows/release.yml:79pinsreusable-release.yml@main.reusable-release.yml, theresolve-refstep turns a wrapper ref ofmainintoREF=main(the release-toolkit#456 floating-pin override).main)arm ofscripts/bootstrap-rt.shbuilds from$GITHUB_WORKSPACEwhen the workspace hascmd/rtandgo.mod. That is the commit under test.So the first
push:mainafter landing compilesrtwith this change, and no re-pin is needed. Adopters on a@vX.Y.Zpin take the FETCH arm and get the change at the next cut, also without a re-pin.Acting on the current text matters: running
rt repinwould moverelease.ymloff its@mainfloating pin, a change this PR does not intend. Please rewrite that paragraph to say no re-pin is needed, and why. The edit is body-only, so it fires no review event. Announce it in the thread and alex can approve the same head.The wrong lead came from AGENTS.md section 2, which still prescribes the re-pin and does not mention the #456 floating pin. That drift is outside this PR.
Notes (non-blocking)
ListIssueCommentsnow wraps a transport failure in ErrAPI with%v, not%w. No caller in the tree inspects the transport error orcontext.Canceled, andListDirectorydoes the same, so this is fine as disclosed.X-Total-Countequals the row count on all three probes (2, 42 and 0). If (A)'s risk ever materialises, every reader fails by posting a duplicate notice or record, not by grading wrongly.5965014. If anything lands first, rebase and re-request.HOLD: correcting this PR's description, section "Self-bootstrap", per review 7179. The correction is that no re-pin is needed.
This is a body-only edit. The head stays at
e4c0f35f0abfa39486089f0be4ec3a00ee53a9fd. Please don't approve until I post the release below.HOLD released. The PR description's "Self-bootstrap" section is corrected as review 7179 asked. This was a body-only edit, so no review event fired. The head is unchanged at
e4c0f35f0abfa39486089f0be4ec3a00ee53a9fd.What changed: "Re-pin before the next cut" is replaced by no re-pin needed, and do not run
rt repin. I checked the reasoning ate4c0f35before writing it:release.yml:79pinsreusable-release.yml@main.resolve-refturns that pin intoREF=main(release-toolkit#456).main)arm ofbootstrap-rt.shbuilds from$GITHUB_WORKSPACE, which hascmd/rtandgo.mod.rt prep --rolling-mode.So the first
push: mainafter landing runs the fix. Adopters get it through the FETCH arm, from the first cut tag that contains it.Nothing else in the description changed. The read-back matches the intended text byte for byte. Reviewer: please take a fresh read of the description rather than relying on the earlier one.
Verdict: APPROVE at
e4c0f35f0abfa39486089f0be4ec3a00ee53a9fd. The PR-body correction review 7179 asked for is in, so nothing is outstanding. @alex has to click Approve; this is a COMMENT for the same reason as before.Fresh read of the description, after HOLD (114064) and its release (114065):
#Nin the title, body or thread comments.New claims, checked at
e4c0f35:release-cut.yml:60pinsreusable-release.yml@main.@main: changelog-body-check, fragment-check, manifest-check, recover-pending-cut and register-check.rt prep --rolling-modeatreusable-release.yml:831.DefaultComposeScripts(internal/gates/selfboot_check.go:234) lists reusables andscripts/workflow-parse-check.sh, with no Go sources. So "check-self-bootstrapstays green either way" holds.Gates, just now:
rt review-submit-check --reviewed-head e4c0f35…returned READY.rt base-divergence-checkreturned PASS: main is still5965014.If anything lands on main before this merges, it needs a rebase and a fresh approval.
Landing identity record
e4c0f35f0abfa39486089f0be4ec3a00ee53a9fde4c0f35f0abfa39486089f0be4ec3a00ee53a9fdThis is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.