forked from heavy-duty/ceremony
feat(forge): refuse loudly when the client cannot speak the forge
The preflight half of #188, landed first so it stands alone: the forge is
decided once, before any sweep, and a client that cannot speak it exits
non-zero with a named reason.
Measured against forgejo.heavyduty.builders at 84bb1a4 — two of the three
actions reported SUCCESS having read nothing:
labels-scope exit 0 "no .github/labeler.yml" (the file is HTTP 200)
labels-reconcile exit 0 "reconciled." (zero PRs enumerated)
issueflow-reconcile exit 1 "unexpected end of JSON input"
labels-reconcile's blind-sweep warning (#96) could not fire: it counts
unreadable PRs against a list `gh pr list` never produced, and a process
substitution's failure does not trip set -e, so total stayed 0. Installing
gh makes it worse, silencing the one loud failure.
Detection is measured, not inferred from docs: a real forgejo-runner v6.3.1
job (probe task 278) shows Forgejo populating the whole GITHUB_* namespace,
so GITHUB_ACTIONS proves nothing. GITHUB_API_URL's shape, GITEA_ACTIONS and
GITHUB_SERVER_URL do. The same probe shows the runner image carries neither
gh nor stoke, which is what makes the forgejo backend REST.
Tests declare CEREMONY_FORGE at the forge boundary rather than stubbing gh
and staying silent about the forge — the boundary move term 5 asks for.
Refs #188
This commit is contained in:
parent
84bb1a424d
commit
7d52b2cd4a
8 changed files with 376 additions and 0 deletions
|
|
@ -27,6 +27,8 @@ TRIAGE_ACTORS=()
|
|||
# The needs-ruling invariants (#52) — one implementation for both surfaces.
|
||||
# shellcheck source=lib/ruling.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/ruling.sh"
|
||||
# shellcheck source=lib/forge.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/forge.sh"
|
||||
|
||||
log() { printf 'issueflow: %s\n' "$*"; }
|
||||
run() { if [ -n "${DRY_RUN:-}" ]; then log "DRY_RUN: $*"; else "$@"; fi; }
|
||||
|
|
@ -484,6 +486,12 @@ reconcile_opened_issue() {
|
|||
}
|
||||
|
||||
main() {
|
||||
# See labels-reconcile's twin (#188). This one already failed loudly on
|
||||
# Forgejo — but with `line 408: gh: command not found`, which names the
|
||||
# symptom and not the cause, and only after the sibling step had already
|
||||
# reported a green blind sweep.
|
||||
CEREMONY_FORGE_CLIENT="${CEREMONY_FORGE_CLIENT:-gh}" forge_preflight || return 1
|
||||
|
||||
local owner name
|
||||
REPO="${REPO:?set REPO to owner/name}"
|
||||
LABELS_CONF="${LABELS_CONF:-.github/labels.conf}"
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ STALE_AFTER=$((48 * 3600))
|
|||
# The needs-ruling invariants (#52) — one implementation for both surfaces.
|
||||
# shellcheck source=lib/ruling.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/ruling.sh"
|
||||
# shellcheck source=lib/forge.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/forge.sh"
|
||||
|
||||
log() { printf 'labels: %s\n' "$*"; }
|
||||
|
||||
|
|
@ -705,6 +707,12 @@ reconcile_pr() { # $1 = PR number; relies on the globals set from its fetch
|
|||
}
|
||||
|
||||
main() {
|
||||
# BEFORE anything reads the board (#188). Every call site below is still
|
||||
# `gh`, so that is what this declares — honestly, which is the point: on
|
||||
# a Forgejo consumer the preflight refuses here instead of letting the
|
||||
# sweep run blind and print "reconciled." over zero PRs (rig run 979).
|
||||
CEREMONY_FORGE_CLIENT="${CEREMONY_FORGE_CLIENT:-gh}" forge_preflight || return 1
|
||||
|
||||
REPO="${REPO:?set REPO to owner/name}"
|
||||
LABELS_CONF="${LABELS_CONF:-.github/labels.conf}"
|
||||
load_config "$LABELS_CONF"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ else
|
|||
set -u
|
||||
fi
|
||||
|
||||
# shellcheck source=lib/forge.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/forge.sh"
|
||||
|
||||
# labels-scope.sh — the additive half of the labels automation: derive
|
||||
# scope:* labels from a PR's changed paths and ADD them, touching nothing
|
||||
# else. This seat belonged to actions/labeler@v5 until #130: even under
|
||||
|
|
@ -125,6 +128,12 @@ derive_labels() { # $1 = "label<TAB>glob" lines, $2 = changed files (one per
|
|||
}
|
||||
|
||||
main() {
|
||||
# See labels-reconcile's twin (#188). This action's degraded read was the
|
||||
# quietest of the three: an unreadable mapping and an absent one produced
|
||||
# the same "nothing to derive" no-op, so on Forgejo a PR simply got no
|
||||
# scope labels and nothing said why.
|
||||
CEREMONY_FORGE_CLIENT="${CEREMONY_FORGE_CLIENT:-gh}" forge_preflight || return 1
|
||||
|
||||
REPO="${REPO:?set REPO to owner/name}"
|
||||
PR_NUMBER="${PR_NUMBER:?set PR_NUMBER to the pull request number}"
|
||||
CONFIG_REF="${CONFIG_REF:?set CONFIG_REF to the base commit the mapping is read at}"
|
||||
|
|
|
|||
16
changelog.d/188.md
Normal file
16
changelog.d/188.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
### Added
|
||||
|
||||
- `lib/forge.sh` — the forge selector: `forge_detect` names the forge from
|
||||
the runner's own environment, `forge_client` names the client it needs, and
|
||||
`forge_preflight` refuses loudly before any sweep when the two disagree
|
||||
(#188).
|
||||
- The reconcilers and `labels-scope` run that preflight first, so a
|
||||
GitHub-shaped client on a Forgejo instance is a named refusal instead of a
|
||||
sweep that reads nothing and reports success (#188).
|
||||
|
||||
### Fixed
|
||||
|
||||
- `labels-reconcile` and `labels-scope` no longer exit 0 on a Forgejo
|
||||
consumer having read zero facts — measured on `heavy-duty/rig`, where the
|
||||
sweep printed `reconciled.` over an empty PR list and scope reported "no
|
||||
labeler.yml" for a file that exists (#188).
|
||||
185
lib/forge.sh
Normal file
185
lib/forge.sh
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env bash
|
||||
# lib/forge.sh — one forge abstraction, two backends (issue #188).
|
||||
#
|
||||
# Sourced, never executed: no set -e/-u here — the sourcing script owns its
|
||||
# own shell options, exactly as lib/version.sh does. This file is the
|
||||
# selector only; the backends live beside it in lib/forge-github.sh and
|
||||
# lib/forge-forgejo.sh, and nothing here talks to a network.
|
||||
#
|
||||
# WHY THIS FILE EXISTS, stated once. Until #188 the reconcilers were `gh`
|
||||
# all the way down — 61 runtime call sites, no indirection, no forge check.
|
||||
# Pointed at a Forgejo instance (heavy-duty/rig, which moved here and runs
|
||||
# its CI on a Forgejo Actions runner) they did not fail usefully. Measured
|
||||
# against forgejo.heavyduty.builders on 2026-08-02, at ceremony 84bb1a4:
|
||||
#
|
||||
# labels-scope exit 0 "no .github/labeler.yml at main — nothing
|
||||
# to derive" — the file exists (HTTP 200)
|
||||
# labels-reconcile exit 0 "reconciled." — having enumerated ZERO PRs
|
||||
# issueflow-reconcile exit 1 "unexpected end of JSON input"
|
||||
#
|
||||
# Two of the three reported SUCCESS having read nothing. labels-reconcile's
|
||||
# own blind-sweep warning (#96) could not fire, because it counts unreadable
|
||||
# PRs against a list `gh pr list` never produced — and a process
|
||||
# substitution's failure does not trip set -e, so `total` stayed 0 and the
|
||||
# sweep called itself reconciled. rig run 979 is the log.
|
||||
#
|
||||
# The tempting fix — install gh on the runner — makes it WORSE. gh speaks
|
||||
# GitHub's /api/v3 against api.github.com; Forgejo serves /api/v1 and no
|
||||
# GraphQL at all. With gh present and GH_HOST set to the Forgejo host, the
|
||||
# one loud failure goes quiet (`gh pr list` hits /api/graphql -> HTTP 405,
|
||||
# prints nothing, exits into the same empty loop) and all three actions go
|
||||
# green while reading nothing. That is this repo's own doctrine — an
|
||||
# unreadable rollup reads as "nothing is failing" — being violated by the
|
||||
# repo that wrote it.
|
||||
#
|
||||
# So: the forge is decided ONCE, before any sweep, and a client that cannot
|
||||
# speak it refuses loudly. Never "probably github".
|
||||
|
||||
# forge_detect — print "github" or "forgejo"; exit 1 loudly when it cannot
|
||||
# tell. Order matters and every signal below was measured, not read from
|
||||
# docs: a real forgejo-runner v6.3.1 job on forgejo.heavyduty.builders
|
||||
# (probe task 278, 2026-08-02) dumped its environment, and a GitHub-hosted
|
||||
# runner's is the control.
|
||||
#
|
||||
# The trap that makes this non-obvious: **the Forgejo runner populates the
|
||||
# whole GITHUB_* namespace.** GITHUB_ACTIONS=true, GITHUB_REPOSITORY,
|
||||
# GITHUB_SHA, GITHUB_TOKEN — all set, all correct-looking. Detecting on
|
||||
# "GITHUB_ACTIONS is set" would answer "github" on both forges, which is
|
||||
# precisely the bug. What actually differs:
|
||||
#
|
||||
# signal GitHub Forgejo (measured)
|
||||
# GITHUB_API_URL https://api.github.com https://<host>/api/v1
|
||||
# GITHUB_GRAPHQL_URL https://api.github.com/… (empty)
|
||||
# GITEA_ACTIONS (unset) true
|
||||
#
|
||||
# GITHUB_GRAPHQL_URL being empty on Forgejo is not a curiosity — it is the
|
||||
# forge telling us the two `gh api graphql` sites #188 retired can never
|
||||
# work here. It is deliberately NOT a detection signal, though: an empty
|
||||
# variable is also what a hand-rolled harness leaves behind, and a signal
|
||||
# that fires on absence is a signal that fires by accident.
|
||||
forge_detect() {
|
||||
# 1. The explicit override outranks every probe — the escape hatch for a
|
||||
# forge this file has not met, and the handle the tests drive. A typo
|
||||
# in it is fatal on purpose: the operator said something and it was
|
||||
# wrong, and falling through to a probe that guesses right by accident
|
||||
# would hide that until the guess was wrong too.
|
||||
if [ -n "${CEREMONY_FORGE:-}" ]; then
|
||||
case "$CEREMONY_FORGE" in
|
||||
github | forgejo) printf '%s\n' "$CEREMONY_FORGE"; return 0 ;;
|
||||
*)
|
||||
echo "forge_detect: unknown forge: CEREMONY_FORGE=$CEREMONY_FORGE (expected github or forgejo)" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 2. Forgejo's and Gitea's own positive marker. Unambiguous where a
|
||||
# hand-set GITHUB_API_URL might not be, so it is read first.
|
||||
if [ "${GITEA_ACTIONS:-}" = true ] || [ "${FORGEJO_ACTIONS:-}" = true ]; then
|
||||
printf 'forgejo\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 3. The API URL's shape. /api/v3 is GitHub's (github.com and GitHub
|
||||
# Enterprise Server alike — GHES is a github backend on a non-github.com
|
||||
# host, and routing it to the forgejo backend would regress term 5's
|
||||
# "GitHub consumers are unchanged"). /api/v1 is the Gitea shape Forgejo
|
||||
# serves.
|
||||
case "${GITHUB_API_URL:-}" in
|
||||
https://api.github.com | https://api.github.com/*) printf 'github\n'; return 0 ;;
|
||||
*/api/v3 | */api/v3/*) printf 'github\n'; return 0 ;;
|
||||
*/api/v1 | */api/v1/*) printf 'forgejo\n'; return 0 ;;
|
||||
esac
|
||||
|
||||
# 4. Last resort, the server host. Only github.com itself is conclusive
|
||||
# here: a bare hostname says nothing about which API it serves.
|
||||
case "${GITHUB_SERVER_URL:-}" in
|
||||
https://github.com | https://github.com/*) printf 'github\n'; return 0 ;;
|
||||
esac
|
||||
|
||||
# 5. Refuse. "Nothing to read" is not "probably github" — guessing here
|
||||
# reinstates the exact blind sweep this file exists to end. Name what
|
||||
# was inspected and the escape hatch, so the log answers "why" without
|
||||
# a second run (#101 D5, one layer up: report, do not diagnose).
|
||||
cat >&2 <<EOF
|
||||
forge_detect: cannot determine which forge this is — refusing to guess (#188).
|
||||
GITHUB_API_URL='${GITHUB_API_URL:-}'
|
||||
GITHUB_SERVER_URL='${GITHUB_SERVER_URL:-}'
|
||||
GITEA_ACTIONS='${GITEA_ACTIONS:-}'
|
||||
Set CEREMONY_FORGE=github or CEREMONY_FORGE=forgejo to say so explicitly.
|
||||
EOF
|
||||
return 1
|
||||
}
|
||||
|
||||
# forge_client <forge> — print the client that backend requires.
|
||||
#
|
||||
# github -> gh the current call set, extracted 1:1 (term 5)
|
||||
# forgejo -> rest /api/v1 over curl+jq
|
||||
#
|
||||
# forgejo is "rest" by MEASUREMENT, not preference. The image the Forgejo
|
||||
# instance actually runs jobs in (ghcr.io/catthehacker/ubuntu:act-22.04,
|
||||
# probe task 278) carries curl, jq and node — and has neither `gh` NOR
|
||||
# `stoke` on PATH. That second absence is what retired option A from the
|
||||
# ruling: porting the call sites to the stoke CLI would have put a binary
|
||||
# on the critical path that the runner does not have and that would need
|
||||
# installing before every job.
|
||||
forge_client() {
|
||||
case "${1:?forge_client: forge required}" in
|
||||
github) printf 'gh\n' ;;
|
||||
forgejo) printf 'rest\n' ;;
|
||||
*)
|
||||
echo "forge_client: unknown forge: $1 (expected github or forgejo)" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# forge_preflight — the gate. Run it BEFORE any sweep: it decides the forge
|
||||
# and proves the client can speak it, or exits non-zero with a named reason.
|
||||
#
|
||||
# CEREMONY_FORGE_CLIENT declares the client the caller will actually use —
|
||||
# how a call site that still hard-codes `gh` announces itself honestly while
|
||||
# the backends are being ported. Two checks run, in order:
|
||||
#
|
||||
# 1. the declaration, when made, must match what this forge needs;
|
||||
# 2. that client's binaries must actually be on PATH — checked whether or
|
||||
# not a declaration was made, because a call site that declares the
|
||||
# right client on a runner that lacks it is still a blind sweep waiting
|
||||
# to happen.
|
||||
forge_preflight() {
|
||||
local forge want
|
||||
forge="$(forge_detect)" || return 1
|
||||
want="$(forge_client "$forge")" || return 1
|
||||
|
||||
if [ -n "${CEREMONY_FORGE_CLIENT:-}" ] && [ "$CEREMONY_FORGE_CLIENT" != "$want" ]; then
|
||||
cat >&2 <<EOF
|
||||
forge_preflight: this is a '$forge' forge and the '$CEREMONY_FORGE_CLIENT' client cannot speak it (#188).
|
||||
gh speaks GitHub's /api/v3 against api.github.com; Forgejo serves /api/v1
|
||||
and has no GraphQL surface at all. Pointing one at the other does not
|
||||
fail usefully — it reads nothing and reports success.
|
||||
This forge needs the '$want' client.
|
||||
EOF
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Then prove the tools are actually here — declared or not. A missing
|
||||
# binary is the rig failure verbatim, "line 692: gh: command not found",
|
||||
# and it must be a refusal before the sweep, not a 127 halfway through
|
||||
# one. Checked on BOTH paths deliberately: a call site that declares the
|
||||
# right client on a runner that lacks it is still a blind sweep waiting
|
||||
# to happen.
|
||||
local missing_bins=() bin
|
||||
case "$want" in
|
||||
gh) for bin in gh; do command -v "$bin" >/dev/null 2>&1 || missing_bins+=("$bin"); done ;;
|
||||
rest) for bin in curl jq; do command -v "$bin" >/dev/null 2>&1 || missing_bins+=("$bin"); done ;;
|
||||
esac
|
||||
if [ "${#missing_bins[@]}" -gt 0 ]; then
|
||||
cat >&2 <<EOF
|
||||
forge_preflight: this is a '$forge' forge, which needs the '$want' client, and ${missing_bins[*]} is not installed (#188).
|
||||
Refusing before the sweep: a reconciler that cannot read the board must
|
||||
not report that it reconciled one.
|
||||
EOF
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
138
test/forge.test.sh
Normal file
138
test/forge.test.sh
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/env bash
|
||||
# Contract tests for lib/forge.sh (issue #188). set -u, not -e: failing
|
||||
# commands 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"
|
||||
# shellcheck source=lib/forge.sh
|
||||
. "$ROOT/lib/forge.sh"
|
||||
|
||||
# eq <want> <cmd...> — succeeds AND prints exactly <want>. check()'s
|
||||
# substring match cannot prove "forgejo" was not printed as "forgejox".
|
||||
eq() {
|
||||
local want="$1" got
|
||||
shift
|
||||
got="$("$@")" || return 1
|
||||
[ "$got" = "$want" ]
|
||||
}
|
||||
|
||||
# detect_in <env-assignments…> — run forge_detect in a clean environment
|
||||
# carrying only the named vars, so a leaked GITHUB_* from the CI running
|
||||
# THIS suite cannot decide the answer. Every case below is hermetic.
|
||||
detect_in() {
|
||||
env -i PATH="$PATH" "$@" bash -c '. '"$ROOT"'/lib/forge.sh; forge_detect'
|
||||
}
|
||||
|
||||
preflight_in() {
|
||||
env -i PATH="$PATH" "$@" bash -c '. '"$ROOT"'/lib/forge.sh; forge_preflight'
|
||||
}
|
||||
|
||||
# --- forge_detect: the explicit override --------------------------------
|
||||
# CEREMONY_FORGE outranks every probe. It is the escape hatch for a forge
|
||||
# whose env this file has not met yet, and the handle the tests below use
|
||||
# to drive the backends without a live instance.
|
||||
|
||||
check "override: github" 0 "" eq github detect_in CEREMONY_FORGE=github
|
||||
check "override: forgejo" 0 "" eq forgejo detect_in CEREMONY_FORGE=forgejo
|
||||
check "override refuses an unknown forge" 1 "unknown forge" \
|
||||
detect_in CEREMONY_FORGE=gitlab
|
||||
# A typo must not silently fall through to a probe that guesses right by
|
||||
# accident: the operator said something, and it was wrong.
|
||||
check "override outranks the env" 1 "unknown forge" \
|
||||
detect_in CEREMONY_FORGE=gitlab GITHUB_API_URL=https://api.github.com
|
||||
|
||||
# --- forge_detect: GITHUB_API_URL, the load-bearing signal ---------------
|
||||
# Measured on forgejo.heavyduty.builders 2026-08-02 with a real
|
||||
# forgejo-runner v6.3.1 job (probe run, task 278). The Forgejo runner
|
||||
# populates the GITHUB_* namespace — GITHUB_ACTIONS=true and all — so
|
||||
# "GITHUB_ACTIONS is set" proves nothing at all. What differs is where
|
||||
# those URLs point:
|
||||
#
|
||||
# GitHub GITHUB_API_URL=https://api.github.com
|
||||
# Forgejo GITHUB_API_URL=https://forgejo.heavyduty.builders/api/v1
|
||||
#
|
||||
# That is the whole bug this issue exists for, in one variable: gh speaks
|
||||
# /api/v3 against api.github.com, and neither half is true here.
|
||||
|
||||
check "api url: api.github.com is github" 0 "" \
|
||||
eq github detect_in GITHUB_API_URL=https://api.github.com
|
||||
check "api url: /api/v1 is forgejo" 0 "" \
|
||||
eq forgejo detect_in GITHUB_API_URL=https://forgejo.heavyduty.builders/api/v1
|
||||
# GitHub Enterprise Server: a self-hosted GitHub still speaks /api/v3, and
|
||||
# it is a github backend on a non-github.com host. Getting this wrong would
|
||||
# route a GHES consumer to the forgejo backend and break term 5.
|
||||
check "api url: GHES /api/v3 is github" 0 "" \
|
||||
eq github detect_in GITHUB_API_URL=https://ghe.example.com/api/v3
|
||||
|
||||
# --- forge_detect: GITEA_ACTIONS, the positive marker --------------------
|
||||
# The Forgejo runner also exports GITEA_ACTIONS=true (measured, task 278),
|
||||
# which GitHub never sets. It is checked BEFORE the URL shape because it is
|
||||
# unambiguous where a hand-set GITHUB_API_URL might not be.
|
||||
|
||||
check "gitea marker alone is enough" 0 "" eq forgejo detect_in GITEA_ACTIONS=true
|
||||
check "gitea marker outranks a github-shaped api url" 0 "" \
|
||||
eq forgejo detect_in GITEA_ACTIONS=true GITHUB_API_URL=https://api.github.com
|
||||
|
||||
# --- forge_detect: refusing to guess ------------------------------------
|
||||
# Nothing to read is NOT "probably github". A wrong guess here is exactly
|
||||
# the silent blind sweep #188 measured; the whole point of this file is
|
||||
# that an unknown forge is loud.
|
||||
|
||||
check "bare environment refuses" 1 "cannot determine which forge" detect_in
|
||||
check "refusal names what it looked at" 1 "GITHUB_API_URL" detect_in
|
||||
check "refusal names the escape hatch" 1 "CEREMONY_FORGE" detect_in
|
||||
|
||||
# --- forge_preflight: the must-fail case --------------------------------
|
||||
# The Test plan's named must-fail: "point it at a Forgejo instance with a
|
||||
# GitHub-shaped client and assert it refuses loudly rather than sweeping
|
||||
# blind."
|
||||
#
|
||||
# Measured before this guard existed, against this instance:
|
||||
# labels-scope exit 0 "no .github/labeler.yml — nothing to derive" (it exists)
|
||||
# labels-reconcile exit 0 "reconciled." (zero PRs read)
|
||||
# issueflow-reconcile exit 1 "unexpected end of JSON input"
|
||||
# Two of three swept blind and reported success. gh present made it WORSE:
|
||||
# it silenced the one loud failure. Hence: refuse before the sweep, not
|
||||
# after — and say which forge and which client, so the log answers "why"
|
||||
# without a second run (#101 D5's report-do-not-diagnose, one layer up).
|
||||
|
||||
check "forgejo + gh-only client refuses" 1 "cannot speak" \
|
||||
preflight_in CEREMONY_FORGE=forgejo CEREMONY_FORGE_CLIENT=gh
|
||||
check "the refusal names the forge" 1 "forgejo" \
|
||||
preflight_in CEREMONY_FORGE=forgejo CEREMONY_FORGE_CLIENT=gh
|
||||
check "the refusal names the client" 1 "gh" \
|
||||
preflight_in CEREMONY_FORGE=forgejo CEREMONY_FORGE_CLIENT=gh
|
||||
# The refusal must be actionable, not merely loud: #188's whole cost was a
|
||||
# red check that told nobody what to do.
|
||||
check "the refusal names the issue" 1 "#188" \
|
||||
preflight_in CEREMONY_FORGE=forgejo CEREMONY_FORGE_CLIENT=gh
|
||||
|
||||
# --- forge_preflight: the passing pairs ---------------------------------
|
||||
|
||||
check "github + gh passes" 0 "" preflight_in CEREMONY_FORGE=github CEREMONY_FORGE_CLIENT=gh
|
||||
check "forgejo + rest passes" 0 "" preflight_in CEREMONY_FORGE=forgejo CEREMONY_FORGE_CLIENT=rest
|
||||
# The mirror of the must-fail: a Forgejo client against GitHub is just as
|
||||
# wrong, and symmetric refusal is cheaper than explaining why only one
|
||||
# direction is checked.
|
||||
check "github + rest refuses" 1 "cannot speak" \
|
||||
preflight_in CEREMONY_FORGE=github CEREMONY_FORGE_CLIENT=rest
|
||||
|
||||
# --- forge_preflight: it refuses when the forge itself is unknown --------
|
||||
# Detection failure must not be swallowed into a pass — that would restore
|
||||
# the blind sweep through the back door.
|
||||
check "unknown forge fails the preflight" 1 "cannot determine which forge" preflight_in
|
||||
|
||||
# --- forge_client: what each backend actually needs ----------------------
|
||||
# Measured in the runner image the Forgejo instance actually uses
|
||||
# (ghcr.io/catthehacker/ubuntu:act-22.04, task 278): gh ABSENT, stoke
|
||||
# ABSENT, curl and jq present. So the forgejo backend is REST-over-curl by
|
||||
# necessity, not preference — this is the measurement that retired option
|
||||
# A (port to stoke) as well: the CLI is not on the runner either.
|
||||
|
||||
check "github backend wants gh" 0 "" eq gh forge_client github
|
||||
check "forgejo backend wants rest" 0 "" eq rest forge_client forgejo
|
||||
check "forge_client refuses an unknown backend" 1 "unknown forge" forge_client gitlab
|
||||
|
||||
summary
|
||||
|
|
@ -581,9 +581,16 @@ printf '%s\n' \
|
|||
'{"data":{"repository":{"pullRequests":{"nodes":[],"pageInfo":{"hasNextPage":false,"endCursor":null}}}}}' \
|
||||
>"$ARRIVAL/fixtures/graphql.json"
|
||||
arrival_fixture() { printf '%s\n' "$1" >"$ARRIVAL/fixtures/repos_owner_repo_issues_91.json"; }
|
||||
# CEREMONY_FORGE=github below, and at the executable-sweep driver further
|
||||
# down: these fixtures ARE a GitHub board (a gh stub on PATH answering
|
||||
# /api/v3 shapes), so the suite says so at the forge boundary rather than
|
||||
# letting main()'s preflight infer a forge from whatever env the CI job
|
||||
# leaked (#188). Stubbing `gh` and staying silent about the forge is the
|
||||
# boundary this issue moved.
|
||||
arrival_run() {
|
||||
: >"$ARRIVAL/fixtures/edits"
|
||||
env PATH="$ARRIVAL/stub:$PATH" GH_FIXTURES="$ARRIVAL/fixtures" \
|
||||
CEREMONY_FORGE=github \
|
||||
REPO=owner/repo LABELS_CONF="$ARRIVAL/labels.conf" \
|
||||
EVENT_NAME=issues EVENT_ACTION=opened EVENT_ISSUE=91 \
|
||||
bash "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh"
|
||||
|
|
@ -633,6 +640,7 @@ printf '[]\n' >"$ARRIVAL/fixtures/repos_owner_repo_issues_40_comments.json"
|
|||
: >"$ARRIVAL/fixtures/edits"
|
||||
subprocess_out="$(
|
||||
env PATH="$ARRIVAL/stub:$PATH" GH_FIXTURES="$ARRIVAL/fixtures" \
|
||||
CEREMONY_FORGE=github \
|
||||
REPO=owner/repo LABELS_CONF="$ARRIVAL/labels.conf" \
|
||||
bash "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" 2>&1
|
||||
)"
|
||||
|
|
|
|||
|
|
@ -738,6 +738,9 @@ blind_main_probe() {
|
|||
GITHUB_EVENT_NAME=schedule
|
||||
REPO=owner/repo
|
||||
LABELS_CONF=.github/labels.conf
|
||||
# This probe IS a GitHub board — say so at the forge boundary rather
|
||||
# than leaving main()'s preflight to infer one (#188).
|
||||
CEREMONY_FORGE=github
|
||||
gh() {
|
||||
if [ "$1" = label ] && [ "$2" = list ]; then
|
||||
core_label_rows | cut -d'|' -f1
|
||||
|
|
@ -922,6 +925,7 @@ printf 'panel=bot-a bot-b bot-c\n' >"$EXEC/labels.conf"
|
|||
exec_env() { # $1 = event name → the real script, executed under the PATH stub
|
||||
: >"$EXEC/record"
|
||||
env PATH="$EXEC/stub:$PATH" GH_RECORD="$EXEC/record" \
|
||||
CEREMONY_FORGE=github \
|
||||
REPO=owner/repo LABELS_CONF="$EXEC/labels.conf" GITHUB_EVENT_NAME="$1" \
|
||||
bash actions/labels-reconcile/labels-reconcile.sh
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue