Merge pull request #96 from codex-bot-andresmgsl/build/95-labels-blind-sweep-warning
fix: surface wholly blind label sweeps
This commit is contained in:
commit
5af1538463
5 changed files with 79 additions and 6 deletions
2
.github/workflows/self-labels.yml
vendored
2
.github/workflows/self-labels.yml
vendored
|
|
@ -13,6 +13,8 @@ on:
|
|||
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled]
|
||||
permissions:
|
||||
contents: read
|
||||
checks: read # mergeability/check-rollup read for PR state
|
||||
statuses: read # commit-status rollup read for PR state
|
||||
issues: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ so entries say what changed, cite the issue, and stop.
|
|||
|
||||
## Unreleased
|
||||
|
||||
- `labels-reconcile` — grant callers private-repo check reads and warn when an entire PR sweep is blind (#95).
|
||||
- `labels-reconcile` — the bootstrap now retires the six GitHub defaults `LABELS.md` publishes as deleted, tolerating both an already-absent label and a refused delete (#93).
|
||||
- `issueflow-reconcile` — a triage-authored issue arrival stands down with exit 0 instead of killing the run before the sweep (#91).
|
||||
- FLEET.md — the assignee's `attention` wake: one role-independent trigger ahead of every per-role list, one acked session per demand; a spec on paper until `duty.sh` polls it (#86).
|
||||
|
|
|
|||
|
|
@ -60,6 +60,12 @@ run() { # every mutation goes through here — DRY_RUN=1 logs instead of doing
|
|||
if [ -n "${DRY_RUN:-}" ]; then log "DRY_RUN: $*"; else "$@"; fi
|
||||
}
|
||||
|
||||
blind_sweep_warning() { # $1 = unreadable PRs, $2 = all open PRs
|
||||
if [ "$2" -gt 0 ] && [ "$1" -eq "$2" ]; then
|
||||
echo "::warning::labels: every open PR was unreadable; grant checks: read and statuses: read in the caller (private repos do not imply them)"
|
||||
fi
|
||||
}
|
||||
|
||||
load_config() { # $1 = consumer labels.conf; panel is mandatory, scopes optional
|
||||
local conf="$1" line panel_seen=false
|
||||
[ -f "$conf" ] || {
|
||||
|
|
@ -592,9 +598,13 @@ main() {
|
|||
REPO_LABELS="$(gh label list -R "$REPO" --limit 200 --json name --jq '.[].name' 2>/dev/null || echo "")"
|
||||
[ -z "$REPO_LABELS" ] && log "WARNING: could not read the label set — applying labels unfiltered"
|
||||
|
||||
local n
|
||||
for n in $(gh pr list -R "$REPO" --state open --limit 100 --json number --jq '.[].number'); do
|
||||
(
|
||||
local n output status total=0 unreadable=0
|
||||
while IFS= read -r n; do
|
||||
[ -n "$n" ] || continue
|
||||
total=$((total + 1))
|
||||
status=0
|
||||
output="$(
|
||||
(
|
||||
PR_JSON="$(gh api "repos/$REPO/pulls/$n")"
|
||||
DRAFT="$(jq -r '.draft' <<<"$PR_JSON")"
|
||||
AUTHOR="$(jq -r '.user.login' <<<"$PR_JSON")"
|
||||
|
|
@ -623,8 +633,16 @@ main() {
|
|||
exit 0
|
||||
fi
|
||||
reconcile_pr "$n"
|
||||
) || log "#$n: reconcile failed — continuing with the remaining PRs"
|
||||
done
|
||||
) 2>&1
|
||||
)" || status=$?
|
||||
[ -n "$output" ] && printf '%s\n' "$output"
|
||||
if grep -qxF "labels: #$n: could not read mergeability/checks — left alone this pass" <<<"$output"; then
|
||||
unreadable=$((unreadable + 1))
|
||||
elif [ "$status" -ne 0 ]; then
|
||||
log "#$n: reconcile failed — continuing with the remaining PRs"
|
||||
fi
|
||||
done < <(gh pr list -R "$REPO" --state open --limit 100 --json number --jq '.[].number')
|
||||
blind_sweep_warning "$unreadable" "$total"
|
||||
log "reconciled."
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -256,6 +256,8 @@ on:
|
|||
types: [opened, labeled, unlabeled, assigned, unassigned, closed]
|
||||
permissions:
|
||||
contents: read
|
||||
checks: read # mergeability/check-rollup read for PR state
|
||||
statuses: read # commit-status rollup read for PR state
|
||||
issues: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
|
|
@ -263,6 +265,11 @@ jobs:
|
|||
uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag>
|
||||
```
|
||||
|
||||
Naming any permission sets every unnamed permission to `none`. Public
|
||||
repositories allow check data to be read regardless, but a private consumer
|
||||
needs both explicit reads above; without them the failure appears as an empty
|
||||
`state:*` axis on the board rather than a red workflow run.
|
||||
|
||||
The `issues:` trigger is **unreleased** and is not in `0.1.0`. A consumer
|
||||
pinned to `0.1.0` omits it. Add it only when bumping every ceremony reference
|
||||
to the first tag carrying ceremony#32; never mix refs to adopt it early.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,18 @@ rev() { # $1=login $2=state $3=commit $4=body $5=submitted_at → one review obj
|
|||
|
||||
reviews() { jq -s '.' <<<"$*"; } # collect review objects into an array
|
||||
|
||||
# -- a sweep-wide read failure is visible without changing any PR ------------
|
||||
warning="$(blind_sweep_warning 3 3)"
|
||||
expect "a wholly blind sweep warns" \
|
||||
"::warning::labels: every open PR was unreadable; grant checks: read and statuses: read in the caller (private repos do not imply them)" \
|
||||
"$warning"
|
||||
expect "the blind warning names checks: read" named \
|
||||
"$(grep -qF "checks: read" <<<"$warning" && echo named || echo missing)"
|
||||
expect "the blind warning names statuses: read" named \
|
||||
"$(grep -qF "statuses: read" <<<"$warning" && echo named || echo missing)"
|
||||
expect "a partially blind sweep does not warn" "" "$(blind_sweep_warning 1 3)"
|
||||
expect "a sweep with no open PRs does not warn" "" "$(blind_sweep_warning 0 0)"
|
||||
|
||||
# -- drafts are building, whoever is requested --------------------------------
|
||||
DRAFT=true HEAD_SHA=head1 REQUESTED="" REVIEWS_JSON='[]'
|
||||
expect "draft PR is building" state:building "$(decide_state)"
|
||||
|
|
@ -592,6 +604,40 @@ expect "exactly one nudge across both sweeps" \
|
|||
expect "no label edit across both sweeps names the ruling flag" \
|
||||
no "$(grep -q 'needs-ruling' "$RTMP/edits" 2>/dev/null && echo yes || echo no)"
|
||||
|
||||
# -- 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
|
||||
gh() {
|
||||
if [ "$1" = label ] && [ "$2" = list ]; then
|
||||
printf 'state:building\nstate:addressing\n'
|
||||
elif [ "$1" = pr ] && [ "$2" = list ]; then
|
||||
printf '101\n102\n'
|
||||
elif [ "$1" = pr ] && [ "$2" = view ]; then
|
||||
printf '{}\n'
|
||||
elif [ "$1" = api ] && [[ "$*" = *"/reviews"* ]]; then
|
||||
return 0
|
||||
elif [ "$1" = api ]; then
|
||||
jq -n --arg n "${*: -1}" \
|
||||
'{draft:false,user:{login:"author"},head:{sha:"head"},labels:[],requested_reviewers:[],created_at:"2026-07-23T00:00:00Z"}'
|
||||
elif [ "$1" = issue ] && [ "$2" = edit ]; then
|
||||
printf 'MUTATION: %s\n' "$*"
|
||||
fi
|
||||
}
|
||||
main
|
||||
)
|
||||
}
|
||||
|
||||
blind_main="$(blind_main_probe)"
|
||||
expect "a wholly blind main sweep emits one actionable annotation" 1 \
|
||||
"$(grep -c '^::warning::.*checks: read.*statuses: read' <<<"$blind_main")"
|
||||
expect "a wholly blind main sweep leaves every PR untouched" no \
|
||||
"$(grep -q '^MUTATION:' <<<"$blind_main" && echo yes || echo no)"
|
||||
expect "the existing per-PR skip still runs for every blind PR" 2 \
|
||||
"$(grep -c 'could not read mergeability/checks — left alone this pass' <<<"$blind_main")"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# bootstrap_labels retires the GitHub defaults (#93). LABELS.md published
|
||||
# them as deleted at bootstrap; nothing deleted them — incubator's first
|
||||
|
|
@ -754,6 +800,5 @@ for ev in schedule pull_request_target; do
|
|||
expect "...and deletes nothing" \
|
||||
no "$(grep -q '^delete ' "$EXEC/record" && echo yes || echo no)"
|
||||
done
|
||||
|
||||
printf 'labels-reconcile tests: %d passed, %d failed\n' "$pass" "$fail"
|
||||
[ "$fail" -eq 0 ]
|
||||
|
|
|
|||
Loading…
Reference in a new issue