#!/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