ceremony/test/refs-not-closing.test.sh

199 lines
7.9 KiB
Bash
Raw Normal View History

2026-08-03 20:29:51 +00:00
#!/usr/bin/env bash
# Contract tests for actions/refs-not-closing (issue #218). Bodies and
# closing-reference sets are fixtures: no network and no pull request are
# involved. set -u, not -e: failures are behavior for the harness to inspect.
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=test/harness.sh
. "$ROOT/test/harness.sh"
SCRIPT="$ROOT/actions/refs-not-closing/refs-not-closing.sh"
ACTION="$ROOT/actions/refs-not-closing/action.yml"
ENTRYPOINT="$ROOT/actions/refs-not-closing/run.sh"
2026-08-03 20:29:51 +00:00
WORKFLOW="$ROOT/.github/workflows/refs-guard.yml"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
body() {
local name="$1"
shift
printf '%s\n' "$@" >"$TMP/$name.md"
}
guard() {
local name="$1"
shift
bash "$SCRIPT" "$TMP/$name.md" "$@"
}
body ref-5 'Refs #5'
check "Refs target with empty closing set passes" 0 "no Refs target" guard ref-5
check "Refs target with itself closing fails" 1 "#5" guard ref-5 5
check "Refs target with another issue closing passes" 0 "no Refs target" guard ref-5 9
body ordinary 'Closes #5'
check "ordinary Closes PR remains green" 0 "no Refs target" guard ordinary 5
body mixed 'Refs #5' '' 'This PR legitimately Closes #9.'
check "Refs #5 plus Closes #9 remains green" 0 "no Refs target" guard mixed 9
body prose 'Refs #5' '' 'Triage closes #5 by hand after the live proof.'
check "closing prose for a Refs target fails" 1 "closes #5" guard prose 5
check "failure prints the surrounding sentence" 1 \
"sentence: Triage closes #5 by hand after the live proof" guard prose 5
check "failure offers number-first rewrite" 1 "#N is" guard prose 5
check "failure offers number-free rewrite" 1 "closes the issue" guard prose 5
2026-08-03 20:34:35 +00:00
body code-span 'Refs #5' '' "The body must not contain \`Closes #5\` anywhere."
check "backticked closing keyword is reported as the match" 1 \
"matched: Closes #5" guard code-span 5
2026-08-03 20:29:51 +00:00
check "backtick failure explains that code spans do not protect" 1 \
"Backticks do not protect" guard code-span 5
body adjacency 'Refs #5' '' 'Triage closes #9 and #5 after the proof.'
check "non-adjacent #5 does not join closing set #9" 0 "no Refs target" \
guard adjacency 9
body empty ''
check "empty body remains green" 0 "no Refs target" guard empty 5
body incidents-211 'Refs #209' 'Triage closes #209 by hand.'
check "#211 incident replays red" 1 "#209" guard incidents-211 209
body incidents-214 'Refs #212' 'Triage closes #212 and #209 on that evidence.'
check "#214 incident replays red" 1 "#212" guard incidents-214 212
2026-08-03 20:34:35 +00:00
body incidents-200 'Refs #199' "A later edit added \`Closes #199\`."
2026-08-03 20:29:51 +00:00
check "#200 incident replays red" 1 "#199" guard incidents-200 199
body multiple 'Refs #5 and Refs #7.' 'Triage closes #5 and fixes #7 by hand.'
check "failure names every intersecting issue" 1 \
"scheduled to close: #5 #7" guard multiple 5 7
body soft-wrap 'Refs #5' '' 'Triage closes' '#5 by hand after the live proof.'
check "soft-wrapped closing prose is reported as one sentence" 1 \
"sentence: Triage closes #5 by hand after the live proof" \
guard soft-wrap 5
body refs-colon 'Refs: #5' '' 'Triage closes #5 after proof.'
check "Refs colon form is protected" 1 "matched: closes #5" \
guard refs-colon 5
body refs-link 'Refs [#5](https://example.test/issues/5)' '' \
'Triage closes #5 after proof.'
check "linked Refs form is protected" 1 "matched: closes #5" \
guard refs-link 5
2026-08-03 20:29:51 +00:00
for number in 207 191 190 176 165 164; do
body "incident-$number" "Refs #$number"
check "#$number incident replays green" 0 "no Refs target" \
guard "incident-$number"
done
check "missing body is a loud failure" 1 "missing or unreadable" \
bash "$SCRIPT" "$TMP/missing.md"
check "invalid closing set is a loud failure" 1 "invalid closing issue" \
guard ref-5 nope
# The action owns the network boundary. Drive its executable entrypoint with
# a fake `gh` so failures are behavioral assertions, not YAML text guesses.
mkdir -p "$TMP/bin"
cat >"$TMP/bin/gh" <<'EOF'
#!/usr/bin/env bash
set -u
# Every call is recorded, so a probe can assert the gather did NOT run — a
# refusal that still reads is not a refusal (#198).
[ -z "${GH_CALL_LOG:-}" ] || printf '%s\n' "$*" >>"$GH_CALL_LOG"
case "${FAKE_GH_MODE:-success}" in
failure)
echo "fake GraphQL read failed" >&2
exit 42
;;
partial)
has_next=true
;;
success)
has_next=false
;;
*)
echo "unknown fake mode: ${FAKE_GH_MODE:-}" >&2
exit 2
;;
esac
printf '{"data":{"repository":{"pullRequest":{"body":"Refs #5","closingIssuesReferences":{"nodes":[],"pageInfo":{"hasNextPage":%s}}}}}}\n' "$has_next"
EOF
chmod +x "$TMP/bin/gh"
action_boundary() {
local mode="$1"
merge upstream 0.6.0 onto the forge tree, and port every gh call site it brought (#198) `git merge` of upstream `8c3a4d1` onto `dad99dd`, common ancestor `84bb1a4`. 18 hunks in 10 files; `lib/forge.sh`, `lib/forge-github.sh` and `lib/forge-forgejo.sh` conflict in none and come out byte-identical. The resolutions the issue decided: VERSION and both CEREMONY_SELF_REF carriers take upstream's numbers; `.github/labels.conf` and `drills/0.4.1.md` keep this forge's; CHANGELOG keeps both sides and names the upstream commit this tree carries. The part the hunks did not contain. Upstream's 0.5.0/0.6.0 work added whole functions to files this tree already owned, so `git merge` took its side without raising a conflict — and with them, EIGHT runtime `gh` call sites that #188 had removed. Seven are ported onto the shim: two reads and four comment writes in issueflow-reconcile, and labels-reconcile's HEAD_COMMIT_AT read. The eighth is `gh workflow run` in labels.yml, which a workflow cannot declare a client for and whose Forgejo equivalent this instance answers with 500 rather than a 4xx — named with its reason rather than ported on a guess. test/no-runtime-gh.test.sh makes the rule mechanical, because reviewing the diff could not: four reviewers reading it each found a different subset, and the contract suite stubs `gh`, so a reintroduced call site passes it. Three seams the resolution decides are silent when resolved wrongly, and each now has a case that fails on the wrong one: the merged record's `merged_at` third column (without it every sort key ties and the highest PR number comes back), the open gather's one-BODY-row-per-line feed (a whole decoded body as one record loses every declaration including the first), and the whole-board read whose COLLISION_FLAGS/WINDOW_FLAGS consumers auto-merged. The open gather carries CLOSING rows as well as BODY rows. `Refs` alone would drop every `Closes #N` link on the open side and reclaim a claim the PR was holding — the existing base64 round-trip case is red without it. actions/refs-not-closing declares CEREMONY_FORGE_CLIENT=gh: its only gather is GraphQL, which Forgejo does not serve at all. #199 ports it. test/run.sh: 28 test files, 0 failed. shellcheck and actionlint clean. Refs #198
2026-08-05 11:56:23 +00:00
# CEREMONY_FORGE=github is the environment this matrix has always assumed
# implicitly — it stubs `gh`. It is explicit now only because the entrypoint
# declares CEREMONY_FORGE_CLIENT=gh and preflights it (#198 spec 4); the
# incident matrix below is unchanged.
env PATH="$TMP/bin:$PATH" FAKE_GH_MODE="$mode" \
merge upstream 0.6.0 onto the forge tree, and port every gh call site it brought (#198) `git merge` of upstream `8c3a4d1` onto `dad99dd`, common ancestor `84bb1a4`. 18 hunks in 10 files; `lib/forge.sh`, `lib/forge-github.sh` and `lib/forge-forgejo.sh` conflict in none and come out byte-identical. The resolutions the issue decided: VERSION and both CEREMONY_SELF_REF carriers take upstream's numbers; `.github/labels.conf` and `drills/0.4.1.md` keep this forge's; CHANGELOG keeps both sides and names the upstream commit this tree carries. The part the hunks did not contain. Upstream's 0.5.0/0.6.0 work added whole functions to files this tree already owned, so `git merge` took its side without raising a conflict — and with them, EIGHT runtime `gh` call sites that #188 had removed. Seven are ported onto the shim: two reads and four comment writes in issueflow-reconcile, and labels-reconcile's HEAD_COMMIT_AT read. The eighth is `gh workflow run` in labels.yml, which a workflow cannot declare a client for and whose Forgejo equivalent this instance answers with 500 rather than a 4xx — named with its reason rather than ported on a guess. test/no-runtime-gh.test.sh makes the rule mechanical, because reviewing the diff could not: four reviewers reading it each found a different subset, and the contract suite stubs `gh`, so a reintroduced call site passes it. Three seams the resolution decides are silent when resolved wrongly, and each now has a case that fails on the wrong one: the merged record's `merged_at` third column (without it every sort key ties and the highest PR number comes back), the open gather's one-BODY-row-per-line feed (a whole decoded body as one record loses every declaration including the first), and the whole-board read whose COLLISION_FLAGS/WINDOW_FLAGS consumers auto-merged. The open gather carries CLOSING rows as well as BODY rows. `Refs` alone would drop every `Closes #N` link on the open side and reclaim a claim the PR was holding — the existing base64 round-trip case is red without it. actions/refs-not-closing declares CEREMONY_FORGE_CLIENT=gh: its only gather is GraphQL, which Forgejo does not serve at all. #199 ports it. test/run.sh: 28 test files, 0 failed. shellcheck and actionlint clean. Refs #198
2026-08-05 11:56:23 +00:00
CEREMONY_FORGE=github \
GITHUB_REPOSITORY="heavy-duty/ceremony" PR_NUMBER=268 \
GITHUB_ACTION_PATH="$ROOT/actions/refs-not-closing" \
bash "$ENTRYPOINT"
}
merge upstream 0.6.0 onto the forge tree, and port every gh call site it brought (#198) `git merge` of upstream `8c3a4d1` onto `dad99dd`, common ancestor `84bb1a4`. 18 hunks in 10 files; `lib/forge.sh`, `lib/forge-github.sh` and `lib/forge-forgejo.sh` conflict in none and come out byte-identical. The resolutions the issue decided: VERSION and both CEREMONY_SELF_REF carriers take upstream's numbers; `.github/labels.conf` and `drills/0.4.1.md` keep this forge's; CHANGELOG keeps both sides and names the upstream commit this tree carries. The part the hunks did not contain. Upstream's 0.5.0/0.6.0 work added whole functions to files this tree already owned, so `git merge` took its side without raising a conflict — and with them, EIGHT runtime `gh` call sites that #188 had removed. Seven are ported onto the shim: two reads and four comment writes in issueflow-reconcile, and labels-reconcile's HEAD_COMMIT_AT read. The eighth is `gh workflow run` in labels.yml, which a workflow cannot declare a client for and whose Forgejo equivalent this instance answers with 500 rather than a 4xx — named with its reason rather than ported on a guess. test/no-runtime-gh.test.sh makes the rule mechanical, because reviewing the diff could not: four reviewers reading it each found a different subset, and the contract suite stubs `gh`, so a reintroduced call site passes it. Three seams the resolution decides are silent when resolved wrongly, and each now has a case that fails on the wrong one: the merged record's `merged_at` third column (without it every sort key ties and the highest PR number comes back), the open gather's one-BODY-row-per-line feed (a whole decoded body as one record loses every declaration including the first), and the whole-board read whose COLLISION_FLAGS/WINDOW_FLAGS consumers auto-merged. The open gather carries CLOSING rows as well as BODY rows. `Refs` alone would drop every `Closes #N` link on the open side and reclaim a claim the PR was holding — the existing base64 round-trip case is red without it. actions/refs-not-closing declares CEREMONY_FORGE_CLIENT=gh: its only gather is GraphQL, which Forgejo does not serve at all. #199 ports it. test/run.sh: 28 test files, 0 failed. shellcheck and actionlint clean. Refs #198
2026-08-05 11:56:23 +00:00
# The declared-client refusal (#198 spec 4). This action is the one call site
# the 0.6.0 merge could NOT port — Forgejo serves no GraphQL at all — so on a
# Forgejo forge it must refuse by name, never produce a verdict from a graph
# it did not read. #199 removes the declaration by making the gather REST.
forgejo_boundary() {
env PATH="$TMP/bin:$PATH" FAKE_GH_MODE=success GH_CALL_LOG="$TMP/gh-calls" \
merge upstream 0.6.0 onto the forge tree, and port every gh call site it brought (#198) `git merge` of upstream `8c3a4d1` onto `dad99dd`, common ancestor `84bb1a4`. 18 hunks in 10 files; `lib/forge.sh`, `lib/forge-github.sh` and `lib/forge-forgejo.sh` conflict in none and come out byte-identical. The resolutions the issue decided: VERSION and both CEREMONY_SELF_REF carriers take upstream's numbers; `.github/labels.conf` and `drills/0.4.1.md` keep this forge's; CHANGELOG keeps both sides and names the upstream commit this tree carries. The part the hunks did not contain. Upstream's 0.5.0/0.6.0 work added whole functions to files this tree already owned, so `git merge` took its side without raising a conflict — and with them, EIGHT runtime `gh` call sites that #188 had removed. Seven are ported onto the shim: two reads and four comment writes in issueflow-reconcile, and labels-reconcile's HEAD_COMMIT_AT read. The eighth is `gh workflow run` in labels.yml, which a workflow cannot declare a client for and whose Forgejo equivalent this instance answers with 500 rather than a 4xx — named with its reason rather than ported on a guess. test/no-runtime-gh.test.sh makes the rule mechanical, because reviewing the diff could not: four reviewers reading it each found a different subset, and the contract suite stubs `gh`, so a reintroduced call site passes it. Three seams the resolution decides are silent when resolved wrongly, and each now has a case that fails on the wrong one: the merged record's `merged_at` third column (without it every sort key ties and the highest PR number comes back), the open gather's one-BODY-row-per-line feed (a whole decoded body as one record loses every declaration including the first), and the whole-board read whose COLLISION_FLAGS/WINDOW_FLAGS consumers auto-merged. The open gather carries CLOSING rows as well as BODY rows. `Refs` alone would drop every `Closes #N` link on the open side and reclaim a claim the PR was holding — the existing base64 round-trip case is red without it. actions/refs-not-closing declares CEREMONY_FORGE_CLIENT=gh: its only gather is GraphQL, which Forgejo does not serve at all. #199 ports it. test/run.sh: 28 test files, 0 failed. shellcheck and actionlint clean. Refs #198
2026-08-05 11:56:23 +00:00
CEREMONY_FORGE=forgejo \
GITHUB_REPOSITORY="heavy-duty/ceremony" PR_NUMBER=268 \
GITHUB_ACTION_PATH="$ROOT/actions/refs-not-closing" \
bash "$ENTRYPOINT"
}
fix(198): the action fails closed, the caller decides scheduling, the guard decides the forge (#198) @codex-reviewer-andresmgsl's second review, both points taken. The refs action goes back to `forge_preflight || exit 1`. 97e63ac had it exit 0 with a notice so the PR check would not be red, and that conflated two different questions: "this action cannot produce a verdict" is the ACTION's contract and must stay a refusal, while "this check should not block the board" is the CALLER's decision. The caller now carries it — refs-guard.yml skips unless github.server_url is github.com, mirroring forge_detect positively. A skipped check is a green head; an action that reports success it did not earn is not. The leaked preflight_err temp file goes with the revert. The workflow guard asked the wrong question. `command -v gh` alone passes the moment a Forgejo runner image happens to ship gh, and then dispatches against a forge that cannot serve it — the client/forge mismatch forge_preflight exists to prevent. It decides the FORGE first now, mirroring forge_detect positively, and the binary second. The source guard splits to match: a declaration guarded only by binary presence is reported, with a fixture that fails on exactly that shape. The warning text was also wrong on the facts, as noted: issue-event sweeps ARE this caller's event-driven wakes, so they are precisely what is lost. It now says the hourly scheduled sweep survives and every event-driven wake through this caller does not, until #205. Point 1 of that review — jq 1.6 accepting an empty payload — was already fixed in 728102a, pushed before the review landed. Verified under the runner's jq 1.6 as well as 1.7: 28 test files, 0 failed both ways. shellcheck 0.10.0 (CI's pin), actionlint, self-ref, marker, vendored, changelog-armed all clean with every file tracked. Refs #198
2026-08-05 12:30:28 +00:00
# The contract on a forge this action cannot speak: refuse, by name, non-zero,
# and read nothing. FAIL CLOSED — an earlier head made this exit 0 so the PR
# check would not be red, which conflated the ACTION's contract with the
# CALLER's scheduling decision (@codex-reviewer-andresmgsl, #198). The caller
# is .github/workflows/refs-guard.yml, which skips on a backend this action
# cannot speak; the action itself never reports success it did not earn.
check "on a forgejo forge the action refuses, non-zero" 1 \
merge upstream 0.6.0 onto the forge tree, and port every gh call site it brought (#198) `git merge` of upstream `8c3a4d1` onto `dad99dd`, common ancestor `84bb1a4`. 18 hunks in 10 files; `lib/forge.sh`, `lib/forge-github.sh` and `lib/forge-forgejo.sh` conflict in none and come out byte-identical. The resolutions the issue decided: VERSION and both CEREMONY_SELF_REF carriers take upstream's numbers; `.github/labels.conf` and `drills/0.4.1.md` keep this forge's; CHANGELOG keeps both sides and names the upstream commit this tree carries. The part the hunks did not contain. Upstream's 0.5.0/0.6.0 work added whole functions to files this tree already owned, so `git merge` took its side without raising a conflict — and with them, EIGHT runtime `gh` call sites that #188 had removed. Seven are ported onto the shim: two reads and four comment writes in issueflow-reconcile, and labels-reconcile's HEAD_COMMIT_AT read. The eighth is `gh workflow run` in labels.yml, which a workflow cannot declare a client for and whose Forgejo equivalent this instance answers with 500 rather than a 4xx — named with its reason rather than ported on a guess. test/no-runtime-gh.test.sh makes the rule mechanical, because reviewing the diff could not: four reviewers reading it each found a different subset, and the contract suite stubs `gh`, so a reintroduced call site passes it. Three seams the resolution decides are silent when resolved wrongly, and each now has a case that fails on the wrong one: the merged record's `merged_at` third column (without it every sort key ties and the highest PR number comes back), the open gather's one-BODY-row-per-line feed (a whole decoded body as one record loses every declaration including the first), and the whole-board read whose COLLISION_FLAGS/WINDOW_FLAGS consumers auto-merged. The open gather carries CLOSING rows as well as BODY rows. `Refs` alone would drop every `Closes #N` link on the open side and reclaim a claim the PR was holding — the existing base64 round-trip case is red without it. actions/refs-not-closing declares CEREMONY_FORGE_CLIENT=gh: its only gather is GraphQL, which Forgejo does not serve at all. #199 ports it. test/run.sh: 28 test files, 0 failed. shellcheck and actionlint clean. Refs #198
2026-08-05 11:56:23 +00:00
"cannot speak it" forgejo_boundary
fix(198): the action fails closed, the caller decides scheduling, the guard decides the forge (#198) @codex-reviewer-andresmgsl's second review, both points taken. The refs action goes back to `forge_preflight || exit 1`. 97e63ac had it exit 0 with a notice so the PR check would not be red, and that conflated two different questions: "this action cannot produce a verdict" is the ACTION's contract and must stay a refusal, while "this check should not block the board" is the CALLER's decision. The caller now carries it — refs-guard.yml skips unless github.server_url is github.com, mirroring forge_detect positively. A skipped check is a green head; an action that reports success it did not earn is not. The leaked preflight_err temp file goes with the revert. The workflow guard asked the wrong question. `command -v gh` alone passes the moment a Forgejo runner image happens to ship gh, and then dispatches against a forge that cannot serve it — the client/forge mismatch forge_preflight exists to prevent. It decides the FORGE first now, mirroring forge_detect positively, and the binary second. The source guard splits to match: a declaration guarded only by binary presence is reported, with a fixture that fails on exactly that shape. The warning text was also wrong on the facts, as noted: issue-event sweeps ARE this caller's event-driven wakes, so they are precisely what is lost. It now says the hourly scheduled sweep survives and every event-driven wake through this caller does not, until #205. Point 1 of that review — jq 1.6 accepting an empty payload — was already fixed in 728102a, pushed before the review landed. Verified under the runner's jq 1.6 as well as 1.7: 28 test files, 0 failed both ways. shellcheck 0.10.0 (CI's pin), actionlint, self-ref, marker, vendored, changelog-armed all clean with every file tracked. Refs #198
2026-08-05 12:30:28 +00:00
check "...naming the client it declared" 1 "'gh' client" forgejo_boundary
check "...and the client the forge actually needs" 1 "'rest' client" forgejo_boundary
# The teeth: it must not have READ anything. The stub counts its own calls, so
# a gather that ran despite the refusal is visible here.
forgejo_read_count() {
: >"$TMP/gh-calls"
forgejo_boundary >/dev/null 2>&1
wc -l <"$TMP/gh-calls"
}
check "...and reached the forge zero times" 0 "0" forgejo_read_count
fix(198): the action fails closed, the caller decides scheduling, the guard decides the forge (#198) @codex-reviewer-andresmgsl's second review, both points taken. The refs action goes back to `forge_preflight || exit 1`. 97e63ac had it exit 0 with a notice so the PR check would not be red, and that conflated two different questions: "this action cannot produce a verdict" is the ACTION's contract and must stay a refusal, while "this check should not block the board" is the CALLER's decision. The caller now carries it — refs-guard.yml skips unless github.server_url is github.com, mirroring forge_detect positively. A skipped check is a green head; an action that reports success it did not earn is not. The leaked preflight_err temp file goes with the revert. The workflow guard asked the wrong question. `command -v gh` alone passes the moment a Forgejo runner image happens to ship gh, and then dispatches against a forge that cannot serve it — the client/forge mismatch forge_preflight exists to prevent. It decides the FORGE first now, mirroring forge_detect positively, and the binary second. The source guard splits to match: a declaration guarded only by binary presence is reported, with a fixture that fails on exactly that shape. The warning text was also wrong on the facts, as noted: issue-event sweeps ARE this caller's event-driven wakes, so they are precisely what is lost. It now says the hourly scheduled sweep survives and every event-driven wake through this caller does not, until #205. Point 1 of that review — jq 1.6 accepting an empty payload — was already fixed in 728102a, pushed before the review landed. Verified under the runner's jq 1.6 as well as 1.7: 28 test files, 0 failed both ways. shellcheck 0.10.0 (CI's pin), actionlint, self-ref, marker, vendored, changelog-armed all clean with every file tracked. Refs #198
2026-08-05 12:30:28 +00:00
# The caller carries the scheduling half, positively: only github.com runs it.
check "the caller skips the job on any non-github forge" 0 \
"github.server_url == 'https://github.com'" \
grep -F "if:" "$ROOT/.github/workflows/refs-guard.yml"
merge upstream 0.6.0 onto the forge tree, and port every gh call site it brought (#198) `git merge` of upstream `8c3a4d1` onto `dad99dd`, common ancestor `84bb1a4`. 18 hunks in 10 files; `lib/forge.sh`, `lib/forge-github.sh` and `lib/forge-forgejo.sh` conflict in none and come out byte-identical. The resolutions the issue decided: VERSION and both CEREMONY_SELF_REF carriers take upstream's numbers; `.github/labels.conf` and `drills/0.4.1.md` keep this forge's; CHANGELOG keeps both sides and names the upstream commit this tree carries. The part the hunks did not contain. Upstream's 0.5.0/0.6.0 work added whole functions to files this tree already owned, so `git merge` took its side without raising a conflict — and with them, EIGHT runtime `gh` call sites that #188 had removed. Seven are ported onto the shim: two reads and four comment writes in issueflow-reconcile, and labels-reconcile's HEAD_COMMIT_AT read. The eighth is `gh workflow run` in labels.yml, which a workflow cannot declare a client for and whose Forgejo equivalent this instance answers with 500 rather than a 4xx — named with its reason rather than ported on a guess. test/no-runtime-gh.test.sh makes the rule mechanical, because reviewing the diff could not: four reviewers reading it each found a different subset, and the contract suite stubs `gh`, so a reintroduced call site passes it. Three seams the resolution decides are silent when resolved wrongly, and each now has a case that fails on the wrong one: the merged record's `merged_at` third column (without it every sort key ties and the highest PR number comes back), the open gather's one-BODY-row-per-line feed (a whole decoded body as one record loses every declaration including the first), and the whole-board read whose COLLISION_FLAGS/WINDOW_FLAGS consumers auto-merged. The open gather carries CLOSING rows as well as BODY rows. `Refs` alone would drop every `Closes #N` link on the open side and reclaim a claim the PR was holding — the existing base64 round-trip case is red without it. actions/refs-not-closing declares CEREMONY_FORGE_CLIENT=gh: its only gather is GraphQL, which Forgejo does not serve at all. #199 ports it. test/run.sh: 28 test files, 0 failed. shellcheck and actionlint clean. Refs #198
2026-08-05 11:56:23 +00:00
check "action boundary fails when GraphQL read fails" 42 \
"fake GraphQL read failed" action_boundary failure
check "action boundary refuses a partial closing-reference page" 5 \
"refusing a partial verdict" action_boundary partial
check "action boundary accepts a complete GraphQL read" 0 \
"no Refs target" action_boundary success
2026-08-03 20:34:35 +00:00
one_graphql_read() {
[ "$(grep -c "gh api graphql" "$ENTRYPOINT")" -eq 1 ]
2026-08-03 20:34:35 +00:00
printf '1\n'
}
2026-08-03 20:29:51 +00:00
check "action performs exactly one GraphQL read" 0 "1" \
2026-08-03 20:34:35 +00:00
one_graphql_read
check "composite delegates to the tested entrypoint" 0 "run.sh" \
2026-08-03 21:00:40 +00:00
grep -F "run: bash \"\$GITHUB_ACTION_PATH/run.sh\"" "$ACTION"
2026-08-03 20:29:51 +00:00
check "workflow wakes on body edits" 0 "types: [opened, edited, reopened, synchronize]" \
grep -F "types: [opened, edited, reopened, synchronize]" "$WORKFLOW"
check "workflow is pull_request-only" 1 "" \
grep -E '^ (push|pull_request_target|workflow_dispatch|schedule|issue_comment):' \
"$WORKFLOW"
2026-08-03 20:29:51 +00:00
check "workflow grants read-only pull request access" 0 "pull-requests: read" \
grep -F "pull-requests: read" "$WORKFLOW"
summary