94 lines
4.8 KiB
Bash
94 lines
4.8 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# The bootstrap bridge across the workflow_call boundary (#215).
|
||
|
|
#
|
||
|
|
# The defect: a called workflow cannot read the caller's dispatch inputs on
|
||
|
|
# this forge — `github.event.inputs.*` is empty inside `workflow_call` even
|
||
|
|
# though the top-level caller receives the value in both contexts (probe runs
|
||
|
|
# 6/7). The old gate read exactly that, so every dispatch-woken sweep
|
||
|
|
# bootstrapped (runs 459/523). The fix moves the value through a DECLARED
|
||
|
|
# `workflow_call` input, passed by the caller, with empty mapped to "no" at
|
||
|
|
# the caller so a cron can never bootstrap.
|
||
|
|
#
|
||
|
|
# These cases pin the wiring at every hop and drive the four value paths
|
||
|
|
# through the semantics of the exact expressions shipped — extracted from the
|
||
|
|
# YAML, never retyped, so an edited expression is an edited test input.
|
||
|
|
set -uo pipefail
|
||
|
|
|
||
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
|
|
# shellcheck source=test/harness.sh
|
||
|
|
. "$ROOT/test/harness.sh"
|
||
|
|
|
||
|
|
REUSABLE="$ROOT/.github/workflows/labels-sweep.yml"
|
||
|
|
CALLER="$ROOT/.github/workflows/self-labels-sweep.yml"
|
||
|
|
CONSUMERS="$ROOT/docs/CONSUMERS.md"
|
||
|
|
|
||
|
|
# --- the declared boundary ---------------------------------------------------
|
||
|
|
decl() { yq -r ".on.workflow_call.inputs.bootstrap.$1 // \"\"" "$REUSABLE"; }
|
||
|
|
declares_boundary() { [ -n "$(yq -r '.on.workflow_call.inputs.bootstrap // ""' "$REUSABLE")" ]; }
|
||
|
|
check "labels-sweep.yml declares bootstrap as a workflow_call input" 0 "" declares_boundary
|
||
|
|
check "...typed string" 0 "string" decl type
|
||
|
|
check "...defaulting to no — an absent pass-through must never bootstrap" 0 "no" \
|
||
|
|
decl default
|
||
|
|
|
||
|
|
# --- the gate reads the declared input, and nothing else ---------------------
|
||
|
|
gate_exprs() { yq -r '.jobs[].steps[] | select(.with.bootstrap != null) | .with.bootstrap' "$REUSABLE"; }
|
||
|
|
gates_are_identity() {
|
||
|
|
[ "$(gate_exprs | sort -u)" = "\${{ inputs.bootstrap }}" ] \
|
||
|
|
&& [ "$(gate_exprs | wc -l)" -eq 2 ]
|
||
|
|
}
|
||
|
|
check "both gate sites feed the DECLARED input, unchanged" 0 "" gates_are_identity
|
||
|
|
# The forbidden context is only live inside an expression: the file NAMES it
|
||
|
|
# in comments and in the declared input's description to explain the defect,
|
||
|
|
# and both are prose. Matching raw text asserted on the explanation — the
|
||
|
|
# adjacent-assertion trap this suite keeps re-learning — so the predicate is
|
||
|
|
# scoped to `${{ … }}` bodies.
|
||
|
|
reads_event_inputs() { grep -qE '\$\{\{[^}]*github\.event\.inputs' "$REUSABLE"; }
|
||
|
|
check "no expression in the reusable reads github.event.inputs — the context this forge empties" 1 "" \
|
||
|
|
reads_event_inputs
|
||
|
|
|
||
|
|
# --- the caller passes it through, empty mapped to no ------------------------
|
||
|
|
CALLER_EXPR="$(yq -r '.jobs.sweep.with.bootstrap // ""' "$CALLER")"
|
||
|
|
check "self-labels-sweep.yml passes with.bootstrap through the boundary" 0 "" \
|
||
|
|
test -n "$CALLER_EXPR"
|
||
|
|
check "...with the exact empty-guard expression" 0 "" \
|
||
|
|
test "$CALLER_EXPR" = "\${{ inputs.bootstrap || 'no' }}"
|
||
|
|
|
||
|
|
# The published stub must carry the same bridge, or every consumer inherits
|
||
|
|
# the defect ceremony just fixed for itself.
|
||
|
|
check "the CONSUMERS.md sweep stub passes bootstrap through the boundary" 0 \
|
||
|
|
"bootstrap: \${{ inputs.bootstrap || 'no' }}" \
|
||
|
|
grep -F "bootstrap: \${{ inputs.bootstrap || 'no' }}" "$CONSUMERS"
|
||
|
|
|
||
|
|
# --- the four value paths, through the shipped expressions -------------------
|
||
|
|
# Evaluate the caller expression's semantics for a given top-level value. The
|
||
|
|
# expression is asserted byte-exact above, so modelling `x || 'no'` here is
|
||
|
|
# modelling the string the tree actually ships, not a hope about it.
|
||
|
|
caller_pass() { [ -n "$1" ] && printf '%s' "$1" || printf 'no'; }
|
||
|
|
# The reusable's gate is asserted to be the identity; the value then meets
|
||
|
|
# actions/labels-reconcile's REAL validate step, extracted and executed.
|
||
|
|
VALIDATE="$(mktemp)"
|
||
|
|
trap 'rm -f "$VALIDATE"' EXIT
|
||
|
|
{
|
||
|
|
printf '%s\n' '#!/usr/bin/env bash'
|
||
|
|
yq -r '.runs.steps[] | select(.name == "validate bootstrap input") | .run' \
|
||
|
|
"$ROOT/actions/labels-reconcile/action.yml"
|
||
|
|
} >"$VALIDATE"
|
||
|
|
chmod +x "$VALIDATE"
|
||
|
|
path() { BOOTSTRAP="$(caller_pass "$1")" bash "$VALIDATE"; }
|
||
|
|
|
||
|
|
check "schedule (empty top-level context) validates as a non-bootstrap sweep" 0 "" path ""
|
||
|
|
check "a REST event wake passing no validates as a non-bootstrap sweep" 0 "" path no
|
||
|
|
check "a manual dispatch passing yes validates as a bootstrap" 0 "" path yes
|
||
|
|
invalid_path() { BOOTSTRAP="maybe" bash "$VALIDATE"; }
|
||
|
|
check "an invalid value reaches the validator UNSANITIZED and refuses" 2 \
|
||
|
|
"bootstrap must be 'yes' or 'no'" invalid_path
|
||
|
|
|
||
|
|
# ...and the non-bootstrap/bootstrap split is what the validator's callers
|
||
|
|
# act on: prove the two accepted values are distinguished, not merely both
|
||
|
|
# accepted, by pinning what each resolves to after the caller pass.
|
||
|
|
check "empty and no resolve identically — the cron can never bootstrap" 0 "" \
|
||
|
|
test "$(caller_pass "")" = "$(caller_pass no)"
|
||
|
|
check "...and yes stays yes through the pass" 0 "" test "$(caller_pass yes)" = yes
|
||
|
|
|
||
|
|
summary
|