diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e109d..34a4612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ so entries say what changed, cite the issue, and stop. - `labels-reconcile` — a degraded mergeability/checks read now logs gh's actual stderr (collapsed, bounded) beside the byte-identical counted line, and the blind-sweep warning leads with the observed reason instead of asserting the permissions cause (#101). - `LABELS.md` — drop the vendored scope-table enumeration; the per-repo set lives in `.github/labels.conf` and the repo's own CONTRIBUTING (#104). - Changelog publication — count entries instead of bytes, refuse dangling grouped headings, and seed grouped re-arms with Added/Changed/Fixed (#98). +- `labels-reconcile` — warn once per sweep when a repository lacks labels declared by the pinned core taxonomy (#105). - `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). diff --git a/LABELS.md b/LABELS.md index 70f586d..fc5830b 100644 --- a/LABELS.md +++ b/LABELS.md @@ -188,7 +188,8 @@ on a PR would say the same thing twice and drift. The labels workflow (issue #10) recomputes PR state statelessly on PR events plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on -manual dispatch. The same workflow reconciles issue-flow labels on issue -events and during the scheduled sweep. Default GitHub labels (`duplicate`, `invalid`, -`question`, `wontfix`, `help wanted`, `good first issue`) are deleted at -bootstrap — a `question` is a discussion, not an issue. +manual dispatch. The sweep warns when the core taxonomy declares a label the +repository lacks. The same workflow reconciles issue-flow labels on issue +events and during the scheduled sweep. Default GitHub labels (`duplicate`, +`invalid`, `question`, `wontfix`, `help wanted`, `good first issue`) are +deleted at bootstrap — a `question` is a discussion, not an issue. diff --git a/actions/labels-reconcile/labels-reconcile.sh b/actions/labels-reconcile/labels-reconcile.sh index 87c301e..d02e6aa 100755 --- a/actions/labels-reconcile/labels-reconcile.sh +++ b/actions/labels-reconcile/labels-reconcile.sh @@ -96,6 +96,21 @@ read_failure_reason() { # $1 = captured stderr → one bounded line; pure (#101) fi } +missing_core_labels_warning() { # $1 = declared rows, $2 = repo label names + local rows="$1" repo_labels="$2" row name missing="" + [ -n "$repo_labels" ] || return 0 + while IFS= read -r row; do + [ -n "$row" ] || continue + name="${row%%|*}" + if ! grep -qxF "$name" <<<"$repo_labels"; then + if [ -n "$missing" ]; then missing="$missing, $name"; else missing="$name"; fi + fi + done <<<"$rows" + if [ -n "$missing" ]; then + echo "::warning::labels: missing core label(s): $missing; bump the ceremony pin, then re-dispatch workflow_dispatch to bootstrap the taxonomy" + fi +} + load_config() { # $1 = consumer labels.conf; panel is mandatory, scopes optional local conf="$1" line panel_seen=false [ -f "$conf" ] || { @@ -627,6 +642,7 @@ main() { # add against it, because one unknown name fails the whole edit call. 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" + missing_core_labels_warning "$(core_label_rows)" "$REPO_LABELS" local n output status total=0 unreadable=0 sampled_reason="" while IFS= read -r n; do diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index efad3f8..2b285bc 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -319,7 +319,10 @@ rows remain consumer-owned because paths and surfaces differ by repository. After adding the caller and configuration, run `workflow_dispatch` once to bootstrap labels on a fresh repository. Scheduled and PR-triggered runs only -reconcile; they do not repeatedly upsert the taxonomy. +reconcile; they do not repeatedly upsert the taxonomy. When a ceremony pin +bump adds a core label, bump the pin first and then re-dispatch +`workflow_dispatch`; the scheduled sweep warns when the pinned taxonomy +declares a core label the repository lacks. ## Doctrine mirror diff --git a/test/labels-reconcile.test.sh b/test/labels-reconcile.test.sh index 8b1849f..4cb1c14 100755 --- a/test/labels-reconcile.test.sh +++ b/test/labels-reconcile.test.sh @@ -74,6 +74,24 @@ expect "...within the 304-byte bound" yes \ exact_reason="$(read_failure_reason "$(printf 'e%.0s' {1..300})")" expect "a 300-char reason passes through whole" 300 "${#exact_reason}" +# -- a missing core taxonomy row is visible without mutating labels ---------- +core_rows="$(core_label_rows)" +core_names="$(cut -d'|' -f1 <<<"$core_rows")" +expect "a complete core taxonomy does not warn" "" \ + "$(missing_core_labels_warning "$core_rows" "$core_names")" +expect "one missing core label is named exactly" \ + "::warning::labels: missing core label(s): attention; bump the ceremony pin, then re-dispatch workflow_dispatch to bootstrap the taxonomy" \ + "$(missing_core_labels_warning "$core_rows" "$(grep -vxF attention <<<"$core_names")")" +expect "three missing core labels are named in table order" \ + "::warning::labels: missing core label(s): offsite, needs-ruling, attention; bump the ceremony pin, then re-dispatch workflow_dispatch to bootstrap the taxonomy" \ + "$(missing_core_labels_warning "$core_rows" "$(grep -vxF -e offsite -e needs-ruling -e attention <<<"$core_names")")" +expect "an unreadable empty label set does not report the taxonomy missing" "" \ + "$(missing_core_labels_warning "$core_rows" "")" +expect "unrelated scope labels do not affect a complete core taxonomy" "" \ + "$(missing_core_labels_warning "$core_rows" "$core_names +scope:consumer-one +scope:consumer-two")" + # -- drafts are building, whoever is requested -------------------------------- DRAFT=true HEAD_SHA=head1 REQUESTED="" REVIEWS_JSON='[]' expect "draft PR is building" state:building "$(decide_state)" @@ -636,7 +654,7 @@ blind_main_probe() { LABELS_CONF=.github/labels.conf gh() { if [ "$1" = label ] && [ "$2" = list ]; then - printf 'state:building\nstate:addressing\n' + core_label_rows | cut -d'|' -f1 elif [ "$1" = pr ] && [ "$2" = list ]; then printf '101\n102\n' elif [ "$1" = pr ] && [ "$2" = view ]; then