forked from heavy-duty/ceremony
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:
parent
ae6b509772
commit
c0e796b6c9
5 changed files with 70 additions and 5 deletions
8
.github/workflows/labels.yml
vendored
8
.github/workflows/labels.yml
vendored
|
|
@ -30,10 +30,16 @@ jobs:
|
||||||
scope:
|
scope:
|
||||||
# Not on labeled/unlabeled: those events change no paths, so labeler has
|
# Not on labeled/unlabeled: those events change no paths, so labeler has
|
||||||
# nothing new to derive — and label churn is precisely what they are.
|
# 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: >-
|
if: >-
|
||||||
github.event_name == 'pull_request_target' &&
|
github.event_name == 'pull_request_target' &&
|
||||||
github.event.action != 'labeled' &&
|
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
|
runs-on: ubuntu-latest
|
||||||
concurrency:
|
concurrency:
|
||||||
group: labels-scope-${{ github.event.pull_request.number }}
|
group: labels-scope-${{ github.event.pull_request.number }}
|
||||||
|
|
|
||||||
6
.github/workflows/self-labels.yml
vendored
6
.github/workflows/self-labels.yml
vendored
|
|
@ -10,7 +10,11 @@ on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]
|
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]
|
||||||
pull_request_target:
|
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:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
checks: read # mergeability/check-rollup read for PR state
|
checks: read # mergeability/check-rollup read for PR state
|
||||||
|
|
|
||||||
10
changelog.d/137.md
Normal file
10
changelog.d/137.md
Normal 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).
|
||||||
|
|
@ -284,7 +284,10 @@ on:
|
||||||
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake
|
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake
|
||||||
workflow_dispatch: # bootstraps missing labels on a fresh repo
|
workflow_dispatch: # bootstraps missing labels on a fresh repo
|
||||||
pull_request_target:
|
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.
|
# Unreleased — not in 0.1.0; add only with the first tag carrying ceremony#32.
|
||||||
issues:
|
issues:
|
||||||
types: [opened, labeled, unlabeled, assigned, unassigned, closed]
|
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
|
`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
|
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.
|
out only the consumer's base branch and the pinned ceremony implementation.
|
||||||
The #52 ruling invariants ride exactly these triggers — the caller above is
|
The #52 ruling invariants ride exactly these triggers — but the caller above
|
||||||
unchanged since #18, so adopting them is a pin bump, not a stub edit.
|
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
|
`.github/labels.conf` has one mandatory panel setting, one mandatory
|
||||||
`triage-actors` setting, and then zero or more scope rows:
|
`triage-actors` setting, and then zero or more scope rows:
|
||||||
|
|
|
||||||
|
|
@ -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" \
|
check "LABELS.md enumerates no repo's scope labels" 1 "0" \
|
||||||
grep -c 'scope:[a-z0-9]' "$ROOT/LABELS.md"
|
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
|
summary
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue