ceremony/actions/refs-not-closing/run.sh

89 lines
4.2 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
set -euo pipefail
fix(refs-not-closing): gather over REST, so the guard produces verdicts here The action's entire gather was one GraphQL query asking GitHub for its own parse of the closing keywords. Forgejo serves no GraphQL at all — /api/graphql 404s here and a forgejo-runner job arrives with GITHUB_GRAPHQL_URL empty — so there was nothing to translate it to. It is re-expressed, as #188 re-expressed its own two GraphQL sites, over two reads both backends serve plus this repo's own parser. The graph was called authoritative for including "closing keywords and sidebar links". Those halves resolve differently here: Forgejo has no sidebar-link concept, so nothing is lost there, but it DOES honour closing keywords in commit messages. A body-only port would miss a PR that closes an issue from a commit subject — exactly the contradiction this action exists to catch — so the closing set unions the body and every commit message. The hasNextPage refusal is relocated, not dropped: --paginate carries the forgejo backend's x-total-count completeness proof, and a short gather refuses rather than returning a partial verdict. lib/issue_references.sh extracts the LOCAL/CROSS classifier from issueflow-reconcile's executable. closes_references.sh's header recorded that dependency in prose; a composite action cannot source a reconciler to borrow one function, because sourcing a reconciler runs one. refs-guard.yml's github-only gate is removed in the same change. A portable action behind that gate is a guard that passes by never running. The contract test drives the boundary on BOTH backends with stubs at the transport. Mutations: body-only parse reds 4 cases, dropping --paginate reds the partial-gather case, ignoring a failed read reds 9. Refs #199
2026-08-05 17:11:19 +00:00
# The composite action's executable boundary (#218). Keeping the gather here
# lets the offline contract test replace the forge and prove that failed and
# partial reads cannot accidentally produce a green verdict.
#
# THE GATHER IS REST, THROUGH THE SHIM (#199). It was a single GraphQL query
# issued through `gh`, asking GitHub for `closingIssuesReferences` — its own
# parse of the closing keywords. Forgejo serves no GraphQL surface at all:
# `/api/graphql` 404s on this instance, and a real forgejo-runner job arrives
# with GITHUB_GRAPHQL_URL set to the empty string (lib/forge.sh's header).
# There was nothing to translate it to, so it is re-expressed — exactly as
# #188 re-expressed its own two GraphQL sites — over two reads both backends
# already serve, plus a parser this repo owns.
#
# WHAT THE GRAPH GAVE THAT TWO READS MUST REPLACE. This file used to call the
# graph "authoritative because it includes both closing keywords and sidebar
# links". Those two halves resolve differently here:
#
# sidebar links Forgejo has no such concept — an issue is closed by a
# keyword, never by a manual link. Nothing is lost.
# commit messages Forgejo DOES honour closing keywords in commit messages.
# A body-only parse would miss a PR that closes an issue
# from a commit subject and let through exactly the
# contradiction this action exists to catch.
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
#
fix(refs-not-closing): gather over REST, so the guard produces verdicts here The action's entire gather was one GraphQL query asking GitHub for its own parse of the closing keywords. Forgejo serves no GraphQL at all — /api/graphql 404s here and a forgejo-runner job arrives with GITHUB_GRAPHQL_URL empty — so there was nothing to translate it to. It is re-expressed, as #188 re-expressed its own two GraphQL sites, over two reads both backends serve plus this repo's own parser. The graph was called authoritative for including "closing keywords and sidebar links". Those halves resolve differently here: Forgejo has no sidebar-link concept, so nothing is lost there, but it DOES honour closing keywords in commit messages. A body-only port would miss a PR that closes an issue from a commit subject — exactly the contradiction this action exists to catch — so the closing set unions the body and every commit message. The hasNextPage refusal is relocated, not dropped: --paginate carries the forgejo backend's x-total-count completeness proof, and a short gather refuses rather than returning a partial verdict. lib/issue_references.sh extracts the LOCAL/CROSS classifier from issueflow-reconcile's executable. closes_references.sh's header recorded that dependency in prose; a composite action cannot source a reconciler to borrow one function, because sourcing a reconciler runs one. refs-guard.yml's github-only gate is removed in the same change. A portable action behind that gate is a guard that passes by never running. The contract test drives the boundary on BOTH backends with stubs at the transport. Mutations: body-only parse reds 4 cases, dropping --paginate reds the partial-gather case, ignoring a failed read reds 9. Refs #199
2026-08-05 17:11:19 +00:00
# Hence both reads, unioned. The commit half is not optional.
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
# shellcheck source=lib/forge.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/forge.sh"
fix(refs-not-closing): gather over REST, so the guard produces verdicts here The action's entire gather was one GraphQL query asking GitHub for its own parse of the closing keywords. Forgejo serves no GraphQL at all — /api/graphql 404s here and a forgejo-runner job arrives with GITHUB_GRAPHQL_URL empty — so there was nothing to translate it to. It is re-expressed, as #188 re-expressed its own two GraphQL sites, over two reads both backends serve plus this repo's own parser. The graph was called authoritative for including "closing keywords and sidebar links". Those halves resolve differently here: Forgejo has no sidebar-link concept, so nothing is lost there, but it DOES honour closing keywords in commit messages. A body-only port would miss a PR that closes an issue from a commit subject — exactly the contradiction this action exists to catch — so the closing set unions the body and every commit message. The hasNextPage refusal is relocated, not dropped: --paginate carries the forgejo backend's x-total-count completeness proof, and a short gather refuses rather than returning a partial verdict. lib/issue_references.sh extracts the LOCAL/CROSS classifier from issueflow-reconcile's executable. closes_references.sh's header recorded that dependency in prose; a composite action cannot source a reconciler to borrow one function, because sourcing a reconciler runs one. refs-guard.yml's github-only gate is removed in the same change. A portable action behind that gate is a guard that passes by never running. The contract test drives the boundary on BOTH backends with stubs at the transport. Mutations: body-only parse reds 4 cases, dropping --paginate reds the partial-gather case, ignoring a failed read reds 9. Refs #199
2026-08-05 17:11:19 +00:00
# shellcheck source=lib/issue_references.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/issue_references.sh"
# shellcheck source=lib/closes_references.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/closes_references.sh"
# No CEREMONY_FORGE_CLIENT declaration any more (#199 removes #198's): this
# file speaks the shim, not a client. Fail CLOSED at the action boundary all
# the same — "this action cannot produce a verdict" is the ACTION's contract
# and stays a refusal, while "this check should not block the board" is the
# CALLER's decision (@codex-reviewer-andresmgsl, #198).
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
forge_preflight || exit 1
fix(refs-not-closing): gather over REST, so the guard produces verdicts here The action's entire gather was one GraphQL query asking GitHub for its own parse of the closing keywords. Forgejo serves no GraphQL at all — /api/graphql 404s here and a forgejo-runner job arrives with GITHUB_GRAPHQL_URL empty — so there was nothing to translate it to. It is re-expressed, as #188 re-expressed its own two GraphQL sites, over two reads both backends serve plus this repo's own parser. The graph was called authoritative for including "closing keywords and sidebar links". Those halves resolve differently here: Forgejo has no sidebar-link concept, so nothing is lost there, but it DOES honour closing keywords in commit messages. A body-only port would miss a PR that closes an issue from a commit subject — exactly the contradiction this action exists to catch — so the closing set unions the body and every commit message. The hasNextPage refusal is relocated, not dropped: --paginate carries the forgejo backend's x-total-count completeness proof, and a short gather refuses rather than returning a partial verdict. lib/issue_references.sh extracts the LOCAL/CROSS classifier from issueflow-reconcile's executable. closes_references.sh's header recorded that dependency in prose; a composite action cannot source a reconciler to borrow one function, because sourcing a reconciler runs one. refs-guard.yml's github-only gate is removed in the same change. A portable action behind that gate is a guard that passes by never running. The contract test drives the boundary on BOTH backends with stubs at the transport. Mutations: body-only parse reds 4 cases, dropping --paginate reds the partial-gather case, ignoring a failed read reds 9. Refs #199
2026-08-05 17:11:19 +00:00
forge_select "" || exit 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
fix(refs-not-closing): gather over REST, so the guard produces verdicts here The action's entire gather was one GraphQL query asking GitHub for its own parse of the closing keywords. Forgejo serves no GraphQL at all — /api/graphql 404s here and a forgejo-runner job arrives with GITHUB_GRAPHQL_URL empty — so there was nothing to translate it to. It is re-expressed, as #188 re-expressed its own two GraphQL sites, over two reads both backends serve plus this repo's own parser. The graph was called authoritative for including "closing keywords and sidebar links". Those halves resolve differently here: Forgejo has no sidebar-link concept, so nothing is lost there, but it DOES honour closing keywords in commit messages. A body-only port would miss a PR that closes an issue from a commit subject — exactly the contradiction this action exists to catch — so the closing set unions the body and every commit message. The hasNextPage refusal is relocated, not dropped: --paginate carries the forgejo backend's x-total-count completeness proof, and a short gather refuses rather than returning a partial verdict. lib/issue_references.sh extracts the LOCAL/CROSS classifier from issueflow-reconcile's executable. closes_references.sh's header recorded that dependency in prose; a composite action cannot source a reconciler to borrow one function, because sourcing a reconciler runs one. refs-guard.yml's github-only gate is removed in the same change. A portable action behind that gate is a guard that passes by never running. The contract test drives the boundary on BOTH backends with stubs at the transport. Mutations: body-only parse reds 4 cases, dropping --paginate reds the partial-gather case, ignoring a failed read reds 9. Refs #199
2026-08-05 17:11:19 +00:00
REPO="${REPO:-${GITHUB_REPOSITORY:-}}"
[ -n "$REPO" ] || {
echo "refs-not-closing: set REPO or GITHUB_REPOSITORY to owner/name" >&2
exit 1
}
[ -n "${PR_NUMBER:-}" ] || {
echo "refs-not-closing: pull request number is unavailable" >&2
exit 1
}
body_file="$(mktemp)"
closing_file="$(mktemp)"
trap 'rm -f "$body_file" "$closing_file"' EXIT
fix(refs-not-closing): gather over REST, so the guard produces verdicts here The action's entire gather was one GraphQL query asking GitHub for its own parse of the closing keywords. Forgejo serves no GraphQL at all — /api/graphql 404s here and a forgejo-runner job arrives with GITHUB_GRAPHQL_URL empty — so there was nothing to translate it to. It is re-expressed, as #188 re-expressed its own two GraphQL sites, over two reads both backends serve plus this repo's own parser. The graph was called authoritative for including "closing keywords and sidebar links". Those halves resolve differently here: Forgejo has no sidebar-link concept, so nothing is lost there, but it DOES honour closing keywords in commit messages. A body-only port would miss a PR that closes an issue from a commit subject — exactly the contradiction this action exists to catch — so the closing set unions the body and every commit message. The hasNextPage refusal is relocated, not dropped: --paginate carries the forgejo backend's x-total-count completeness proof, and a short gather refuses rather than returning a partial verdict. lib/issue_references.sh extracts the LOCAL/CROSS classifier from issueflow-reconcile's executable. closes_references.sh's header recorded that dependency in prose; a composite action cannot source a reconciler to borrow one function, because sourcing a reconciler runs one. refs-guard.yml's github-only gate is removed in the same change. A portable action behind that gate is a guard that passes by never running. The contract test drives the boundary on BOTH backends with stubs at the transport. Mutations: body-only parse reds 4 cases, dropping --paginate reds the partial-gather case, ignoring a failed read reds 9. Refs #199
2026-08-05 17:11:19 +00:00
# A read that fails must never reach the parser: an empty body parses to an
# empty closing set, which is a PASSING verdict this action never earned.
# `set -e` covers the assignment, and the explicit checks below name which
# read failed rather than leaving the operator to guess.
if ! forge_api "repos/$REPO/pulls/$PR_NUMBER" --jq '.body // ""' >"$body_file"; then
echo "refs-not-closing: could not read PR $PR_NUMBER's body — refusing a verdict" >&2
exit 1
fi
# --paginate carries the completeness proof: the forgejo backend walks pages
# and then compares what it collected against the server's declared
# x-total-count, refusing a short gather (#188, #4699). That IS this action's
# `hasNextPage` refusal, relocated rather than reinvented — upstream refused
# past 100 closing references rather than issue a partial verdict, and an
# incomplete commit read is the same failure wearing REST's clothes.
commits_file="$(mktemp)"
trap 'rm -f "$body_file" "$closing_file" "$commits_file"' EXIT
if ! forge_api --paginate "repos/$REPO/pulls/$PR_NUMBER/commits" \
--jq '.[].commit.message' >"$commits_file"; then
echo "refs-not-closing: could not read PR $PR_NUMBER's commits completely — refusing a partial verdict" >&2
exit 1
fi
# The union. closes_references is line-oriented, so concatenating the body and
# every commit message and parsing once IS the union of parsing each — and it
# keeps one parse to reason about instead of two that could drift.
cat "$body_file" "$commits_file" | closes_references >"$closing_file"
mapfile -t closing_issues <"$closing_file"
bash "$GITHUB_ACTION_PATH/refs-not-closing.sh" \
"$body_file" "${closing_issues[@]}"