Merge pull request '.github/workflows/labels.yml — wake the sweep over REST, so a board event reconciles in seconds (#205)' (#213) from build/205-dispatch-rest into main
All checks were successful
CI / test (push) Successful in 3m15s
CI / release-exercise (push) Has been skipped
CI / self-guards (push) Successful in 7s
CI / action-exercise (push) Successful in 6s
CI / docs-sync-exercise (push) Successful in 6s
release / release (push) Successful in 7s
All checks were successful
CI / test (push) Successful in 3m15s
CI / release-exercise (push) Has been skipped
CI / self-guards (push) Successful in 7s
CI / action-exercise (push) Successful in 6s
CI / docs-sync-exercise (push) Successful in 6s
release / release (push) Successful in 7s
Reviewed-on: #213 Reviewed-by: codex-reviewer-andresmgsl <andres+2@heavyduty.builders> Reviewed-by: kimi-reviewer-andresmgsl <andres+4@heavyduty.builders>
This commit is contained in:
commit
c5e987eb89
8 changed files with 355 additions and 45 deletions
2
.github/workflows/labels-sweep.yml
vendored
2
.github/workflows/labels-sweep.yml
vendored
|
|
@ -87,7 +87,7 @@ jobs:
|
|||
# equivalent of.
|
||||
#
|
||||
# bootstrap: every trigger-driven wake arrives as workflow_dispatch
|
||||
# too (that is how `gh workflow run` wakes the caller), so the event
|
||||
# too (that is how the trigger's dispatch wakes the caller), so the event
|
||||
# name alone no longer separates the operator's manual full-board
|
||||
# bootstrap from an event-woken sweep — the caller's `bootstrap`
|
||||
# dispatch input does: the trigger passes "no", a bare manual
|
||||
|
|
|
|||
93
.github/workflows/labels.yml
vendored
93
.github/workflows/labels.yml
vendored
|
|
@ -126,33 +126,76 @@ jobs:
|
|||
steps:
|
||||
- name: dispatch the sweep
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
SWEEP_WORKFLOW: ${{ inputs.sweep_workflow }}
|
||||
# This step speaks gh and says so, the same declaration
|
||||
# actions/refs-not-closing carries (#198 spec 4). A workflow has no
|
||||
# shell to call forge_preflight from, so the refusal is inline
|
||||
# below; #205 owns the REST port that removes both.
|
||||
CEREMONY_FORGE_CLIENT: gh
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
# Two questions, not one. @codex-reviewer-andresmgsl: a guard that
|
||||
# only asks `command -v gh` passes the moment a Forgejo runner image
|
||||
# happens to ship gh — and then runs a GitHub dispatch against a
|
||||
# forge that cannot serve it, which is the client/forge mismatch
|
||||
# forge_preflight exists to prevent. So the FORGE is decided first,
|
||||
# mirroring forge_detect positively (only github.com is accepted;
|
||||
# anything else, known or not, is refused — "Never 'probably
|
||||
# github'"), and the binary is checked second.
|
||||
# REST, not `gh` (#205). The workflow-dispatch endpoint has the SAME
|
||||
# shape on both forges —
|
||||
# POST {api}/repos/{owner}/{repo}/actions/workflows/{file}/dispatches
|
||||
# {"ref": "<branch>", "inputs": {...}} -> 204, empty body
|
||||
# — so this step no longer decides a forge at all. That is why the
|
||||
# `CEREMONY_FORGE_CLIENT: gh` declaration and both inline refusals are
|
||||
# gone rather than ported: there is nothing left to refuse. Measured
|
||||
# on this instance (Forgejo 8.0.3+gitea-1.22.0) and published in its
|
||||
# own swagger; run 459 was raised this way.
|
||||
#
|
||||
# A warning, not a failure: this trigger is the misconfiguration
|
||||
# alarm for a CONSUMER's missing sweep caller, and reddening every
|
||||
# sweep on a forge for a gap #205 already owns would drown that
|
||||
# signal. #205 ports the dispatch to REST and removes all of this.
|
||||
if [ "${GITHUB_SERVER_URL:-}" != "https://github.com" ]; then
|
||||
echo "::warning::labels: the sweep was NOT woken from this trigger — it dispatches with \`gh\` against GitHub, and this is not a GitHub forge (GITHUB_SERVER_URL=${GITHUB_SERVER_URL:-unset}). #205 ports it to REST. The hourly SCHEDULED sweep still runs; every event-driven wake through this caller — issue events included — is unavailable until then."
|
||||
exit 0
|
||||
# STILL LOUD on failure, per this job's contract: a consumer missing
|
||||
# the sweep caller, its `bootstrap` input, or `actions: write` must
|
||||
# fail HERE and visibly, not sweep silently never again.
|
||||
# NEVER "probably github" (lib/forge.sh). Defaulting an unset
|
||||
# GITHUB_API_URL to api.github.com would send this forge's dispatch
|
||||
# to GitHub and report success — the same unset-environment guess
|
||||
# #201 just refused for docs-sync. The API root is injected by the
|
||||
# forge running us; if it is absent we do not know where we are, and
|
||||
# a guess is worse than a red trigger
|
||||
# (@codex-reviewer-andresmgsl, #205 review).
|
||||
api="${GITHUB_API_URL:-}"
|
||||
if [ -z "$api" ]; then
|
||||
echo "::error::labels: the sweep was NOT woken — GITHUB_API_URL is unset, so the forge's API root is unknown. Refusing to guess a forge."
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "::warning::labels: the sweep was NOT woken from this trigger — this runner does not carry \`gh\`. #205 ports the dispatch to REST. The hourly SCHEDULED sweep still runs; every event-driven wake through this caller is unavailable until then."
|
||||
exit 0
|
||||
|
||||
# `gh workflow run` defaulted the ref to the repository's default
|
||||
# branch; REST has no default and 400s without one. Prefer the event
|
||||
# payload, fall back to an API read: on a `pull_request_target` run
|
||||
# GITHUB_REF_NAME is `<n>/merge`, which is not a branch and would
|
||||
# dispatch nothing.
|
||||
branch="${DEFAULT_BRANCH:-}"
|
||||
if [ -z "$branch" ]; then
|
||||
branch="$(curl -fsS -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"$api/repos/$GITHUB_REPOSITORY" | jq -r '.default_branch // empty')"
|
||||
fi
|
||||
gh workflow run "$SWEEP_WORKFLOW" -R "$GITHUB_REPOSITORY" -f bootstrap=no
|
||||
if [ -z "$branch" ]; then
|
||||
echo "::error::labels: the sweep was NOT woken — could not determine the default branch to dispatch $SWEEP_WORKFLOW on."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
out="$(mktemp)"
|
||||
err="$(mktemp)"
|
||||
trap 'rm -f "$out" "$err"' EXIT
|
||||
# A transport failure is named, not merely propagated. Letting `set
|
||||
# -e` carry curl's own exit code out of the assignment DID fail the
|
||||
# job — the invariant holds — but it failed with a bare status and no
|
||||
# sentence, which is the opposite of this step owning its diagnostic.
|
||||
if ! code="$(curl -sS -o "$out" -w '%{http_code}' -X POST \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$(jq -nc --arg ref "$branch" '{ref: $ref, inputs: {bootstrap: "no"}}')" \
|
||||
"$api/repos/$GITHUB_REPOSITORY/actions/workflows/$SWEEP_WORKFLOW/dispatches" \
|
||||
2>"$err")"; then
|
||||
echo "::error::labels: the sweep was NOT woken — the request to $api never completed: $(tr -d '\n' <"$err")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$code" != "204" ]; then
|
||||
# Own the diagnostic rather than pass the status through. This
|
||||
# Forgejo answers an unknown workflow name — and a bare ref that
|
||||
# does not resolve — with `500` and an EMPTY body, so the raw
|
||||
# status alone sends the reader looking for a server fault that is
|
||||
# not there.
|
||||
echo "::error::labels: the sweep was NOT woken — POST $api/repos/$GITHUB_REPOSITORY/actions/workflows/$SWEEP_WORKFLOW/dispatches (ref=$branch) returned HTTP $code: $(tr -d '\n' <"$out")"
|
||||
echo "::error::labels: check that $SWEEP_WORKFLOW exists on $branch, declares a \`bootstrap\` workflow_dispatch input, and that this caller grants \`actions: write\`. An empty 500 body from Forgejo means the workflow name or the ref did not resolve."
|
||||
exit 1
|
||||
fi
|
||||
echo "labels: sweep dispatched — $SWEEP_WORKFLOW on $branch (bootstrap=no)"
|
||||
|
|
|
|||
2
.github/workflows/self-labels.yml
vendored
2
.github/workflows/self-labels.yml
vendored
|
|
@ -39,7 +39,7 @@ permissions:
|
|||
contents: read
|
||||
checks: read # mergeability/check-rollup read for PR state
|
||||
statuses: read # commit-status rollup read for PR state
|
||||
actions: write # the trigger job's `gh workflow run` dispatch of the sweep caller (#209)
|
||||
actions: write # the trigger job's dispatch of the sweep caller (#209, #205)
|
||||
issues: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
|
|
|
|||
44
changelog.d/205.md
Normal file
44
changelog.d/205.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
### Fixed
|
||||
|
||||
- `.github/workflows/labels.yml` wakes the sweep over REST instead of
|
||||
`gh workflow run`, so a board event reconciles within seconds on any forge
|
||||
rather than waiting up to an hour for the scheduled sweep (#205).
|
||||
|
||||
- The workflow-dispatch endpoint has the same shape on both forges, so that
|
||||
step no longer decides one: the `CEREMONY_FORGE_CLIENT=gh` declaration and
|
||||
both inline refusals are gone rather than ported (#205).
|
||||
|
||||
- The dispatch supplies its `ref` explicitly, because REST has no default
|
||||
branch where `gh workflow run` had one, and refuses without it (#205).
|
||||
|
||||
- It takes that ref from the repository, never from `GITHUB_REF_NAME` — on a
|
||||
`pull_request_target` run that is `<n>/merge`, which is not a branch (#205).
|
||||
|
||||
- A failed dispatch names the endpoint, the ref and the status, and says that
|
||||
an empty `500` body from Forgejo means the workflow name or the ref did not
|
||||
resolve — a bare status sends the reader after a server fault that is not
|
||||
there (#205).
|
||||
|
||||
### Added
|
||||
|
||||
- `test/labels-dispatch.test.sh` extracts the shipped step and executes it
|
||||
against a recording stub, asserting the method, endpoint, ref and
|
||||
`inputs.bootstrap` actually sent (#205).
|
||||
|
||||
- That test also drives the failure path: any non-204 still fails the job, so
|
||||
the misconfiguration alarm the trigger exists to be cannot decay into a
|
||||
warning (#205).
|
||||
|
||||
- An unset `GITHUB_API_URL` refuses before any request instead of defaulting
|
||||
to `api.github.com`. Guessing sent this forge's dispatch to GitHub and
|
||||
reported success — the "Never 'probably github'" rule, and the same
|
||||
unset-environment refusal #201 established for docs-sync (#205).
|
||||
|
||||
- A dispatch that never reaches the forge names the failure. Letting `set -e`
|
||||
carry curl's exit code out did fail the job, but with a bare status and no
|
||||
sentence (#205).
|
||||
|
||||
- `docs/CONSUMERS.md` and both caller comments describe the REST dispatch, and
|
||||
the manual bootstrap command carries a forge-neutral form beside the `gh`
|
||||
one — a cross-forge runbook that directs this forge to a missing binary is
|
||||
wrong even where the surrounding prose is right (#205).
|
||||
|
|
@ -339,7 +339,9 @@ together at the same pin:
|
|||
- **`labels.yml`** — the event-facing half, called on PR and issue events.
|
||||
Two jobs: additive path-based `scope:*` labels, and a few-seconds
|
||||
`trigger` job that wakes the sweep by dispatching the consumer's sweep
|
||||
caller (`gh workflow run`, plain `GITHUB_TOKEN` — `workflow_dispatch` is
|
||||
caller (a REST `POST` to the forge's own
|
||||
`${GITHUB_API_URL}/repos/{owner}/{repo}/actions/workflows/{file}/dispatches`,
|
||||
plain `GITHUB_TOKEN` — `workflow_dispatch` is
|
||||
one of the two documented exemptions from the token's no-retrigger rule,
|
||||
so no PAT anywhere in the path and no loop: the sweep dispatches
|
||||
nothing).
|
||||
|
|
@ -404,7 +406,7 @@ permissions:
|
|||
contents: read
|
||||
checks: read # mergeability/check-rollup read for PR state
|
||||
statuses: read # commit-status rollup read for PR state
|
||||
actions: write # the trigger job's `gh workflow run` dispatch of the sweep caller (#209)
|
||||
actions: write # the trigger job's dispatch of the sweep caller (#209, #205)
|
||||
issues: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
|
|
@ -468,8 +470,8 @@ repositories allow check data to be read regardless, but a private consumer
|
|||
needs the explicit reads above; without them the failure appears as an empty
|
||||
`state:*` axis on the board rather than a red workflow run. The labels
|
||||
caller's `actions: write` is different — it is required everywhere, public
|
||||
repos included: the trigger job's `gh workflow run` is a write, and without
|
||||
it every event run goes red at the trigger.
|
||||
repos included: the trigger job's dispatch is a write, and without it every
|
||||
event run goes red at the trigger.
|
||||
|
||||
**The failure mode to know before bumping**: a consumer that bumps its pin
|
||||
to a #209-carrying tag without adding the sweep caller keeps green-looking
|
||||
|
|
@ -526,8 +528,8 @@ carrying the split:
|
|||
fires both callers into the one shared `labels-reconcile` group — so
|
||||
displacement goes **up**, and the fix reads as the bug getting worse.
|
||||
4. **`actions: write` on the labels caller** — consumers carry
|
||||
`actions: read` today (crew does); the trigger job's `gh workflow run`
|
||||
is a write. The sweep caller keeps `actions: read`.
|
||||
`actions: read` today (crew does); the trigger job's dispatch is a
|
||||
write. The sweep caller keeps `actions: read`.
|
||||
|
||||
Bump without the sweep caller and the trigger job goes red on every PR
|
||||
and issue event — the loud failure mode above — so never split these
|
||||
|
|
@ -596,10 +598,25 @@ to bootstrap labels on a fresh repository. A bare dispatch is also the
|
|||
operator's general manual full-board sweep — the answer when the board
|
||||
looks wrong now rather than after the next scheduled cadence:
|
||||
|
||||
On GitHub, with the `gh` CLI:
|
||||
|
||||
```sh
|
||||
gh workflow run labels-sweep.yml -R <owner>/<repo>
|
||||
```
|
||||
|
||||
On any forge — including Forgejo, whose runners carry no `gh` — the same
|
||||
dispatch over REST, which is what the trigger job itself sends (#205):
|
||||
|
||||
```sh
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"ref":"main","inputs":{"bootstrap":"yes"}}' \
|
||||
"$API/repos/<owner>/<repo>/actions/workflows/labels-sweep.yml/dispatches"
|
||||
```
|
||||
|
||||
`$API` is the forge's API root — `https://api.github.com` on GitHub,
|
||||
`<instance>/api/v1` on Forgejo — and success is `204` with an empty body.
|
||||
|
||||
Ceremony dogfoods the callers under the filenames `self-labels.yml` and
|
||||
`self-labels-sweep.yml`, so the equivalent command in this repository
|
||||
substitutes that filename. Scheduled and trigger-driven runs only
|
||||
|
|
|
|||
188
test/labels-dispatch.test.sh
Executable file
188
test/labels-dispatch.test.sh
Executable file
|
|
@ -0,0 +1,188 @@
|
|||
#!/usr/bin/env bash
|
||||
# The sweep dispatch in .github/workflows/labels.yml (#205).
|
||||
#
|
||||
# This EXTRACTS the shipped step's `run:` script and EXECUTES it against a
|
||||
# recording stub, rather than grepping the YAML for strings. A grep here would
|
||||
# pass on a step that assembles a perfect request and never sends it — the
|
||||
# shape of defect this repo keeps finding in its own tests. So every case
|
||||
# asserts on what the step actually sent, or on what it actually did when the
|
||||
# forge refused.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=test/harness.sh
|
||||
. "$ROOT/test/harness.sh"
|
||||
|
||||
WORKFLOW="$ROOT/.github/workflows/labels.yml"
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "${TMP:?}"' EXIT
|
||||
|
||||
# --- the step under test, taken from the shipped workflow --------------------
|
||||
STEP="$TMP/step.sh"
|
||||
{
|
||||
printf '%s\n' '#!/usr/bin/env bash' 'set -e'
|
||||
yq -r '.jobs.trigger.steps[] | select(.name == "dispatch the sweep") | .run' "$WORKFLOW"
|
||||
} >"$STEP"
|
||||
chmod +x "$STEP"
|
||||
|
||||
step_extracted() { [ "$(wc -l <"$STEP")" -ge 10 ]; }
|
||||
check "the step's script was extracted from the shipped workflow" 0 "" step_extracted
|
||||
|
||||
# --- stubs -------------------------------------------------------------------
|
||||
# `curl` records every invocation and answers with the code the case wants. It
|
||||
# parses only what the step actually passes, so a step that stopped sending
|
||||
# `-d`, or changed the method, fails here rather than recording nothing.
|
||||
make_curl() { # make_curl <http-code> <body>
|
||||
cat >"$TMP/bin/curl" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
method=GET; data=; url=; out=; write=
|
||||
while [ \$# -gt 0 ]; do
|
||||
case "\$1" in
|
||||
-X) method="\$2"; shift 2 ;;
|
||||
-d) data="\$2"; shift 2 ;;
|
||||
-o) out="\$2"; shift 2 ;;
|
||||
-w) write="\$2"; shift 2 ;;
|
||||
-H) printf '%s\n' "HEADER \$2" >>"$TMP/calls"; shift 2 ;;
|
||||
-fsS|-sS|-s) shift ;;
|
||||
*) url="\$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
printf '%s\n' "METHOD \$method" "URL \$url" "DATA \$data" >>"$TMP/calls"
|
||||
# the default-branch read is a plain GET whose stdout the step pipes to jq
|
||||
if [ "\$method" = GET ]; then printf '{"default_branch":"trunk"}'; exit 0; fi
|
||||
[ -n "\$out" ] && printf '%s' '$2' >"\$out"
|
||||
[ -n "\$write" ] && printf '%s' '$1'
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$TMP/bin/curl"
|
||||
}
|
||||
|
||||
run_step() { # run_step <http-code> <body> [env assignments...]
|
||||
local code="$1" body="$2"
|
||||
shift 2
|
||||
rm -rf "${TMP:?}/bin"
|
||||
mkdir -p "$TMP/bin"
|
||||
: >"$TMP/calls"
|
||||
make_curl "$code" "$body"
|
||||
env PATH="$TMP/bin:$PATH" \
|
||||
GITHUB_TOKEN=tok \
|
||||
GITHUB_API_URL=https://forge.example/api/v1 \
|
||||
GITHUB_REPOSITORY=owner/repo \
|
||||
SWEEP_WORKFLOW=self-labels-sweep.yml \
|
||||
"$@" \
|
||||
"$STEP"
|
||||
}
|
||||
|
||||
sent() { grep -h "^$1 " "$TMP/calls" | tail -1 | cut -d' ' -f2-; }
|
||||
sent_field() { jq -r "$1" <<<"$(sent DATA)"; }
|
||||
|
||||
# --- the success path --------------------------------------------------------
|
||||
check "a 204 dispatch succeeds, naming what it woke and where" 0 \
|
||||
"sweep dispatched — self-labels-sweep.yml on main" \
|
||||
run_step 204 "" DEFAULT_BRANCH=main
|
||||
|
||||
posted() { [ "$(sent METHOD)" = POST ]; }
|
||||
check "...by POST, not GET" 0 "" posted
|
||||
|
||||
right_endpoint() {
|
||||
[ "$(sent URL)" = \
|
||||
"https://forge.example/api/v1/repos/owner/repo/actions/workflows/self-labels-sweep.yml/dispatches" ]
|
||||
}
|
||||
check "...to the dispatches endpoint of the workflow it was told to wake" 0 "" right_endpoint
|
||||
|
||||
ref_is_main() { [ "$(sent_field .ref)" = main ]; }
|
||||
check "...carrying a ref, because REST has no default and refuses without one" 0 "" ref_is_main
|
||||
|
||||
bootstrap_is_string_no() { [ "$(sent_field '.inputs.bootstrap')" = no ]; }
|
||||
check "...and bootstrap=no as a STRING input, not a bare flag" 0 "" bootstrap_is_string_no
|
||||
|
||||
bearer_sent() { grep -qF 'HEADER Authorization: Bearer tok' "$TMP/calls"; }
|
||||
check "...under the bearer header both forges accept" 0 "" bearer_sent
|
||||
|
||||
# --- the ref it must NOT inherit ---------------------------------------------
|
||||
# On pull_request_target GITHUB_REF_NAME is `<n>/merge`. A step that reaches
|
||||
# for it dispatches at something that is not a branch — and the forge answers
|
||||
# that with the opaque 500, so it would look like an outage.
|
||||
check "a pull_request_target run still dispatches at the branch" 0 "" \
|
||||
run_step 204 "" DEFAULT_BRANCH=main GITHUB_REF_NAME=203/merge
|
||||
ref_is_not_a_merge_ref() { case "$(sent_field .ref)" in *merge*) return 1 ;; *) return 0 ;; esac; }
|
||||
check "...never at its merge ref" 0 "" ref_is_not_a_merge_ref
|
||||
|
||||
# --- the fallback ------------------------------------------------------------
|
||||
check "an absent default branch is read from the forge, not guessed" 0 "" \
|
||||
run_step 204 "" DEFAULT_BRANCH=
|
||||
ref_is_trunk() { [ "$(sent_field .ref)" = trunk ]; }
|
||||
check "...and the dispatch uses what the read returned" 0 "" ref_is_trunk
|
||||
|
||||
# --- failure is loud, and the diagnostic is owned ----------------------------
|
||||
# `gh workflow run` failing WAS the misconfiguration alarm. The port keeps that
|
||||
# contract: a consumer missing the caller, its input, or `actions: write` must
|
||||
# fail here rather than sweep silently never again.
|
||||
check "an empty 500 fails the step — the alarm still rings" 1 \
|
||||
"/self-labels-sweep.yml/dispatches (ref=main)" \
|
||||
run_step 500 "" DEFAULT_BRANCH=main
|
||||
check "...explaining Forgejo's EMPTY 500 rather than passing it through" 1 \
|
||||
"empty 500 body from Forgejo means the workflow name or the ref did not resolve" \
|
||||
run_step 500 "" DEFAULT_BRANCH=main
|
||||
check "...and naming the consumer causes the alarm exists for" 1 "actions: write" \
|
||||
run_step 500 "" DEFAULT_BRANCH=main
|
||||
check "any non-204 fails, not only the statuses the API documents" 1 "forbidden" \
|
||||
run_step 403 '{"message":"forbidden"}' DEFAULT_BRANCH=main
|
||||
|
||||
# --- the API root is not guessed ---------------------------------------------
|
||||
# Defaulting an unset GITHUB_API_URL to api.github.com sent this forge's
|
||||
# dispatch to GitHub and reported success (@codex-reviewer-andresmgsl). The
|
||||
# teeth are the call count: refusing AFTER a request is not refusing.
|
||||
run_step_no_api() {
|
||||
rm -rf "${TMP:?}/bin"; mkdir -p "$TMP/bin"; : >"$TMP/calls"
|
||||
make_curl 204 ""
|
||||
# -u, because plain `env` PRESERVES the parent environment: on the runner
|
||||
# every step arrives with GITHUB_API_URL set — the premise of the fix under
|
||||
# test — so without the unset this case inherits it, the refusal path never
|
||||
# executes, and the case passes only in a dev shell that lacks the variable
|
||||
# (@kimi-reviewer-andresmgsl, run 468).
|
||||
env -u GITHUB_API_URL \
|
||||
PATH="$TMP/bin:$PATH" GITHUB_TOKEN=tok GITHUB_REPOSITORY=owner/repo \
|
||||
SWEEP_WORKFLOW=self-labels-sweep.yml DEFAULT_BRANCH=main "$STEP"
|
||||
}
|
||||
check "an unset GITHUB_API_URL refuses rather than guessing GitHub" 1 \
|
||||
"GITHUB_API_URL is unset" run_step_no_api
|
||||
no_calls_made() { [ ! -s "$TMP/calls" ]; }
|
||||
check "...having made zero requests: refusing after a POST is not refusing" 0 "" no_calls_made
|
||||
|
||||
# --- a transport failure is not silence --------------------------------------
|
||||
# The old `|| true` invariant was asserted by grepping the gh line this port
|
||||
# removed, so it passed on any REST implementation including one that swallows
|
||||
# a failed POST (@codex-reviewer-andresmgsl). Driven instead: curl itself exits
|
||||
# non-zero, which `-w` cannot report because nothing is written.
|
||||
make_failing_curl() {
|
||||
printf '%s\n' '#!/usr/bin/env bash' 'echo "curl: (7) failed to connect" >&2' 'exit 7' \
|
||||
>"$TMP/bin/curl"
|
||||
chmod +x "$TMP/bin/curl"
|
||||
}
|
||||
run_step_curl_dies() {
|
||||
rm -rf "${TMP:?}/bin"; mkdir -p "$TMP/bin"; : >"$TMP/calls"
|
||||
make_failing_curl
|
||||
env PATH="$TMP/bin:$PATH" GITHUB_TOKEN=tok \
|
||||
GITHUB_API_URL=https://forge.example/api/v1 GITHUB_REPOSITORY=owner/repo \
|
||||
SWEEP_WORKFLOW=self-labels-sweep.yml DEFAULT_BRANCH=main "$STEP"
|
||||
}
|
||||
check "a POST that never reaches the forge fails the step, and says so" 1 \
|
||||
"never completed" run_step_curl_dies
|
||||
|
||||
# A code-aware guard alongside the behavioural one: no swallowing operator on
|
||||
# the dispatch itself.
|
||||
never_silenced() { sed 's/#.*//' "$STEP" | grep -qE '\|\|[[:space:]]*true'; }
|
||||
check "...and the step carries no || true" 1 "" never_silenced
|
||||
|
||||
# --- what the port removed ---------------------------------------------------
|
||||
# Strip comments first: the step's prose NAMES `gh workflow run` and
|
||||
# CEREMONY_FORGE_CLIENT to explain what it replaced, so a raw grep asserts on
|
||||
# the explanation instead of the code.
|
||||
step_code() { sed 's/#.*//' "$STEP"; }
|
||||
invokes_gh() { step_code | grep -qE '(^|[^[:alnum:]_])gh[[:space:]]'; }
|
||||
decides_forge() { step_code | grep -qE 'GITHUB_SERVER_URL|CEREMONY_FORGE_CLIENT'; }
|
||||
check "the step no longer INVOKES gh, its comments about it aside" 1 "" invokes_gh
|
||||
check "...and no longer decides a forge, because REST needs no branch" 1 "" decides_forge
|
||||
|
||||
summary
|
||||
|
|
@ -70,13 +70,24 @@ check "the sweep keeps the ONE shared concurrency group" 0 "group: labels-reconc
|
|||
grep -F 'group: labels-reconcile' "$SWEEP"
|
||||
check "labels.yml carries the trigger job" 0 " trigger:" \
|
||||
grep -E '^ trigger:' "$REUSABLE"
|
||||
# #205 ported this dispatch from `gh workflow run` to REST. The assertion is
|
||||
# the same one it always was — the sweep caller is woken BY NAME and never
|
||||
# bootstrapped — but it now has to hold against a request rather than a CLI
|
||||
# line. What the step actually SENDS is driven in test/labels-dispatch.test.sh;
|
||||
# these two keep the wiring pinned here alongside the rest of the trigger.
|
||||
# shellcheck disable=SC2016 # $SWEEP_WORKFLOW is the workflow's own env var, asserted literally
|
||||
check "the trigger dispatches the sweep caller, never bootstrapping" 0 \
|
||||
'gh workflow run "$SWEEP_WORKFLOW" -R "$GITHUB_REPOSITORY" -f bootstrap=no' \
|
||||
grep -F 'gh workflow run' "$REUSABLE"
|
||||
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
|
||||
check "the trigger dispatch is never silenced with || true" 1 "" \
|
||||
bash -c 'grep -F "gh workflow run" "$1" | grep -qF "|| true"' _ "$REUSABLE"
|
||||
check "the trigger dispatches the sweep caller by name" 0 \
|
||||
'actions/workflows/$SWEEP_WORKFLOW/dispatches' \
|
||||
grep -F '/dispatches' "$REUSABLE"
|
||||
check "...never bootstrapping" 0 'bootstrap: "no"' \
|
||||
grep -F 'bootstrap' "$REUSABLE"
|
||||
# The never-silenced invariant moved to test/labels-dispatch.test.sh, where it
|
||||
# is BEHAVIOURAL: a curl that dies at the transport must fail the extracted
|
||||
# step, plus a code-aware no-`|| true` guard on the step itself. The check that
|
||||
# lived here grepped the `gh workflow run` line #205 removed, so after the port
|
||||
# it passed on every implementation including one that swallows a failed POST —
|
||||
# a green assertion whose name claimed an invariant its implementation could
|
||||
# not observe (@codex-reviewer-andresmgsl, #213 review).
|
||||
check "the sweep caller filename input defaults to labels-sweep.yml" 0 \
|
||||
"default: labels-sweep.yml" grep -F 'default: labels-sweep.yml' "$REUSABLE"
|
||||
# the dogfood callers wear the split: the event caller names its deviant
|
||||
|
|
|
|||
|
|
@ -205,13 +205,20 @@ printf '%s\n' 'jobs:' ' t:' ' steps:' ' - env:' \
|
|||
' gh workflow run x' >"$TMP/declared-both.yml"
|
||||
check "...and a declaration guarding BOTH forge and binary is" 0 "" \
|
||||
refuses_when_unavailable "$TMP/declared-both.yml"
|
||||
# The shipped workflow is the real customer for that pair.
|
||||
check "labels.yml declares the client it speaks" 0 "" \
|
||||
# labels.yml WAS the real customer for that pair. #205 ported its dispatch to
|
||||
# REST, so it no longer speaks gh and must no longer declare a client — the
|
||||
# exemption is spent, not inherited. Asserting its ABSENCE is what stops the
|
||||
# declaration coming back as cover for a re-added `gh` call: an opt-out with no
|
||||
# gh behind it is a standing permission slip.
|
||||
check "labels.yml no longer declares a client, because it speaks none (#205)" 1 "" \
|
||||
declares_gh_client "$ROOT/.github/workflows/labels.yml"
|
||||
check "...decides the forge before dispatching" 0 "" \
|
||||
refuses_wrong_forge "$ROOT/.github/workflows/labels.yml"
|
||||
check "...and checks the binary too, rather than dying on command not found" 0 "" \
|
||||
refuses_missing_binary "$ROOT/.github/workflows/labels.yml"
|
||||
# shellcheck disable=SC2016 # `$SWEEP_WORKFLOW` is the literal the YAML must
|
||||
# carry: the endpoint has to be built from the caller's input, not hardcoded.
|
||||
labels_yml_dispatches_by_rest() {
|
||||
grep -qF 'actions/workflows/$SWEEP_WORKFLOW/dispatches' \
|
||||
"$ROOT/.github/workflows/labels.yml"
|
||||
}
|
||||
check "...and dispatches the sweep over REST instead" 0 "" labels_yml_dispatches_by_rest
|
||||
check "...and an undeclared one does not" 1 "" declares_gh_client "$TMP/bad.sh"
|
||||
# A mention of the variable in prose is not a declaration.
|
||||
printf '%s\n' '#!/usr/bin/env bash' '# CEREMONY_FORGE_CLIENT=gh would opt out' \
|
||||
|
|
|
|||
Loading…
Reference in a new issue