From baa683211eb342f608718b97dc931aa65df6224a Mon Sep 17 00:00:00 2001 From: clad2 Date: Wed, 5 Aug 2026 21:06:29 +0000 Subject: [PATCH 1/6] fix(labels): pass bootstrap through the workflow_call boundary it was lost at MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 sweep's gate read exactly that, so every dispatch-woken sweep bootstrapped — runs 459 and 523, ~20 label upserts per board event — while the trigger honestly logged bootstrap=no. The bridge, per the #6361 contract: labels-sweep.yml declares workflow_call.inputs.bootstrap (string, default "no"); the dogfood caller and the published CONSUMERS.md stub pass it via with.bootstrap with empty mapped to "no" at the caller — kimi's edge: on schedule the top-level context is empty, and an empty that slipped through would have turned every cron into a bootstrap. The gate feeds the declared input to labels-reconcile unchanged, so an invalid value meets the action's own yes|no refusal. test/labels-bootstrap.test.sh pins every hop: the declared boundary, both gates as the identity, no expression reading github.event.inputs (scoped to ${{ }} bodies — the file's prose names the context to explain it), the two pass-throughs byte-exact, and the four value paths driven through the shipped expressions into the action's real validator. Mutations: dropping the declaration reds 4, dropping the pass-through reds 3, restoring the old gate reds 2. Refs #215 --- .github/workflows/labels-sweep.yml | 34 ++++++--- .github/workflows/self-labels-sweep.yml | 7 ++ changelog.d/215.md | 29 ++++++++ docs/CONSUMERS.md | 8 ++- test/labels-bootstrap.test.sh | 93 +++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 changelog.d/215.md create mode 100755 test/labels-bootstrap.test.sh diff --git a/.github/workflows/labels-sweep.yml b/.github/workflows/labels-sweep.yml index defca41..760590b 100644 --- a/.github/workflows/labels-sweep.yml +++ b/.github/workflows/labels-sweep.yml @@ -34,6 +34,19 @@ name: labels-sweep on: workflow_call: inputs: + bootstrap: + description: >- + Bootstrap the label taxonomy before sweeping. The CALLER passes + this through from its own workflow_dispatch input; a called + workflow cannot read the caller's dispatch inputs on every forge + (Forgejo drops them at the workflow_call boundary — ceremony#215, + probe runs 6/7 vs merged runs 459/523), so the value must arrive + through this declared boundary, never via github.event.inputs. + Absent means "no": an event- or cron-woken sweep must never + re-upsert ~20 labels. + type: string + required: false + default: "no" pr_workflow_name: description: >- The `name:` of the consumer's PR-facing labels caller, exported @@ -86,18 +99,21 @@ jobs: # papers over for `run:` steps, which composite `uses:` has no # equivalent of. # - # bootstrap: every trigger-driven wake arrives as workflow_dispatch - # 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 - # dispatch defaults to "yes". A caller reached on any other event - # (the cron) has no input and stays "no". + # bootstrap: read from the DECLARED workflow_call input and nothing + # else. The old gate read `github.event.inputs.bootstrap` from inside + # this called workflow — which Forgejo leaves empty at the + # workflow_call boundary even though the top-level caller receives the + # value in both contexts (ceremony#215; probe runs 6/7 measured the + # boundary, merged runs 459/523 paid for it: every dispatch-woken + # sweep bootstrapped). The caller passes the value through + # `with.bootstrap`; anything not exactly "yes" is fed through for + # labels-reconcile's own yes|no validation to judge, so a typo refuses + # loudly instead of silently bootstrapping. - name: reconcile state + stale if: github.repository != 'heavy-duty/ceremony' uses: ./.ceremony-src/actions/labels-reconcile with: - bootstrap: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.bootstrap != 'no' && 'yes' || 'no' }} + bootstrap: ${{ inputs.bootstrap }} env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} @@ -106,7 +122,7 @@ jobs: if: github.repository == 'heavy-duty/ceremony' uses: ./actions/labels-reconcile with: - bootstrap: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.bootstrap != 'no' && 'yes' || 'no' }} + bootstrap: ${{ inputs.bootstrap }} env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} diff --git a/.github/workflows/self-labels-sweep.yml b/.github/workflows/self-labels-sweep.yml index 94ae4a3..e2dcca3 100644 --- a/.github/workflows/self-labels-sweep.yml +++ b/.github/workflows/self-labels-sweep.yml @@ -44,3 +44,10 @@ jobs: # pr_workflow_name keeps its default: ceremony's PR-facing caller is # named `labels` (self-labels.yml). uses: ./.github/workflows/labels-sweep.yml + with: + # The dispatch input crosses the workflow_call boundary HERE, or not at + # all: the called workflow cannot read this caller's dispatch inputs on + # this forge (ceremony#215). On `schedule` the top-level context is + # empty, and empty maps to "no" EXPLICITLY — a cron that bootstraps is + # the failure kimi named before it could exist. + bootstrap: ${{ inputs.bootstrap || 'no' }} diff --git a/changelog.d/215.md b/changelog.d/215.md new file mode 100644 index 0000000..d94f219 --- /dev/null +++ b/changelog.d/215.md @@ -0,0 +1,29 @@ +### Fixed + +- The sweep's `bootstrap` value crosses the `workflow_call` boundary as a + declared input passed by the caller — a called workflow cannot read the + caller's dispatch inputs on this forge (#215). + +- Before the bridge, `github.event.inputs` was empty inside the called + workflow, so every dispatch-woken sweep bootstrapped: ~20 label upserts on + each board event (#215). + +- The caller maps an empty top-level value to `no` explicitly, so a + cron-woken sweep can never bootstrap; the declared input also defaults to + `no`, so a consumer that passes nothing gets the safe path (#215). + +- The gate feeds the declared input to `labels-reconcile` unchanged, so an + invalid value meets the action's own `yes|no` refusal instead of being + silently coerced (#215). + +- `docs/CONSUMERS.md`'s published sweep stub carries the same pass-through — + without it every consumer inherits the defect ceremony fixed for + itself (#215). + +### Added + +- `test/labels-bootstrap.test.sh` pins the bridge at every hop: the declared + boundary, both gate sites as the identity, no expression reading + `github.event.inputs`, the caller and stub pass-throughs byte-exact, and + the four value paths driven through the shipped expressions into the + action's real validator (#215). diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index e5a3d59..e5153f6 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -458,8 +458,14 @@ permissions: jobs: sweep: uses: heavy-duty/ceremony/.github/workflows/labels-sweep.yml@ + with: + # Pass the dispatch input through the workflow_call boundary — the + # called workflow cannot read this caller's dispatch inputs on every + # forge (ceremony#215). Empty (schedule) maps to "no" explicitly, so a + # cron-woken sweep never re-upserts the taxonomy. + bootstrap: ${{ inputs.bootstrap || 'no' }} # If this repo's PR-facing labels caller is named anything but `labels`, - # pass that name: `with: { pr_workflow_name: }`. The sweep exports + # pass that name alongside: `pr_workflow_name: `. The sweep exports # it as SELF_WORKFLOW so the label machinery's own check entries (scope, # trigger) never count toward blocker:ci-red — a red trigger means "fix # the caller", which no PR edit can do (#208 reads it). diff --git a/test/labels-bootstrap.test.sh b/test/labels-bootstrap.test.sh new file mode 100755 index 0000000..f8c0158 --- /dev/null +++ b/test/labels-bootstrap.test.sh @@ -0,0 +1,93 @@ +#!/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 From 69d674cb67e563205571c85dc27cbc5e3bebfda1 Mon Sep 17 00:00:00 2001 From: clad2 Date: Wed, 5 Aug 2026 21:08:51 +0000 Subject: [PATCH 2/6] docs(changelog): split the test entry under the 300-character bound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pushed the previous commit with this red — the cite check names the bound and I read only the first failure it printed. Refs #215 --- changelog.d/215.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/changelog.d/215.md b/changelog.d/215.md index d94f219..f89ef00 100644 --- a/changelog.d/215.md +++ b/changelog.d/215.md @@ -24,6 +24,9 @@ - `test/labels-bootstrap.test.sh` pins the bridge at every hop: the declared boundary, both gate sites as the identity, no expression reading - `github.event.inputs`, the caller and stub pass-throughs byte-exact, and - the four value paths driven through the shipped expressions into the - action's real validator (#215). + `github.event.inputs`, and the caller and stub pass-throughs + byte-exact (#215). + +- The same test drives the four value paths — schedule-empty, `no`, `yes`, + invalid — through the shipped expressions into the action's real + validator (#215). From ceaf66bd135fef2c4264e75da9f00771a17d6962 Mon Sep 17 00:00:00 2001 From: clad2 Date: Wed, 5 Aug 2026 21:18:29 +0000 Subject: [PATCH 3/6] docs(labels): claim the measured invariant, not an unmeasured Forgejo defect The evidence shows the called workflow did not see the caller's event inputs on THIS instance while the top level received them (runs 459/523 vs probe 6/7); it does not show what GitHub or a declared-and-passed input does, so every prose site now states declare-and-pass as the measured-reliable channel rather than attributing a drop to Forgejo (@codex-reviewer-andresmgsl, !218 blocker 2). Refs #215 --- .github/workflows/labels-sweep.yml | 29 +++++++++++++------------ .github/workflows/self-labels-sweep.yml | 7 +++--- changelog.d/215.md | 5 +++-- docs/CONSUMERS.md | 9 ++++---- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/.github/workflows/labels-sweep.yml b/.github/workflows/labels-sweep.yml index 760590b..705f4bf 100644 --- a/.github/workflows/labels-sweep.yml +++ b/.github/workflows/labels-sweep.yml @@ -37,12 +37,13 @@ on: bootstrap: description: >- Bootstrap the label taxonomy before sweeping. The CALLER passes - this through from its own workflow_dispatch input; a called - workflow cannot read the caller's dispatch inputs on every forge - (Forgejo drops them at the workflow_call boundary — ceremony#215, - probe runs 6/7 vs merged runs 459/523), so the value must arrive - through this declared boundary, never via github.event.inputs. - Absent means "no": an event- or cron-woken sweep must never + this through from its own workflow_dispatch input. The measured + invariant (ceremony#215): the value must be DECLARED here and + EXPLICITLY passed — on this instance the called workflow did not + see the caller's event inputs as an implicit substitute (runs + 459/523 bootstrapped on a bootstrap=no dispatch) while the + top-level caller received the value in both contexts (probe runs + 6/7). Absent means "no": an event- or cron-woken sweep must never re-upsert ~20 labels. type: string required: false @@ -101,14 +102,14 @@ jobs: # # bootstrap: read from the DECLARED workflow_call input and nothing # else. The old gate read `github.event.inputs.bootstrap` from inside - # this called workflow — which Forgejo leaves empty at the - # workflow_call boundary even though the top-level caller receives the - # value in both contexts (ceremony#215; probe runs 6/7 measured the - # boundary, merged runs 459/523 paid for it: every dispatch-woken - # sweep bootstrapped). The caller passes the value through - # `with.bootstrap`; anything not exactly "yes" is fed through for - # labels-reconcile's own yes|no validation to judge, so a typo refuses - # loudly instead of silently bootstrapping. + # this called workflow, and on this instance that context arrived + # empty (runs 459/523: every dispatch-woken sweep bootstrapped on a + # bootstrap=no body) while the top-level caller received the value in + # both contexts (probe runs 6/7) — ceremony#215. The reliable channel + # is declare-and-pass, so that is the only one used. The caller passes + # the value through `with.bootstrap`; anything not exactly yes|no is + # fed through for labels-reconcile's own validation to judge, so a + # typo refuses loudly instead of silently bootstrapping. - name: reconcile state + stale if: github.repository != 'heavy-duty/ceremony' uses: ./.ceremony-src/actions/labels-reconcile diff --git a/.github/workflows/self-labels-sweep.yml b/.github/workflows/self-labels-sweep.yml index e2dcca3..b929cb5 100644 --- a/.github/workflows/self-labels-sweep.yml +++ b/.github/workflows/self-labels-sweep.yml @@ -45,9 +45,10 @@ jobs: # named `labels` (self-labels.yml). uses: ./.github/workflows/labels-sweep.yml with: - # The dispatch input crosses the workflow_call boundary HERE, or not at - # all: the called workflow cannot read this caller's dispatch inputs on - # this forge (ceremony#215). On `schedule` the top-level context is + # The dispatch input crosses the workflow_call boundary HERE, or not + # at all: on this instance the called workflow did not see this + # caller's event inputs implicitly (ceremony#215), so declare-and-pass + # is the only channel used. On `schedule` the top-level context is # empty, and empty maps to "no" EXPLICITLY — a cron that bootstraps is # the failure kimi named before it could exist. bootstrap: ${{ inputs.bootstrap || 'no' }} diff --git a/changelog.d/215.md b/changelog.d/215.md index f89ef00..9411adf 100644 --- a/changelog.d/215.md +++ b/changelog.d/215.md @@ -1,8 +1,9 @@ ### Fixed - The sweep's `bootstrap` value crosses the `workflow_call` boundary as a - declared input passed by the caller — a called workflow cannot read the - caller's dispatch inputs on this forge (#215). + declared input, explicitly passed by the caller — the one channel measured + to work; the called workflow did not see the caller's event inputs as an + implicit substitute on this instance (#215). - Before the bridge, `github.event.inputs` was empty inside the called workflow, so every dispatch-woken sweep bootstrapped: ~20 label upserts on diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index e5153f6..9afc321 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -459,10 +459,11 @@ jobs: sweep: uses: heavy-duty/ceremony/.github/workflows/labels-sweep.yml@ with: - # Pass the dispatch input through the workflow_call boundary — the - # called workflow cannot read this caller's dispatch inputs on every - # forge (ceremony#215). Empty (schedule) maps to "no" explicitly, so a - # cron-woken sweep never re-upserts the taxonomy. + # Pass the dispatch input through the workflow_call boundary + # explicitly — a called workflow must not rely on reading the caller's + # event inputs (ceremony#215 measured that failing). Empty (schedule) + # maps to "no" explicitly, so a cron-woken sweep never re-upserts the + # taxonomy. bootstrap: ${{ inputs.bootstrap || 'no' }} # If this repo's PR-facing labels caller is named anything but `labels`, # pass that name alongside: `pr_workflow_name: `. The sweep exports From 6986deede7aec4eb8e05ab13fddbf905adc590ae Mon Sep 17 00:00:00 2001 From: clad2 Date: Wed, 5 Aug 2026 21:32:21 +0000 Subject: [PATCH 4/6] fix(labels): the bootstrap keys on the BOOTSTRAP input, never the event name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The venue drill caught what no hermetic test had: with the workflow_call bridge delivering "no" perfectly, drill runs 16/17 still bootstrapped. The script gated on GITHUB_EVENT_NAME = workflow_dispatch and never read $BOOTSTRAP at all; the wrapper's only coupling was exporting the event name for yes. Correct while an operator's manual dispatch was the only dispatch there was — inert from #209 on, when the trigger job made every machine wake a workflow_dispatch event. Runs 459/523 bootstrapped for this reason, not for the input-delivery defect, which is real but was never the operative cause of the observed re-upserts. The script now gates on ${BOOTSTRAP:-no} = yes; the wrapper passes the input through untouched; the hermetic suite pins the exact regression pair (a dispatch event with no/unset creates and deletes nothing) alongside the yes path's full create+delete assertions. Refs #215 --- actions/labels-reconcile/action.yml | 7 +++--- actions/labels-reconcile/labels-reconcile.sh | 16 ++++++++++---- changelog.d/215.md | 14 ++++++++++++ test/labels-reconcile.test.sh | 23 +++++++++++++------- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/actions/labels-reconcile/action.yml b/actions/labels-reconcile/action.yml index 36257fe..90a5f53 100644 --- a/actions/labels-reconcile/action.yml +++ b/actions/labels-reconcile/action.yml @@ -23,7 +23,8 @@ runs: BOOTSTRAP: ${{ inputs.bootstrap }} LABELS_CONF: ${{ github.workspace }}/.github/labels.conf run: | - if [ "$BOOTSTRAP" = yes ]; then - export GITHUB_EVENT_NAME=workflow_dispatch - fi + # BOOTSTRAP passes through as-is: the script gates on the input. The + # export-the-event-name hack that lived here died with ceremony#215 — + # the script keyed on GITHUB_EVENT_NAME, which #209 made true for + # every machine wake, so "no" could never mean no. bash "$GITHUB_ACTION_PATH/labels-reconcile.sh" diff --git a/actions/labels-reconcile/labels-reconcile.sh b/actions/labels-reconcile/labels-reconcile.sh index 449ab12..72e8375 100755 --- a/actions/labels-reconcile/labels-reconcile.sh +++ b/actions/labels-reconcile/labels-reconcile.sh @@ -28,8 +28,9 @@ fi # stale approval must never promote unreviewed code to the human. # # DRY_RUN=1 narrates every mutation instead of performing it (how this script -# is rehearsed against the live repo). A workflow_dispatch run also bootstraps -# the taxonomy (label create --force) — that heal is dispatch-only; the cron +# is rehearsed against the live repo). A run with BOOTSTRAP=yes also +# bootstraps the taxonomy (label create --force) — the operator's manual +# dispatch defaults the input to yes; every machine wake passes no. The cron # sweep tolerates a missing label rather than recreating it. # # The state machine below is pure (globals in, state out) and covered by @@ -1021,8 +1022,15 @@ main() { load_config "$LABELS_CONF" NOW="$(date +%s)" - if [ "${GITHUB_EVENT_NAME:-}" = workflow_dispatch ]; then - log "workflow_dispatch: bootstrapping the taxonomy" + # The bootstrap keys on the INPUT, never the event name. It used to test + # GITHUB_EVENT_NAME = workflow_dispatch — correct while an operator's manual + # dispatch was the only dispatch there was, and wrong from #209 on, when the + # trigger job made EVERY event-woken sweep a workflow_dispatch run: the + # bootstrap=no input became inert by construction, and every board event + # re-upserted the taxonomy (ceremony#215 — runs 459/523, then venue drill + # runs 16/17, which bootstrapped on a delivered "no" and caught this). + if [ "${BOOTSTRAP:-no}" = yes ]; then + log "bootstrap=yes: bootstrapping the taxonomy" bootstrap_labels fi diff --git a/changelog.d/215.md b/changelog.d/215.md index 9411adf..160ee13 100644 --- a/changelog.d/215.md +++ b/changelog.d/215.md @@ -31,3 +31,17 @@ - The same test drives the four value paths — schedule-empty, `no`, `yes`, invalid — through the shipped expressions into the action's real validator (#215). + +- The taxonomy bootstrap keys on the `BOOTSTRAP` input, never the event name. + It tested `GITHUB_EVENT_NAME = workflow_dispatch` — correct while an + operator's manual dispatch was the only dispatch there was, inert-by- + construction from #209 on, when every machine wake became a dispatch + event (#215). + +- The venue drill caught that: with the bridge delivering `no` perfectly, + drill runs 16/17 still bootstrapped, because the script never read the + input the whole chain existed to deliver (#215). + +- `test/labels-reconcile.test.sh` pins the regression pair exactly: a + `workflow_dispatch` event with `BOOTSTRAP=no` (or unset) creates and + deletes nothing; only `BOOTSTRAP=yes` bootstraps (#215). diff --git a/test/labels-reconcile.test.sh b/test/labels-reconcile.test.sh index d9e9a9a..696fb9f 100755 --- a/test/labels-reconcile.test.sh +++ b/test/labels-reconcile.test.sh @@ -1148,17 +1148,18 @@ EOF chmod +x "$EXEC/stub/gh" 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_env() { # $1 = event name, $2 = BOOTSTRAP value ("" = unset) : >"$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" \ + ${2:+BOOTSTRAP="$2"} \ bash actions/labels-reconcile/labels-reconcile.sh } exec_rc=0 -exec_out="$(exec_env workflow_dispatch 2>&1)" || exec_rc=$? -expect "an executed dispatch with all six absent completes green" 0 "$exec_rc" +exec_out="$(exec_env workflow_dispatch yes 2>&1)" || exec_rc=$? +expect "an executed bootstrap=yes with all six absent completes green" 0 "$exec_rc" expect "...reaching the end of the sweep" \ yes "$(grep -q 'reconciled.' <<<"$exec_out" && echo yes || echo no)" expect "...having attempted all six deletions" \ @@ -1167,12 +1168,18 @@ expect "...and created the full taxonomy" \ "$(core_label_rows | cut -d'|' -f1)" \ "$(sed -n 's/^create //p' "$EXEC/record")" -# -- bootstrap is dispatch-only, deletes included: the cron and -# pull_request_target paths touch no label -for ev in schedule pull_request_target; do +# -- the bootstrap keys on the INPUT, never the event (ceremony#215): from +# #209 on, EVERY machine wake is a workflow_dispatch event, so an event +# gate made bootstrap=no inert — runs 459/523 and venue drill runs 16/17 +# bootstrapped on a delivered "no". The regression case is exactly that +# pair: dispatch event, no. +for pair in "workflow_dispatch:no" "workflow_dispatch:" "schedule:no" "schedule:" "pull_request_target:"; do + ev="${pair%%:*}"; bs="${pair#*:}" ev_rc=0 - exec_env "$ev" >/dev/null 2>&1 || ev_rc=$? - expect "the $ev path completes green" 0 "$ev_rc" + exec_env "$ev" "$bs" >/dev/null 2>&1 || ev_rc=$? + expect "the $ev event with BOOTSTRAP='${bs:-unset}' completes green" 0 "$ev_rc" + expect "...and creates nothing" \ + no "$(grep -q '^create ' "$EXEC/record" && echo yes || echo no)" expect "...and deletes nothing" \ no "$(grep -q '^delete ' "$EXEC/record" && echo yes || echo no)" done From 7d387dd93699b9336cddf0a4d51e96cf6bdcfb5d Mon Sep 17 00:00:00 2001 From: clad2 Date: Wed, 5 Aug 2026 21:35:55 +0000 Subject: [PATCH 5/6] test(labels): drop the now-dead event-name assignment SC2034 flagged The script stopped reading GITHUB_EVENT_NAME with the input gate; the previous commit pushed with this red because I trusted an && echo that printed nothing. Refs #215 --- test/labels-reconcile.test.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/labels-reconcile.test.sh b/test/labels-reconcile.test.sh index 696fb9f..6ac2269 100755 --- a/test/labels-reconcile.test.sh +++ b/test/labels-reconcile.test.sh @@ -1548,7 +1548,9 @@ expect "...and does print reconciled." \ # exited 0 (@codex-reviewer-andresmgsl, #192 review). stale_fail_probe() { ( - GITHUB_EVENT_NAME=schedule + # No event name: the script stopped reading GITHUB_EVENT_NAME when the + # bootstrap moved to the BOOTSTRAP input (#215); default no is the + # non-bootstrap sweep this probe simulates. REPO=owner/repo LABELS_CONF=.github/labels.conf CEREMONY_FORGE=github From 960e581f91f2e5108584f5a59ba6958f413df4f8 Mon Sep 17 00:00:00 2001 From: clad2 Date: Wed, 5 Aug 2026 21:39:45 +0000 Subject: [PATCH 6/6] test(labels): remove the remaining dead event-name assignments All three schedule-simulation probes carried the assignment the script no longer reads. This time the head was verified clean BEFORE the push, not after. Refs #215 --- test/labels-reconcile.test.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/labels-reconcile.test.sh b/test/labels-reconcile.test.sh index 6ac2269..4874afb 100755 --- a/test/labels-reconcile.test.sh +++ b/test/labels-reconcile.test.sh @@ -883,7 +883,6 @@ expect "attention diagnosis caused no PR mutation" "$attention_mutations_before" # -- the sweep wiring observes the existing per-PR skip without writing ------- 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 @@ -942,7 +941,6 @@ expect "exactly the blind PRs match the counted shape whole-line — no more, no # shape, where the probes could not reach the per-PR path at all. unrequested_main_probe() { # $1 = read | denied, the head-commit read's outcome ( - GITHUB_EVENT_NAME=schedule REPO=owner/repo LABELS_CONF=.github/labels.conf # main() preflights the forge before it reads anything, so a probe that @@ -1478,7 +1476,6 @@ expect "...and a malformed panel= line in that same file is refused, not passed" # --------------------------------------------------------------------------- write_fail_probe() { # $1 = ok | fail — whether the label edit write succeeds ( - GITHUB_EVENT_NAME=schedule REPO=owner/repo LABELS_CONF=.github/labels.conf CEREMONY_FORGE=github