ceremony/changelog.d/188.md
cluade-reviewer-andresmgsl ab23a3b1b6 feat(forge): two backends behind one call surface, and the shim owns paging
Term 1's foundation. lib/forge.sh gains forge_select, which sources exactly
one of lib/forge-github.sh or lib/forge-forgejo.sh; both define the same
verbs, so no branching reaches the 61 call sites. The github backend is the
current gh invocation extracted 1:1 — term 5 is kept by making that path
boring.

The page size moves OUT of the call sites and into the backend, because it
is not portable and fails silently. Measured 2026-08-02:

  ?per_page=100   GitHub 100 items   Forgejo 30 items  (ignored)
  ?limit=100      GitHub  30 items   Forgejo 50 items  (capped)

Both answer HTTP 200 with valid JSON. Every call site here is GitHub-shaped,
so a verbatim port would have swept 30 of rig's 137 issues and printed
"reconciled." — criterion 2 failing green, the same failure class as the
blind sweep. Both page_url helpers strip a stray page-size parameter in
either dialect, so a call site cannot reintroduce it by accident.

Forgejo caps a page at 50 whatever is asked, so pagination is mandatory, not
an optimisation. The gather is then PROVEN complete against x-total-count
rather than assumed complete because a loop ended.

@kimi-reviewer-andresmgsl's hardening (#4699): a missing x-total-count is
itself a loud refusal. Header exposure is a server setting, and an assert
that cannot run must not silently pass — that is the failure class
re-entering through the guard built to stop it.

Call sites are not ported yet; that is the next commit.

Refs #188
2026-08-02 19:00:58 +00:00

1.7 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).

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).