fix: the CONSUMERS.md stub's issues: types match the caller's, parity-tested #150

Merged
claude-bot-andresmgsl merged 1 commit from build/144-consumers-issues-parity into main 2026-07-24 13:56:30 +00:00
3 changed files with 59 additions and 21 deletions

10
changelog.d/144.md Normal file
View file

@ -0,0 +1,10 @@
### Fixed
- The `docs/CONSUMERS.md` labels-caller stub lists the same `issues:` types
as ceremony's own caller — `edited` and `reopened` included — so a consumer
adopting the stub wakes when an issue body's `Blocked by #N` declaration is
edited, and when a closed issue re-enters the queue wearing labels derived
at close. The two lists drifted apart inside PR #32; a parity test now pins
them together, red if either file drops a type or the lists diverge.
Adopting the widened list is a stub edit riding the pin bump to the first
tag carrying this change (#144).

View file

@ -306,7 +306,10 @@ on:
# Available at 0.2.0 and later (the first tag carrying ceremony#32); a
# consumer pinned to 0.1.0 omits this block.
issues:
types: [opened, labeled, unlabeled, assigned, unassigned, closed]
# edited/reopened are unreleased — not in 0.2.0; add them with the pin
# bump to the first tag carrying ceremony#144, the same bump as the
# pull_request_target additions above.
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]
permissions:
contents: read
checks: read # mergeability/check-rollup read for PR state
@ -326,18 +329,23 @@ needs both explicit reads above; without them the failure appears as an empty
The `issues:` trigger is available at `0.2.0` and later — `0.2.0` is the
first tag carrying ceremony#32. A consumer pinned to `0.1.0` omits it. Adopt
it only by bumping every ceremony reference to `0.2.0` or later; never mix
refs to adopt it early.
refs to adopt it early. At `0.2.0` the type list stops at `closed`: `edited`
and `reopened` are newer, part of the pending stub edit below.
`pull_request_target` is intentional: fork PRs need the base repository's
token to write labels. The reusable workflow executes no PR code. It checks
out only the consumer's base branch and the pinned ceremony implementation.
The #52 ruling invariants ride exactly these triggers — but the caller above
is no longer the #18 shape, so adopting current triggers is a stub edit, not
a bare pin bump. The pending edit is `review_requested` and
`review_request_removed` (#137): the wake that clears `blocker:unrequested`
the moment the panel is asked, without which a quiet repo wears that flag
until the advisory cron. Make that edit with the pin bump to the first tag
carrying ceremony#137 — never before it and never through mixed refs.
a bare pin bump. The pending edits are `review_requested` and
`review_request_removed` on `pull_request_target:` (#137) — the wake that
clears `blocker:unrequested` the moment the panel is asked, without which a
quiet repo wears that flag until the advisory cron — and `edited` and
`reopened` on `issues:` (#144): the wakes for a body edit rewriting the
`Blocked by #N` declaration the reconcile sweep parses, and for a closed
issue re-entering the queue wearing labels derived when it closed. Make both
edits together, with the pin bump to the first tag carrying ceremony#137 and
ceremony#144 — never before it and never through mixed refs.
`.github/labels.conf` has one mandatory panel setting, one mandatory
`triage-actors` setting, and then zero or more scope rows:

View file

@ -65,41 +65,61 @@ check "PR author is recused from the required panel" 0 "one three" printf '%s\n'
check "LABELS.md enumerates no repo's scope labels" 1 "0" \
grep -c 'scope:[a-z0-9]' "$ROOT/LABELS.md"
# The caller's pull_request_target list and the CONSUMERS.md stub's must be
# the same list. review_requested/review_request_removed are the wake that
# The caller's trigger type lists and the CONSUMERS.md stub's must be the
# same lists. review_requested/review_request_removed are the wake that
# clears blocker:unrequested — the label sat false for as long as a quiet
# repo stayed quiet because the one event that falsifies it was never
# listed (#137). The stub is prose, so nothing but this row keeps the two
# lists from drifting: a type in one file only is a wake that fires at home
# and nowhere in the fleet, or the reverse.
pr_target_types() { # $1 = file → its pull_request_target types line, unindented
awk '/pull_request_target:/{f=1; next} f && /types: /{sub(/^ */,""); print; exit}' "$1"
# listed (#137). edited/reopened are the wakes for the two events that
# falsify issue labels silently — an edited body rewrites the `Blocked by
# #N` declaration the reconcile sweep parses, and a reopened issue
# re-enters the queue wearing labels derived at close; PR #32 widened the
# caller by both and the stub never followed (#144). The stub is prose, so
# nothing but these rows keeps the lists from drifting: a type in one file
# only is a wake that fires at home and nowhere in the fleet, or the
# reverse. The NF guard keeps `issues: write` under permissions: from
# matching the issues: trigger key.
event_types() { # $1 = file, $2 = trigger key → that trigger's types line, unindented
awk -v key="$2:" '$1 == key && NF == 1 {f=1; next} f && /types: /{sub(/^ */,""); print; exit}' "$1"
}
types_in_sync() { # $1 = caller, $2 = stub → 0 when both lists exist and match
types_in_sync() { # $1 = trigger key, $2 = caller, $3 = stub → 0 when both lists exist and match
local a b
a="$(pr_target_types "$1")" b="$(pr_target_types "$2")"
a="$(event_types "$2" "$1")" b="$(event_types "$3" "$1")"
[ -n "$a" ] && [ "$a" = "$b" ]
}
CALLER="$ROOT/.github/workflows/self-labels.yml"
STUB="$ROOT/docs/CONSUMERS.md"
check "caller and stub pull_request_target lists are identical" 0 "" \
types_in_sync "$CALLER" "$STUB"
types_in_sync pull_request_target "$CALLER" "$STUB"
# shellcheck disable=SC2016 # expansion belongs to the nested bash
check "the caller lists both review-request wakes" 0 "" bash -c \
'awk "/pull_request_target:/{f=1; next} f && /types: /{print; exit}" "$1" |
grep -F review_requested | grep -qF review_request_removed' _ "$CALLER"
check "caller and stub issues lists are identical" 0 "" \
types_in_sync issues "$CALLER" "$STUB"
check "the caller still lists all eight issue types" 0 \
"types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]" \
event_types "$CALLER" issues
# the failing cases: drop a type from either file, or reorder one list only,
# and the identity row above goes red — exercised here on mutated copies
# and the identity rows above go red — exercised here on mutated copies
mut_caller="$TMP/mut-caller.yml" mut_stub="$TMP/mut-stub.md"
sed 's/, review_request_removed//' "$CALLER" >"$mut_caller"
check "a type dropped from the caller goes red" 1 "" \
types_in_sync "$mut_caller" "$STUB"
types_in_sync pull_request_target "$mut_caller" "$STUB"
sed 's/, review_request_removed//' "$STUB" >"$mut_stub"
check "a type dropped from the stub goes red" 1 "" \
types_in_sync "$CALLER" "$mut_stub"
types_in_sync pull_request_target "$CALLER" "$mut_stub"
sed 's/review_requested, review_request_removed/review_request_removed, review_requested/' \
"$STUB" >"$mut_stub"
check "a reorder in one list only goes red" 1 "" \
types_in_sync "$CALLER" "$mut_stub"
types_in_sync pull_request_target "$CALLER" "$mut_stub"
sed 's/, edited//' "$CALLER" >"$mut_caller"
check "an issue type dropped from the caller goes red" 1 "" \
types_in_sync issues "$mut_caller" "$STUB"
sed 's/, edited//' "$STUB" >"$mut_stub"
check "an issue type dropped from the stub goes red" 1 "" \
types_in_sync issues "$CALLER" "$mut_stub"
sed 's/closed, reopened/reopened, closed/' "$STUB" >"$mut_stub"
check "an issue-list reorder in one file only goes red" 1 "" \
types_in_sync issues "$CALLER" "$mut_stub"
summary