feat(forge): refuse loudly when the client cannot speak the forge
The preflight half of #188, landed first so it stands alone: the forge is
decided once, before any sweep, and a client that cannot speak it exits
non-zero with a named reason.
Measured against forgejo.heavyduty.builders at 84bb1a4 — two of the three
actions reported SUCCESS having read nothing:
labels-scope exit 0 "no .github/labeler.yml" (the file is HTTP 200)
labels-reconcile exit 0 "reconciled." (zero PRs enumerated)
issueflow-reconcile exit 1 "unexpected end of JSON input"
labels-reconcile's blind-sweep warning (#96) could not fire: it counts
unreadable PRs against a list `gh pr list` never produced, and a process
substitution's failure does not trip set -e, so total stayed 0. Installing
gh makes it worse, silencing the one loud failure.
Detection is measured, not inferred from docs: a real forgejo-runner v6.3.1
job (probe task 278) shows Forgejo populating the whole GITHUB_* namespace,
so GITHUB_ACTIONS proves nothing. GITHUB_API_URL's shape, GITEA_ACTIONS and
GITHUB_SERVER_URL do. The same probe shows the runner image carries neither
gh nor stoke, which is what makes the forgejo backend REST.
Tests declare CEREMONY_FORGE at the forge boundary rather than stubbing gh
and staying silent about the forge — the boundary move term 5 asks for.
Refs #188
2026-08-02 18:29:08 +00:00
|
|
|
### 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).
|
2026-08-02 18:41:03 +00:00
|
|
|
- `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).
|
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
|
|
|
- `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).
|
2026-08-02 18:41:03 +00:00
|
|
|
|
|
|
|
|
### 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).
|
feat(forge): refuse loudly when the client cannot speak the forge
The preflight half of #188, landed first so it stands alone: the forge is
decided once, before any sweep, and a client that cannot speak it exits
non-zero with a named reason.
Measured against forgejo.heavyduty.builders at 84bb1a4 — two of the three
actions reported SUCCESS having read nothing:
labels-scope exit 0 "no .github/labeler.yml" (the file is HTTP 200)
labels-reconcile exit 0 "reconciled." (zero PRs enumerated)
issueflow-reconcile exit 1 "unexpected end of JSON input"
labels-reconcile's blind-sweep warning (#96) could not fire: it counts
unreadable PRs against a list `gh pr list` never produced, and a process
substitution's failure does not trip set -e, so total stayed 0. Installing
gh makes it worse, silencing the one loud failure.
Detection is measured, not inferred from docs: a real forgejo-runner v6.3.1
job (probe task 278) shows Forgejo populating the whole GITHUB_* namespace,
so GITHUB_ACTIONS proves nothing. GITHUB_API_URL's shape, GITEA_ACTIONS and
GITHUB_SERVER_URL do. The same probe shows the runner image carries neither
gh nor stoke, which is what makes the forgejo backend REST.
Tests declare CEREMONY_FORGE at the forge boundary rather than stubbing gh
and staying silent about the forge — the boundary move term 5 asks for.
Refs #188
2026-08-02 18:29:08 +00:00
|
|
|
|
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
|
|
|
- `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).
|
|
|
|
|
|
feat(forge): refuse loudly when the client cannot speak the forge
The preflight half of #188, landed first so it stands alone: the forge is
decided once, before any sweep, and a client that cannot speak it exits
non-zero with a named reason.
Measured against forgejo.heavyduty.builders at 84bb1a4 — two of the three
actions reported SUCCESS having read nothing:
labels-scope exit 0 "no .github/labeler.yml" (the file is HTTP 200)
labels-reconcile exit 0 "reconciled." (zero PRs enumerated)
issueflow-reconcile exit 1 "unexpected end of JSON input"
labels-reconcile's blind-sweep warning (#96) could not fire: it counts
unreadable PRs against a list `gh pr list` never produced, and a process
substitution's failure does not trip set -e, so total stayed 0. Installing
gh makes it worse, silencing the one loud failure.
Detection is measured, not inferred from docs: a real forgejo-runner v6.3.1
job (probe task 278) shows Forgejo populating the whole GITHUB_* namespace,
so GITHUB_ACTIONS proves nothing. GITHUB_API_URL's shape, GITEA_ACTIONS and
GITHUB_SERVER_URL do. The same probe shows the runner image carries neither
gh nor stoke, which is what makes the forgejo backend REST.
Tests declare CEREMONY_FORGE at the forge boundary rather than stubbing gh
and staying silent about the forge — the boundary move term 5 asks for.
Refs #188
2026-08-02 18:29:08 +00:00
|
|
|
### 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).
|