ceremony/changelog.d/188.md
cluade-reviewer-andresmgsl f2d5fcd565 feat(forge): derive outstanding review requests from the head, not the field
Term 4. GitHub clears requested_reviewers when a verdict lands, so the field
answers "who still owes a verdict" by itself. Forgejo never clears it —
measured: rig!140 listed all three panelists with all three verdicts in, and
rig!146 still lists three while MERGED, so the field is stale even on a
closed PR.

Read raw on Forgejo that is not a cosmetic over-count. `requested` drives
three decisions, and a permanently-true field pins a PR at
state:bots-reviewing for life and stops blocker:unrequested from ever being
true: the sweep believes a round is live forever and no staleness can
correct it.

So the requested set is intersected with who has NOT submitted a verdict for
the current head, derived from /pulls/{n}/reviews — the read that is true on
both forges. On GitHub the filter removes nothing, because the field is
already accurate; term 5 holds by construction rather than by care.

A STALE approval — an approval of an older head — still owes a verdict. That
is the case that matters: treating it as answered would let a stale round
read as complete, which is the shape #136 exists to prevent.

Mutation-verified both ways: reading the field raw again reds three cases,
and treating STALE as answered reds two.

Also documents @grok-reviewer-andresmgsl's ask (#4763): every panel= account
must be able to read the repo, or the forge refuses the review request —
422 naming the account on Forgejo. A real failure mode for private
consumers, and it fails loudly rather than sweeping blind.

Refs #188
2026-08-02 19:48:09 +00:00

2 KiB

Added

  • lib/forge.sh — the forge selector: forge_detect names the forge from the runner's own environment, forge_client names the client it needs, and forge_preflight refuses loudly before any sweep when the two disagree (#188).
  • The reconcilers and labels-scope run that preflight first, so a GitHub-shaped client on a Forgejo instance is a named refusal instead of a sweep that reads nothing and reports success (#188).
  • lib/closes_references.sh — the closing-keyword parser, sibling of refs_references, so "which issues does this PR close" is answered from a PR body rather than from GitHub's GraphQL API (#188).
  • lib/forge-github.sh and lib/forge-forgejo.sh — one call surface, two backends, selected by forge_select; no forge branching at the call sites (#188).
  • The forgejo backend proves each paginated gather complete against the server's x-total-count and refuses loudly when it cannot — a missing header is a refusal, not a pass (#188).

Changed

  • issueflow-reconcile gathers open and merged PRs over REST instead of gh api graphql. Forgejo serves no GraphQL at all, so the two queries were replaced rather than translated; both forges return number and body from /pulls in the same shape (#188).

  • forge_api owns the page size, because each forge silently ignores the other's parameter: per_page=100 reads 30 items on Forgejo and limit=100 reads 30 on GitHub, both HTTP 200. No call site names one (#188).

  • Outstanding review requests are derived from the reviews on the current head rather than from requested_reviewers, which Forgejo never clears — read raw there, a PR would sit at state:bots-reviewing forever (#188).

Fixed

  • labels-reconcile and labels-scope no longer exit 0 on a Forgejo consumer having read zero facts — measured on heavy-duty/rig, where the sweep printed reconciled. over an empty PR list and scope reported "no labeler.yml" for a file that exists (#188).