2026-08-03 20:58:56 +00:00
|
|
|
#!/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
|
|
|
|
|
}
|
2026-08-03 20:58:56 +00:00
|
|
|
[ -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"
|
2026-08-03 20:58:56 +00:00
|
|
|
|
|
|
|
|
mapfile -t closing_issues <"$closing_file"
|
|
|
|
|
bash "$GITHUB_ACTION_PATH/refs-not-closing.sh" \
|
|
|
|
|
"$body_file" "${closing_issues[@]}"
|