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
67 lines
2.6 KiB
Bash
67 lines
2.6 KiB
Bash
#!/usr/bin/env bash
|
|
# lib/forge-github.sh — the GitHub backend (issue #188, term 1). Sourced by
|
|
# lib/forge.sh when forge_detect says github; never at the same time as the
|
|
# forgejo backend — they define the same verbs on purpose.
|
|
#
|
|
# This file is the CURRENT call set, extracted 1:1 and nothing more. Term 5
|
|
# of the frozen Spec is "GitHub consumers are unchanged", and the cheapest
|
|
# way to keep that true is for every verb here to be a thin pass-through to
|
|
# the `gh` invocation the call site used before the port. No behaviour is
|
|
# added, fixed or tidied on this path; anything that looks like an
|
|
# improvement here is a regression risk against a forge nobody is currently
|
|
# reporting bugs on.
|
|
|
|
# forge_api [--paginate] <endpoint> [--jq <expr>]
|
|
#
|
|
# The one deliberate difference from a pure pass-through: the caller no
|
|
# longer names a page size, because the page-size parameter is not portable
|
|
# and is therefore the backend's to own (#188).
|
|
#
|
|
# ?per_page=100 GitHub: 100 items Forgejo: 30 items (IGNORED)
|
|
# ?limit=100 GitHub: 30 items Forgejo: 50 items (capped)
|
|
#
|
|
# Both answer HTTP 200 either way, so a call site that names one is a silent
|
|
# truncation waiting for the other forge. per_page=100 is injected here —
|
|
# exactly what the call sites said before — so the GitHub path is unchanged
|
|
# in behaviour while the parameter stops being a call-site concern.
|
|
forge_api() {
|
|
local paginate=false endpoint="" jqexpr="" have_jq=false
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--paginate) paginate=true ;;
|
|
--jq) jqexpr="$2"; have_jq=true; shift ;;
|
|
-*) ;;
|
|
*) [ -n "$endpoint" ] || endpoint="$1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
[ -n "$endpoint" ] || { echo "forge_api: endpoint required" >&2; return 1; }
|
|
|
|
if [ "$paginate" = true ]; then
|
|
endpoint="$(github_page_url "$endpoint")"
|
|
if [ "$have_jq" = true ]; then
|
|
gh api --paginate "$endpoint" --jq "$jqexpr"
|
|
else
|
|
gh api --paginate "$endpoint"
|
|
fi
|
|
else
|
|
if [ "$have_jq" = true ]; then
|
|
gh api "$endpoint" --jq "$jqexpr"
|
|
else
|
|
gh api "$endpoint"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# github_page_url <endpoint> — pure, so the page-size contract is testable
|
|
# without a network. Strips any page-size parameter a caller left behind in
|
|
# either dialect, then applies GitHub's own.
|
|
github_page_url() {
|
|
local endpoint="${1:?github_page_url: endpoint required}" clean
|
|
clean="$(printf '%s' "$endpoint" | sed -E 's/([?&])(per_page|limit|page)=[0-9]+/\1/g; s/[?&]+$//; s/([?&])&+/\1/g')"
|
|
case "$clean" in
|
|
*\?) printf '%sper_page=100\n' "$clean" ;;
|
|
*\?*) printf '%s&per_page=100\n' "$clean" ;;
|
|
*) printf '%s?per_page=100\n' "$clean" ;;
|
|
esac
|
|
}
|