fix: review_requested wakes the sweep — blocker:unrequested clears when the ask lands

The reconciler's rule was right and blind: the caller never listened on
review_requested/review_request_removed, so the one event that falsifies
(or restores) blocker:unrequested could not clear it, and a quiet repo
wore the red flag until the advisory cron (#137's timeline: 93 seconds,
cleared only by an unrelated PR's push).

- self-labels.yml + the CONSUMERS.md stub gain both types; the scope job
  skips them (no paths change; running labeler there widens #130's window)
- test/labels.test.sh: caller/stub parity row with mutation cases —
  dropped type either side, one-sided reorder, all red
- CONSUMERS.md no longer claims trigger adoption is a bare pin bump; the
  pending stub edit is named and rides the first tag carrying ceremony#137

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
claude-bot-andresmgsl 2026-07-24 12:50:09 +00:00
parent ae6b509772
commit c0e796b6c9
5 changed files with 70 additions and 5 deletions

View file

@ -30,10 +30,16 @@ jobs:
scope:
# Not on labeled/unlabeled: those events change no paths, so labeler has
# nothing new to derive — and label churn is precisely what they are.
# review_requested/review_request_removed likewise change no paths — they
# exist to wake reconcile (#137) — and running labeler on them widens
# exactly the window #130 documents, where a label written during a
# scope run is clobbered.
if: >-
github.event_name == 'pull_request_target' &&
github.event.action != 'labeled' &&
github.event.action != 'unlabeled'
github.event.action != 'unlabeled' &&
github.event.action != 'review_requested' &&
github.event.action != 'review_request_removed'
runs-on: ubuntu-latest
concurrency:
group: labels-scope-${{ github.event.pull_request.number }}

View file

@ -10,7 +10,11 @@ on:
issues:
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]
pull_request_target:
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled]
# review_requested/review_request_removed wake the sweep that clears (or
# restores) blocker:unrequested — without them the one event that makes
# the label false could not clear it, and a quiet repo wore the red flag
# until the advisory cron (#137).
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed]
permissions:
contents: read
checks: read # mergeability/check-rollup read for PR state

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

@ -0,0 +1,10 @@
### Fixed
- `blocker:unrequested` now clears the moment the panel is asked: the labels
caller (and the `docs/CONSUMERS.md` stub) listens on `review_requested` and
`review_request_removed`, so the one event that falsifies the label — or
makes it true again — wakes the reconcile sweep instead of waiting for an
unrelated push or the advisory cron. The `scope` job skips both events:
they change no paths, and running the labeler on them widens the #130
clobber window. Adopting the new triggers is a stub edit riding the pin
bump to the first tag carrying this change (#137).

View file

@ -284,7 +284,10 @@ on:
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake
workflow_dispatch: # bootstraps missing labels on a fresh repo
pull_request_target:
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled]
# review_requested/review_request_removed are unreleased — not in 0.2.0;
# add them with the pin bump to the first tag carrying ceremony#137. They
# wake the sweep that clears blocker:unrequested when the panel is asked.
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed]
# Unreleased — not in 0.1.0; add only with the first tag carrying ceremony#32.
issues:
types: [opened, labeled, unlabeled, assigned, unassigned, closed]
@ -311,8 +314,13 @@ to the first tag carrying ceremony#32; never mix refs to adopt it early.
`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 — the caller above is
unchanged since #18, so adopting them is a pin bump, not a stub edit.
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.
`.github/labels.conf` has one mandatory panel setting, one mandatory
`triage-actors` setting, and then zero or more scope rows:

View file

@ -65,4 +65,41 @@ 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
# 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"
}
types_in_sync() { # $1 = caller, $2 = stub → 0 when both lists exist and match
local a b
a="$(pr_target_types "$1")" b="$(pr_target_types "$2")"
[ -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"
# 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"
# 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
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"
sed 's/, review_request_removed//' "$STUB" >"$mut_stub"
check "a type dropped from the stub goes red" 1 "" \
types_in_sync "$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"
summary