#!/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 cat >"$TMP/bin/curl" <>"$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 [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 `/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