Add test/labels-triggers.test.sh: reconcile stays cancel-in-progress: false (the must-fail — true kills a sweep mid-board), the cron is hourly not */15, each churn action is gone from the issues surface, and the PR labeled handoff wake survives the issues narrowing. Update the #137/#144 parity block in labels.test.sh to the narrowed [opened, closed] contract and replace its fragile inline pull_request_target scan (the #199 prose comments name the trigger keys) with the anchored event_types reader. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
12887cc9de
commit
f8ad0b34c7
2 changed files with 108 additions and 19 deletions
86
test/labels-triggers.test.sh
Normal file
86
test/labels-triggers.test.sh
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# The labels TRIGGER SURFACE is a cost lever (#199): a full-board sweep is
|
||||||
|
# billed a 1-minute minimum every time a trigger fires, so how OFTEN it fires
|
||||||
|
# is what exhausted the fleet's shared Actions allotment. These assertions
|
||||||
|
# pin the reductions #199 made and the guard it must not trade away — none of
|
||||||
|
# them touch the reconciler's LOGIC, which its own fixtures cover.
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=test/harness.sh
|
||||||
|
source "$ROOT/test/harness.sh"
|
||||||
|
|
||||||
|
REUSABLE="$ROOT/.github/workflows/labels.yml"
|
||||||
|
SELF="$ROOT/.github/workflows/self-labels.yml"
|
||||||
|
STUB="$ROOT/docs/CONSUMERS.md" # the published caller stub, a fenced yaml block
|
||||||
|
|
||||||
|
# The `cancel-in-progress:` value of a named top-level job, read from the first
|
||||||
|
# such line inside that job's block. Job keys sit at two-space indent.
|
||||||
|
job_cancel_in_progress() { # $1 = file, $2 = job name
|
||||||
|
awk -v job="^ $2:\$" '
|
||||||
|
$0 ~ job { f = 1; next }
|
||||||
|
f && /^ [a-z]/ { exit } # next job — stop before leaking into it
|
||||||
|
f && /cancel-in-progress:/ { sub(/.*cancel-in-progress:[[:space:]]*/, ""); print; exit }
|
||||||
|
' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The `types:` list of a trigger key (issues:, pull_request_target:), read from
|
||||||
|
# the first `types:` line after the bare key. The key is bare (nothing after
|
||||||
|
# the colon) so it never collides with `issues: write` in the permissions block.
|
||||||
|
trigger_types() { # $1 = file, $2 = trigger key
|
||||||
|
awk -v key="^ $2:\$" '
|
||||||
|
$0 ~ key { f = 1; next }
|
||||||
|
f && /^ types:/ { sub(/^ types:[[:space:]]*/, ""); print; exit }
|
||||||
|
f && /^ [a-z]/ { exit }
|
||||||
|
' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- the guard the cost fix must never trade away (#199 test plan must-fail) --
|
||||||
|
# cancel-in-progress: true on reconcile kills a sweep mid-board, the exact race
|
||||||
|
# the shared concurrency group exists to prevent. It WOULD cut run count — by
|
||||||
|
# trading correctness for minutes — so it stays false, forever.
|
||||||
|
check "reconcile serializes, never cancels mid-board" 0 "false" \
|
||||||
|
job_cancel_in_progress "$REUSABLE" reconcile
|
||||||
|
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
|
||||||
|
check "reconcile is never cancel-in-progress: true" 1 "" \
|
||||||
|
bash -c 'job_cancel_in_progress() {
|
||||||
|
awk -v job="^ reconcile:\$" "\$0 ~ job{f=1;next} f&&/^ [a-z]/{exit} f&&/cancel-in-progress:/{sub(/.*cancel-in-progress:[[:space:]]*/,\"\");print;exit}" "$1"
|
||||||
|
}; [ "$(job_cancel_in_progress "$1")" = true ]' _ "$REUSABLE"
|
||||||
|
# scope MAY cancel — it is per-PR and additive, so a superseded run is waste,
|
||||||
|
# not a lost sweep. This asserts the must-fail above is scoped to reconcile.
|
||||||
|
check "scope stays cancel-in-progress: true (per-PR, additive)" 0 "true" \
|
||||||
|
job_cancel_in_progress "$REUSABLE" scope
|
||||||
|
|
||||||
|
# ---- the cron is a backstop, relaxed to hourly (#199 candidate 1) -----------
|
||||||
|
# Scope the */15 assertion to the cron LINE — the prose comments cite */15 by
|
||||||
|
# name to explain the change, and must not re-red their own documentation.
|
||||||
|
check "self caller cron is hourly" 0 '0 * * * *' grep -F 'cron:' "$SELF"
|
||||||
|
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
|
||||||
|
check "self caller cron line no longer fires */15" 1 "" \
|
||||||
|
bash -c 'grep -F "cron:" "$1" | grep -qF "*/15"' _ "$SELF"
|
||||||
|
check "stub cron is hourly" 0 '0 * * * *' grep -F 'cron:' "$STUB"
|
||||||
|
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
|
||||||
|
check "stub cron line no longer fires */15" 1 "" \
|
||||||
|
bash -c 'grep -F "cron:" "$1" | grep -qF "*/15"' _ "$STUB"
|
||||||
|
|
||||||
|
# ---- issues: is narrowed to the two promptness-critical actions (#199) -------
|
||||||
|
# opened → mint→needs-triage; closed → blocker-closes→ready self-heal. The
|
||||||
|
# churn actions must not reappear on the issues surface without a fresh why.
|
||||||
|
# (labels.test.sh owns the exact-list and caller<->stub parity assertions; here
|
||||||
|
# we name each dropped action so its return produces a #199-specific failure.)
|
||||||
|
for churn in labeled unlabeled assigned unassigned edited reopened; do
|
||||||
|
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
|
||||||
|
check "self caller issues surface drops '$churn'" 1 "" \
|
||||||
|
bash -c 'trigger_types() {
|
||||||
|
awk -v key="^ issues:\$" "\$0 ~ key{f=1;next} f&&/^ types:/{sub(/^ types:[[:space:]]*/,\"\");print;exit} f&&/^ [a-z]/{exit}" "$1"
|
||||||
|
}; trigger_types "$1" | grep -qw "$2"' _ "$SELF" "$churn"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---- the PR handoff wake is NOT collateral of the issues narrowing ----------
|
||||||
|
# The handoff (state:needs-human, confirmed by the caller's labeled event) rides
|
||||||
|
# pull_request_target, not issues. A future edit that strips it there re-reds.
|
||||||
|
check "pull_request_target keeps the labeled handoff wake" 0 "labeled" \
|
||||||
|
trigger_types "$SELF" pull_request_target
|
||||||
|
|
||||||
|
summary
|
||||||
|
|
@ -69,15 +69,15 @@ check "LABELS.md enumerates no repo's scope labels" 1 "0" \
|
||||||
# same lists. review_requested/review_request_removed are the wake that
|
# same lists. review_requested/review_request_removed are the wake that
|
||||||
# clears blocker:unrequested — the label sat false for as long as a quiet
|
# 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
|
# repo stayed quiet because the one event that falsifies it was never
|
||||||
# listed (#137). edited/reopened are the wakes for the two events that
|
# listed (#137). The issues list narrowed to [opened, closed] (#199): those
|
||||||
# falsify issue labels silently — an edited body rewrites the `Blocked by
|
# two carry reconcile behavior the hourly cron cannot wait one cadence for —
|
||||||
# #N` declaration the reconcile sweep parses, and a reopened issue
|
# opened drives mint→needs-triage, closed drives the blocker-closes→ready
|
||||||
# re-enters the queue wearing labels derived at close; PR #32 widened the
|
# self-heal — while the churn actions (labeled/unlabeled/assigned/unassigned/
|
||||||
# caller by both and the stub never followed (#144). The stub is prose, so
|
# edited/reopened) came off. The stub is prose, so nothing but these rows
|
||||||
# nothing but these rows keeps the lists from drifting: a type in one file
|
# keeps the lists from drifting: a type in one file only is a wake that fires
|
||||||
# only is a wake that fires at home and nowhere in the fleet, or the
|
# at home and nowhere in the fleet, or the reverse — the drift #144 caught.
|
||||||
# reverse. The NF guard keeps `issues: write` under permissions: from
|
# The NF guard keeps `issues: write` under permissions: from matching the
|
||||||
# matching the issues: trigger key.
|
# issues: trigger key.
|
||||||
event_types() { # $1 = file, $2 = trigger key → that trigger's types line, unindented
|
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"
|
awk -v key="$2:" '$1 == key && NF == 1 {f=1; next} f && /types: /{sub(/^ */,""); print; exit}' "$1"
|
||||||
}
|
}
|
||||||
|
|
@ -88,17 +88,20 @@ types_in_sync() { # $1 = trigger key, $2 = caller, $3 = stub → 0 when both lis
|
||||||
}
|
}
|
||||||
CALLER="$ROOT/.github/workflows/self-labels.yml"
|
CALLER="$ROOT/.github/workflows/self-labels.yml"
|
||||||
STUB="$ROOT/docs/CONSUMERS.md"
|
STUB="$ROOT/docs/CONSUMERS.md"
|
||||||
|
# event_types anchors on the bare trigger key (NF == 1), so it reads the real
|
||||||
|
# types line even though the #199 comments name pull_request_target: and
|
||||||
|
# issues: in prose above the keys — an inline /pull_request_target:/ scan would
|
||||||
|
# latch onto the first mention and read the wrong list.
|
||||||
|
pr_has_both_review_wakes() {
|
||||||
|
event_types "$CALLER" pull_request_target | grep -F review_requested | grep -qF review_request_removed
|
||||||
|
}
|
||||||
check "caller and stub pull_request_target lists are identical" 0 "" \
|
check "caller and stub pull_request_target lists are identical" 0 "" \
|
||||||
types_in_sync pull_request_target "$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 "" pr_has_both_review_wakes
|
||||||
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 "" \
|
check "caller and stub issues lists are identical" 0 "" \
|
||||||
types_in_sync issues "$CALLER" "$STUB"
|
types_in_sync issues "$CALLER" "$STUB"
|
||||||
check "the caller still lists all eight issue types" 0 \
|
check "the caller lists exactly the two promptness-critical issue types" 0 \
|
||||||
"types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]" \
|
"types: [opened, closed]" event_types "$CALLER" issues
|
||||||
event_types "$CALLER" issues
|
|
||||||
# the failing cases: drop a type from either file, or reorder one list only,
|
# the failing cases: drop a type from either file, or reorder one list only,
|
||||||
# and the identity rows above go 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"
|
mut_caller="$TMP/mut-caller.yml" mut_stub="$TMP/mut-stub.md"
|
||||||
|
|
@ -112,13 +115,13 @@ sed 's/review_requested, review_request_removed/review_request_removed, review_r
|
||||||
"$STUB" >"$mut_stub"
|
"$STUB" >"$mut_stub"
|
||||||
check "a reorder in one list only goes red" 1 "" \
|
check "a reorder in one list only goes red" 1 "" \
|
||||||
types_in_sync pull_request_target "$CALLER" "$mut_stub"
|
types_in_sync pull_request_target "$CALLER" "$mut_stub"
|
||||||
sed 's/, edited//' "$CALLER" >"$mut_caller"
|
sed 's/, closed//' "$CALLER" >"$mut_caller"
|
||||||
check "an issue type dropped from the caller goes red" 1 "" \
|
check "an issue type dropped from the caller goes red" 1 "" \
|
||||||
types_in_sync issues "$mut_caller" "$STUB"
|
types_in_sync issues "$mut_caller" "$STUB"
|
||||||
sed 's/, edited//' "$STUB" >"$mut_stub"
|
sed 's/, closed//' "$STUB" >"$mut_stub"
|
||||||
check "an issue type dropped from the stub goes red" 1 "" \
|
check "an issue type dropped from the stub goes red" 1 "" \
|
||||||
types_in_sync issues "$CALLER" "$mut_stub"
|
types_in_sync issues "$CALLER" "$mut_stub"
|
||||||
sed 's/closed, reopened/reopened, closed/' "$STUB" >"$mut_stub"
|
sed 's/opened, closed/closed, opened/' "$STUB" >"$mut_stub"
|
||||||
check "an issue-list reorder in one file only goes red" 1 "" \
|
check "an issue-list reorder in one file only goes red" 1 "" \
|
||||||
types_in_sync issues "$CALLER" "$mut_stub"
|
types_in_sync issues "$CALLER" "$mut_stub"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue