2026-08-03 20:58:56 +00:00
#!/usr/bin/env bash
set -euo pipefail
# The composite action's executable boundary (#218). Keeping the GraphQL
# gather here lets the offline contract test replace `gh` and prove that
# failed and partial reads cannot accidentally produce a green verdict.
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
# THIS ACTION IS STILL gh-ONLY, AND SAYS SO (#198 spec 4, #199 ports it).
# Its entire gather is a single GraphQL query issued through `gh`, and
# 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 is no endpoint to translate this to, so
# unlike every other call site the merge touched it cannot be ported here;
# it has to be re-expressed over REST, which is #199.
#
# Until then the declaration is the honest move: CEREMONY_FORGE_CLIENT names
# the client this file actually speaks, and forge_preflight refuses loudly on
# a forge that cannot serve it — rather than reading nothing and reporting a
# verdict. That is lib/forge.sh's own rule, "Never 'probably github'",
# applied to the one action that has not caught up yet.
# shellcheck source=lib/forge.sh
. " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) /../../lib/forge.sh "
export CEREMONY_FORGE_CLIENT = gh
fix(refs-not-closing): report and skip on a forge it cannot speak, rather than reddening every PR (#198)
The first head's `Refs guard` failed on this PR, correctly: spec 4's
CEREMONY_FORGE_CLIENT=gh declaration made forge_preflight refuse by name on
this forge. But that workflow runs on every pull request here, so the
declaration as first written turns every future PR red until #199 lands —
blocking the board for a gap that already has its own issue.
Refusing and scheduling are different questions. This action must never
produce a verdict from a graph it did not read, and it does not: on a forge it
cannot speak it now says so by name, cites #199, states that no verdict was
produced, and reaches the forge zero times. A preflight failure for any other
reason stays fatal, and on a forge it CAN speak nothing changes.
Also: five SC2016 findings in test/no-runtime-gh.test.sh. They were invisible
locally because shellcheck-all.sh lints TRACKED files and the guard was still
untracked when I ran it — a new file is exactly the case that check cannot
see. Verified this time against CI's pinned shellcheck 0.10.0 with the file
committed.
test/run.sh: 28 test files, 0 failed, under CI's CEREMONY_REQUIRE_* env.
shellcheck, actionlint, self-ref, marker and vendored guards all clean.
Refs #198
2026-08-05 12:09:05 +00:00
# The refusal and the SCHEDULING are two different questions, and #198's first
# head conflated them: preflight refused correctly on this forge and turned
# every PR's `Refs guard` red, which blocks the board rather than protecting
# it. A guard that cannot run here must not claim a verdict — but it also must
# not stand permanently red for a port that has its own issue.
#
# So: not-runnable-here is reported and skipped, loudly and by name, and only
# a preflight failure for any OTHER reason is fatal. The distinction is the
# forge, not the exit code — on a forge this action CAN speak, a preflight
# failure is still a hard refusal, which is the case the tests drive.
preflight_err = " $( mktemp) "
trap 'rm -f "$preflight_err"' EXIT
if ! forge_preflight 2>" $preflight_err " ; then
cat " $preflight_err " >& 2
if [ " $( forge_detect 2>/dev/null) " != github ] ; then
printf '::notice::refs-not-closing: not run — this action is still gh-only (its gather is GraphQL, which this forge does not serve) and #199 ports it. No verdict was produced.\n'
exit 0
fi
exit 1
fi
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
2026-08-03 20:58:56 +00:00
owner = " ${ GITHUB_REPOSITORY %%/* } "
name = " ${ GITHUB_REPOSITORY #*/ } "
[ -n " ${ PR_NUMBER :- } " ] || {
echo "refs-not-closing: pull request number is unavailable" >& 2
exit 1
}
2026-08-03 21:00:40 +00:00
# GraphQL variables are literal API syntax; the shell must not expand them.
# shellcheck disable=SC2016
2026-08-03 20:58:56 +00:00
facts = " $( gh api graphql \
-f query = ' query( $owner : String!, $name : String!, $number : Int!) {
repository( owner: $owner , name: $name ) {
pullRequest( number: $number ) {
body
closingIssuesReferences( first: 100) {
nodes { number }
pageInfo { hasNextPage }
}
}
}
} ' \
-F owner = " $owner " -F name = " $name " -F number = " $PR_NUMBER " ) "
body_file = " $( mktemp) "
closing_file = " $( mktemp) "
trap 'rm -f "$body_file" "$closing_file"' EXIT
jq -er '
.data.repository.pullRequest
| if . = = null then error( "pull request was not returned" ) else .body // "" end
' <<< " $facts " >" $body_file "
jq -r '
.data.repository.pullRequest.closingIssuesReferences
| if . = = null then
error( "closing issue references were not returned" )
elif .pageInfo.hasNextPage then
error( "more than 100 closing issue references; refusing a partial verdict" )
else
.nodes[ ] .number
end
' <<< " $facts " >" $closing_file "
mapfile -t closing_issues <" $closing_file "
bash " $GITHUB_ACTION_PATH /refs-not-closing.sh " \
" $body_file " " ${ closing_issues [@] } "