forked from heavy-duty/ceremony
Merge pull request 'actions/refs-not-closing — gather over REST, so the guard produces verdicts on this forge (#199)' (#214) from build/199-refs-not-closing-rest into main
Reviewed-on: heavy-duty/ceremony#214 Reviewed-by: codex-reviewer-andresmgsl <andres+2@heavyduty.builders> Reviewed-by: kimi-reviewer-andresmgsl <andres+4@heavyduty.builders>
This commit is contained in:
commit
9daeeb756e
6 changed files with 280 additions and 154 deletions
26
.github/workflows/refs-guard.yml
vendored
26
.github/workflows/refs-guard.yml
vendored
|
|
@ -12,18 +12,22 @@ permissions:
|
|||
|
||||
jobs:
|
||||
refs-not-closing:
|
||||
# The action is gh-only until #199: its whole gather is a GraphQL query,
|
||||
# and Forgejo serves no GraphQL at all. The ACTION refuses by name on a
|
||||
# backend it cannot speak (that is its contract, and its contract test);
|
||||
# scheduling it where it can only refuse is this workflow's decision, and
|
||||
# a permanently red required check would block every merge on this forge
|
||||
# for a gap #199 already owns. So the job does not run there — a skipped
|
||||
# check is a green head, an invented verdict is not.
|
||||
# The action is portable (#199): its gather is two REST reads through the
|
||||
# shim plus this repo's own closing-keyword parser, so it produces a real
|
||||
# verdict on either forge. It still refuses by name when it cannot read —
|
||||
# that is its contract, and its contract test.
|
||||
#
|
||||
# The condition mirrors lib/forge.sh's forge_detect positively: only
|
||||
# github.com is accepted, and anything else — Forgejo, or a host this
|
||||
# file has not met — is not run. "Never 'probably github'."
|
||||
if: ${{ github.server_url == 'https://github.com' }}
|
||||
# #199 removed the forge gate that used to sit here. While the action's
|
||||
# only gather was GraphQL it could do nothing but refuse on Forgejo, and
|
||||
# scheduling a permanently red required check would have blocked every
|
||||
# merge on this forge; a skipped check is a green head, an invented
|
||||
# verdict is not. The gather is REST now, so the job RUNS here and
|
||||
# produces verdicts again.
|
||||
#
|
||||
# Deleting the action's client declaration without deleting this gate
|
||||
# would have left it portable and never scheduled — a guard that passes
|
||||
# by never running, which is this repo's blind-sweep shape wearing a
|
||||
# different hat (@kimi-reviewer-andresmgsl, #198).
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ TRIAGE_ACTORS=()
|
|||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/ruling.sh"
|
||||
# shellcheck source=lib/forge.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/forge.sh"
|
||||
# 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"
|
||||
# The attention target invariants (#232) — diagnosis only, both surfaces.
|
||||
|
|
@ -267,16 +269,6 @@ post_merge_transition_marker() { # $1 merged PR number
|
|||
printf 'post-merge-transition-pr-%s\n' "$1"
|
||||
}
|
||||
|
||||
issue_references() { # text on stdin -> LOCAL/CROSS<TAB>reference
|
||||
# A qualified reference belongs to another repository. Classify the whole
|
||||
# token before extracting numbers so rig#112 can never become local #112.
|
||||
{ grep -Eo '([[:alnum:]_.-]+/)?[[:alnum:]_.-]+#[0-9]+|#[0-9]+' || true; } \
|
||||
| awk '
|
||||
index($0, "#") == 1 { print "LOCAL\t" substr($0, 2); next }
|
||||
{ print "CROSS\t" $0 }
|
||||
'
|
||||
}
|
||||
|
||||
blocked_reference_records() { # body on stdin -> classified reference records
|
||||
# Every occurrence of the marker contributes a clause. Binding to the first
|
||||
# occurrence alone dropped the later sentences of a repeated declaration
|
||||
|
|
|
|||
|
|
@ -1,76 +1,87 @@
|
|||
#!/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.
|
||||
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# Hence both reads, unioned. The commit half is not optional.
|
||||
# shellcheck source=lib/forge.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/forge.sh"
|
||||
export CEREMONY_FORGE_CLIENT=gh
|
||||
# Fail CLOSED, at the action boundary. An earlier head here exited 0 with a
|
||||
# notice so the PR check would not be red; @codex-reviewer-andresmgsl was
|
||||
# right that this conflates two different questions. "This action cannot
|
||||
# produce a verdict" is the ACTION's contract and must stay a refusal; "this
|
||||
# check should not block the board" is the CALLER's decision, and it belongs
|
||||
# in .github/workflows/refs-guard.yml, which skips on a backend this action
|
||||
# cannot speak until #199 ports it.
|
||||
forge_preflight || exit 1
|
||||
# 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"
|
||||
|
||||
owner="${GITHUB_REPOSITORY%%/*}"
|
||||
name="${GITHUB_REPOSITORY#*/}"
|
||||
# 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).
|
||||
forge_preflight || exit 1
|
||||
forge_select "" || exit 1
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# GraphQL variables are literal API syntax; the shell must not expand them.
|
||||
# shellcheck disable=SC2016
|
||||
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"
|
||||
|
||||
# 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" \
|
||||
|
|
|
|||
36
changelog.d/199.md
Normal file
36
changelog.d/199.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
### Fixed
|
||||
|
||||
- `actions/refs-not-closing` gathers over REST through the forge shim instead
|
||||
of one GraphQL query, so it produces a real verdict on Forgejo — which
|
||||
serves no GraphQL surface at all — rather than refusing (#199).
|
||||
|
||||
- The closing set is parsed by `lib/closes_references.sh` over the PR body
|
||||
**and** every commit message, unioned. Forgejo honours closing keywords in
|
||||
commit messages, so a body-only port would miss a PR that closes an issue
|
||||
from a commit subject (#199).
|
||||
|
||||
- The `hasNextPage` refusal is carried onto the paginated commit read: an
|
||||
incomplete gather refuses instead of returning a partial verdict, reusing
|
||||
the backend's `x-total-count` completeness proof (#199).
|
||||
|
||||
- A failed read never reaches the parser. An unread body parses to an empty
|
||||
closing set, which is a passing verdict the action never earned (#199).
|
||||
|
||||
- `.github/workflows/refs-guard.yml` no longer gates the job on the forge.
|
||||
A portable action behind a github-only gate is a guard that passes by never
|
||||
running (#199, #198).
|
||||
|
||||
- The `CEREMONY_FORGE_CLIENT=gh` declaration is gone, and the contract test
|
||||
asserts its absence: an opt-out with no `gh` behind it is a standing
|
||||
permission slip (#199).
|
||||
|
||||
### Added
|
||||
|
||||
- `lib/issue_references.sh` — the LOCAL/CROSS classifier, moved out of
|
||||
`actions/issueflow-reconcile`'s executable so a second caller can use it
|
||||
without sourcing a reconciler, which would run one (#199, #61).
|
||||
|
||||
- `test/refs-not-closing.test.sh` drives the action's boundary on **both**
|
||||
backends with stubs at the transport, proving one fixture yields the same
|
||||
verdict on each — including a closing keyword that appears only in a commit
|
||||
message (#199).
|
||||
28
lib/issue_references.sh
Normal file
28
lib/issue_references.sh
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# lib/issue_references.sh — the LOCAL / CROSS reference classifier (#61).
|
||||
#
|
||||
# Sourced, never executed: no set -e/-u — the sourcing script owns its shell
|
||||
# options, as lib/closes_references.sh and lib/forge.sh do.
|
||||
#
|
||||
# WHY IT LIVES HERE. It was defined inside actions/issueflow-reconcile's
|
||||
# executable, and lib/closes_references.sh's header recorded the resulting
|
||||
# wart in prose: "DEPENDENCY: issue_references, from issueflow-reconcile.sh".
|
||||
# That was tolerable while the reconciler was its only caller. #199 makes
|
||||
# actions/refs-not-closing a second one, and a composite action cannot source
|
||||
# another action's program to borrow one function — sourcing a reconciler
|
||||
# runs a reconciler.
|
||||
#
|
||||
# So the dependency the comment described is now a file, and both callers
|
||||
# source it the same way. Nothing about the classifier changed.
|
||||
#
|
||||
# A qualified reference belongs to another repository. The whole token is
|
||||
# classified BEFORE any number is extracted, so `rig#112` can never be read
|
||||
# as local `#112` — which is the entire point of the function.
|
||||
|
||||
issue_references() { # text on stdin -> LOCAL/CROSS<TAB>reference
|
||||
{ grep -Eo '([[:alnum:]_.-]+/)?[[:alnum:]_.-]+#[0-9]+|#[0-9]+' || true; } \
|
||||
| awk '
|
||||
index($0, "#") == 1 { print "LOCAL\t" substr($0, 2); next }
|
||||
{ print "CROSS\t" $0 }
|
||||
'
|
||||
}
|
||||
|
|
@ -94,96 +94,151 @@ check "missing body is a loud failure" 1 "missing or unreadable" \
|
|||
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.
|
||||
# The action owns the network boundary. #199 made that boundary REST through
|
||||
# the shim, so it is driven here on BOTH backends with stubs at the transport
|
||||
# — a fake `gh api` for the github backend, a fake `curl` for the forgejo one.
|
||||
# Stubbing the shim itself would prove only that the test can stub the shim.
|
||||
mkdir -p "$TMP/bin"
|
||||
|
||||
# The fixture, one PR, expressed once and served by both stubs. The body
|
||||
# REFERENCES #5 while a commit CLOSES it: the contradiction this action
|
||||
# exists to catch, and the case a body-only port would miss (#199).
|
||||
PR_BODY_DEFAULT='Refs #5'
|
||||
COMMIT_MSG_DEFAULT='Closes #5
|
||||
|
||||
body text'
|
||||
|
||||
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
|
||||
;;
|
||||
[ -z "${FORGE_CALL_LOG:-}" ] || printf '%s\n' "$*" >>"$FORGE_CALL_LOG"
|
||||
endpoint=""; jqexpr=""
|
||||
shift # `api`
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--paginate) ;;
|
||||
--jq) jqexpr="$2"; shift ;;
|
||||
-*) ;;
|
||||
*) [ -n "$endpoint" ] || endpoint="$1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
case "${FAKE_FORGE_MODE:-success}" in
|
||||
body-fails) case "$endpoint" in */commits*) ;; *) echo "fake body read failed" >&2; exit 42 ;; esac ;;
|
||||
commits-fails) case "$endpoint" in */commits*) echo "fake commit read failed" >&2; exit 42 ;; esac ;;
|
||||
esac
|
||||
printf '{"data":{"repository":{"pullRequest":{"body":"Refs #5","closingIssuesReferences":{"nodes":[],"pageInfo":{"hasNextPage":%s}}}}}}\n' "$has_next"
|
||||
case "$endpoint" in
|
||||
*/commits*) out="$(jq -nc --arg m "${FAKE_COMMIT_MSG:-}" '[{commit:{message:$m}}]')" ;;
|
||||
*) out="$(jq -nc --arg b "${FAKE_PR_BODY:-}" '{body:$b}')" ;;
|
||||
esac
|
||||
if [ -n "$jqexpr" ]; then printf '%s' "$out" | jq -r "$jqexpr"; else printf '%s' "$out"; fi
|
||||
EOF
|
||||
chmod +x "$TMP/bin/gh"
|
||||
|
||||
action_boundary() {
|
||||
local mode="$1"
|
||||
# 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" \
|
||||
CEREMONY_FORGE=github \
|
||||
GITHUB_REPOSITORY="heavy-duty/ceremony" PR_NUMBER=268 \
|
||||
# curl as the forgejo backend calls it: -D <headers> -o <body> <url>. The
|
||||
# x-total-count header is what forge_api's completeness proof compares
|
||||
# against, so `partial` here is a REAL short gather, not a flag the test
|
||||
# invents (#188, #4699).
|
||||
cat >"$TMP/bin/curl" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
hdr=""; body=""; url=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-D) hdr="$2"; shift 2 ;;
|
||||
-o) body="$2"; shift 2 ;;
|
||||
-H) shift 2 ;;
|
||||
-sS|-s|-S) shift ;;
|
||||
*) url="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
[ -z "${FORGE_CALL_LOG:-}" ] || printf '%s\n' "$url" >>"$FORGE_CALL_LOG"
|
||||
case "${FAKE_FORGE_MODE:-success}" in
|
||||
body-fails) case "$url" in *"/commits"*) ;; *) echo "fake body read failed" >&2; exit 42 ;; esac ;;
|
||||
commits-fails) case "$url" in *"/commits"*) echo "fake commit read failed" >&2; exit 42 ;; esac ;;
|
||||
esac
|
||||
total=1
|
||||
case "$url" in
|
||||
*"/commits"*)
|
||||
payload="$(jq -nc --arg m "${FAKE_COMMIT_MSG:-}" '[{commit:{message:$m}}]')"
|
||||
# `partial`: declare 9, serve 1 and then nothing. Serving the same item on
|
||||
# every page instead would NOT be a short gather — forge_api walks until a
|
||||
# short page, so a stub that never runs out lets it collect exactly the
|
||||
# declared total and pass honestly. The first draft of this stub did that
|
||||
# and the case passed while proving nothing.
|
||||
if [ "${FAKE_FORGE_MODE:-success}" = partial ]; then
|
||||
total=9
|
||||
grep -q 'page=1\b' <<<"$url" || payload='[]'
|
||||
fi
|
||||
;;
|
||||
*) payload="$(jq -nc --arg b "${FAKE_PR_BODY:-}" '{body:$b}')" ;;
|
||||
esac
|
||||
printf 'HTTP/1.1 200 OK\r\nx-total-count: %s\r\n\r\n' "$total" >"$hdr"
|
||||
printf '%s' "$payload" >"$body"
|
||||
EOF
|
||||
chmod +x "$TMP/bin/curl"
|
||||
|
||||
# boundary <forge> <mode> [body] [commit-message]
|
||||
boundary() {
|
||||
local forge="$1" mode="$2"
|
||||
local body="${3-$PR_BODY_DEFAULT}" msg="${4-$COMMIT_MSG_DEFAULT}"
|
||||
env PATH="$TMP/bin:$PATH" \
|
||||
FAKE_FORGE_MODE="$mode" FAKE_PR_BODY="$body" FAKE_COMMIT_MSG="$msg" \
|
||||
CEREMONY_FORGE="$forge" \
|
||||
CEREMONY_FORGE_API="https://forge.example/api/v1" GH_TOKEN=tok \
|
||||
REPO="heavy-duty/ceremony" GITHUB_REPOSITORY="heavy-duty/ceremony" \
|
||||
PR_NUMBER=268 \
|
||||
GITHUB_ACTION_PATH="$ROOT/actions/refs-not-closing" \
|
||||
bash "$ENTRYPOINT"
|
||||
}
|
||||
|
||||
# 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" \
|
||||
CEREMONY_FORGE=forgejo \
|
||||
GITHUB_REPOSITORY="heavy-duty/ceremony" PR_NUMBER=268 \
|
||||
GITHUB_ACTION_PATH="$ROOT/actions/refs-not-closing" \
|
||||
bash "$ENTRYPOINT"
|
||||
}
|
||||
# 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 \
|
||||
"cannot speak it" forgejo_boundary
|
||||
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
|
||||
# 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"
|
||||
# THE PORTABILITY CLAIM, driven rather than asserted: one fixture, both
|
||||
# backends, same verdict. This is the acceptance criterion #199 states.
|
||||
check "a Refs promise contradicted by a commit is caught — github backend" 1 \
|
||||
"#5" boundary github success
|
||||
check "...and identically on the forgejo backend" 1 \
|
||||
"#5" boundary forgejo success
|
||||
|
||||
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
|
||||
# The half a body-only port would miss: the keyword is ONLY in the commit.
|
||||
check "a closing keyword only in a commit message is detected — github" 1 \
|
||||
"#5" boundary github success 'Refs #5' 'Closes #5'
|
||||
check "...and on forgejo" 1 \
|
||||
"#5" boundary forgejo success 'Refs #5' 'Closes #5'
|
||||
# ...and the same PR passes when nothing closes it, so the case above is
|
||||
# detecting the keyword rather than just failing on every input.
|
||||
check "a body that only references, with no closing keyword anywhere, passes" 0 \
|
||||
"" boundary forgejo success 'Refs #5' 'plain commit subject'
|
||||
|
||||
one_graphql_read() {
|
||||
[ "$(grep -c "gh api graphql" "$ENTRYPOINT")" -eq 1 ]
|
||||
printf '1\n'
|
||||
}
|
||||
# A failed read must never reach the parser: an empty body parses to an empty
|
||||
# closing set, which is a PASSING verdict the action never earned.
|
||||
check "a failed body read refuses, non-zero — github" 1 \
|
||||
"refusing a verdict" boundary github body-fails
|
||||
check "...and on forgejo" 1 "refusing a verdict" boundary forgejo body-fails
|
||||
check "a failed commit read refuses, non-zero" 1 \
|
||||
"refusing a partial verdict" boundary github commits-fails
|
||||
|
||||
# The `hasNextPage` refusal, relocated (#199 spec 3): a short paginated gather
|
||||
# is the REST equivalent, and the forgejo backend's x-total-count proof is
|
||||
# what catches it. This is the case that would silently pass if the port had
|
||||
# dropped --paginate.
|
||||
check "an incomplete commit read refuses a partial verdict" 1 \
|
||||
"refusing a partial verdict" boundary forgejo partial
|
||||
|
||||
# Strip comments first. The entrypoint's prose NAMES gh, GraphQL and
|
||||
# CEREMONY_FORGE_CLIENT to explain what it replaced, so a raw grep asserts on
|
||||
# the explanation rather than the code — it passes with the call still there.
|
||||
entrypoint_code() { sed 's/#.*//' "$ENTRYPOINT"; }
|
||||
invokes_gh() { entrypoint_code | grep -qE '(^|[^[:alnum:]_])gh[[:space:]]'; }
|
||||
holds_graphql() { entrypoint_code | grep -qi 'graphql'; }
|
||||
declares_client() { entrypoint_code | grep -q 'CEREMONY_FORGE_CLIENT'; }
|
||||
check "the entrypoint invokes no gh (#199)" 1 "" invokes_gh
|
||||
check "...and holds no GraphQL" 1 "" holds_graphql
|
||||
check "...and declares no forge client, because it speaks the shim" 1 "" declares_client
|
||||
# The caller must SCHEDULE it now. A portable action behind a github-only gate
|
||||
# is a guard that passes by never running (@kimi-reviewer-andresmgsl, #198).
|
||||
check "the caller no longer gates the job on the forge" 1 "" \
|
||||
grep -F "github.server_url" "$ROOT/.github/workflows/refs-guard.yml"
|
||||
|
||||
check "action performs exactly one GraphQL read" 0 "1" \
|
||||
one_graphql_read
|
||||
check "composite delegates to the tested entrypoint" 0 "run.sh" \
|
||||
grep -F "run: bash \"\$GITHUB_ACTION_PATH/run.sh\"" "$ACTION"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue