forked from heavy-duty/ceremony
feat: diagnose malformed attention targets
This commit is contained in:
parent
5626a4ef1e
commit
e7750c0c8f
3 changed files with 121 additions and 0 deletions
|
|
@ -27,6 +27,9 @@ TRIAGE_ACTORS=()
|
|||
# The needs-ruling invariants (#52) — one implementation for both surfaces.
|
||||
# shellcheck source=lib/ruling.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/ruling.sh"
|
||||
# The attention target invariants (#232) — diagnosis only, both surfaces.
|
||||
# shellcheck source=lib/attention.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/attention.sh"
|
||||
# The guarded read and its reason line (#101, #247) — one implementation for
|
||||
# both surfaces.
|
||||
# shellcheck source=lib/read.sh
|
||||
|
|
@ -452,6 +455,7 @@ reconcile_issue() {
|
|||
local n="$1" decision refs cross_refs states age created assignees open_pr=false label owners
|
||||
local merged_ref_pr="" transition_marker="" transition_handled=false
|
||||
local unchecked="" remove_claimed=claimed
|
||||
local attention_active=true attention_suppression=""
|
||||
decision="$(queue_decision <<<"$ISSUE_LABELS")"
|
||||
case "$decision" in
|
||||
ADD_NEEDS_TRIAGE)
|
||||
|
|
@ -495,6 +499,7 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
|||
--remove-label "$remove_claimed" --add-label post-merge >/dev/null
|
||||
fi
|
||||
log "#$n: merged Refs PR -> post-merge; claim released"
|
||||
attention_active=false
|
||||
else
|
||||
created="$(jq -r '.created_at' <<<"$ISSUE_JSON")"
|
||||
guarded_read age last_issue_activity "$n" "$created" \
|
||||
|
|
@ -526,6 +531,7 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
|||
fi
|
||||
log "#$n: stale claim reclaimed -> ready" ;;
|
||||
esac
|
||||
[ "$decision" != FLAG_UNASSIGNED ] || attention_suppression=claimed-unassigned
|
||||
if has_issue_label offsite; then
|
||||
local timeline
|
||||
if timeline="$(offsite_timeline "$n")"; then
|
||||
|
|
@ -546,6 +552,7 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
|||
'This `post-merge` issue has an assignee or `attention`. The sweep will not undo hand-set intent; triage must clear the invalid composition or move the issue back into buildable queue state.'
|
||||
log "#$n: assigned or attention-bearing post-merge issue flagged"
|
||||
fi
|
||||
attention_suppression=post-merge-assigned
|
||||
elif has_issue_label blocked; then
|
||||
refs="$(blocked_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
|
||||
cross_refs="$(blocked_cross_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
|
||||
|
|
@ -574,6 +581,14 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
|||
fi
|
||||
fi
|
||||
|
||||
# The flag composes with every build queue state, but requires an assignee.
|
||||
# Existing post-merge/claimed diagnostics take precedence so one board bug
|
||||
# draws one comment (#232 D5); the shared helper still logs the suppression.
|
||||
if [ "$attention_active" = true ] && has_issue_label attention; then
|
||||
[ -n "${assignees:-}" ] || assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")"
|
||||
reconcile_attention "$n" issue "$assignees" "$attention_suppression"
|
||||
fi
|
||||
|
||||
# ---- the ruling invariants (#52), on any queue state ----
|
||||
# The flag composes with the queue labels (#50 D8), so this runs after the
|
||||
# queue branches rather than inside one of them. The FLAG_CONFLICT return
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ SELF_WORKFLOW="${SELF_WORKFLOW:-${GITHUB_WORKFLOW:-}}"
|
|||
# The needs-ruling invariants (#52) — one implementation for both surfaces.
|
||||
# shellcheck source=lib/ruling.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/ruling.sh"
|
||||
# The attention target invariants (#232) — diagnosis only, both surfaces.
|
||||
# shellcheck source=lib/attention.sh
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/attention.sh"
|
||||
# The guarded read and its reason line (#101) — one implementation for both
|
||||
# surfaces. read_failure_reason lived here until the issue surface needed the
|
||||
# identical rule (#247); a second copy of it is the failure lib/ruling.sh's
|
||||
|
|
@ -921,6 +924,12 @@ reconcile_pr() { # $1 = PR number; relies on the globals set from its fetch
|
|||
if has_label needs-ruling; then
|
||||
reconcile_ruling "$n" "$last_activity_epoch" "$NOW"
|
||||
fi
|
||||
|
||||
# `attention` belongs on the assigned issue that owns the claim, never on
|
||||
# a pull request (#232). Behind the label gate so ordinary PRs pay no read.
|
||||
if has_label attention; then
|
||||
reconcile_attention "$n" pr "$(jq '.assignees | length' <<<"$PR_JSON")" ""
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
|
|
|
|||
97
lib/attention.sh
Normal file
97
lib/attention.sh
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env bash
|
||||
# lib/attention.sh — the `attention` target invariants (#232, epic #229).
|
||||
#
|
||||
# Both reconcilers source this file. Pure decisions sit above the divider;
|
||||
# the impure orchestrator below reads the current label episode and comments
|
||||
# through the sourcing script's run()/log(). The machine diagnoses only: it
|
||||
# never sets, clears, retargets or assigns anything on the strength of these
|
||||
# checks (#229 D2).
|
||||
|
||||
ATTENTION_MARKER_PREFIX='<!-- ceremony:attention-malformed:'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure decisions. Facts in, verdict out. No gh, no clock.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
attention_target_decision() { # $1 pr|issue, $2 assignee count → MALFORMED_* | KEEP
|
||||
case "$1" in
|
||||
pr) echo MALFORMED_PR ;;
|
||||
issue)
|
||||
if [ "$2" -eq 0 ]; then echo MALFORMED_UNASSIGNED; else echo KEEP; fi ;;
|
||||
*) return 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
attention_comment_decision() { # $1 target verdict, $2 suppression → POST | SUPPRESS | KEEP
|
||||
case "$1" in
|
||||
KEEP) echo KEEP ;;
|
||||
MALFORMED_UNASSIGNED)
|
||||
if [ -n "$2" ]; then echo SUPPRESS; else echo POST; fi ;;
|
||||
MALFORMED_PR) echo POST ;;
|
||||
*) return 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
attention_newest_flag() { # labeled-event ISO-8601 timestamps on stdin → newest
|
||||
sort | tail -n1
|
||||
}
|
||||
|
||||
attention_episode_marker() { # $1 current episode's labeled timestamp
|
||||
printf '%s%s -->\n' "$ATTENTION_MARKER_PREFIX" "$1"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The impure orchestrator. Called only behind a has-attention gate.
|
||||
# Needs REPO; uses the caller's run() and log().
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
reconcile_attention() { # $1 item, $2 pr|issue, $3 assignees, $4 suppression
|
||||
local n="$1" surface="$2" assignees="$3" suppression="${4:-}"
|
||||
local target comment labeled_events labeled_at marker comments body
|
||||
: "${REPO:?reconcile_attention: REPO is required}"
|
||||
|
||||
target="$(attention_target_decision "$surface" "$assignees")"
|
||||
comment="$(attention_comment_decision "$target" "$suppression")"
|
||||
[ "$comment" != KEEP ] || return 0
|
||||
|
||||
if [ "$comment" = SUPPRESS ]; then
|
||||
log "#$n: malformed attention detected; comment suppressed by $suppression precedence"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! labeled_events="$(gh api --paginate "repos/$REPO/issues/$n/timeline" \
|
||||
--jq '.[] | select(.event == "labeled" and .label.name == "attention")
|
||||
| .created_at' 2>/dev/null)"; then
|
||||
log "#$n: attention timeline unreadable — no verdict invented this pass"
|
||||
return 0
|
||||
fi
|
||||
if [ -z "$labeled_events" ]; then
|
||||
log "#$n: attention flag has no visible labeled event — no verdict invented this pass"
|
||||
return 0
|
||||
fi
|
||||
labeled_at="$(attention_newest_flag <<<"$labeled_events")"
|
||||
marker="$(attention_episode_marker "$labeled_at")"
|
||||
|
||||
if ! comments="$(gh api --paginate "repos/$REPO/issues/$n/comments" \
|
||||
--jq '.[].body // ""' 2>/dev/null)"; then
|
||||
log "#$n: attention comments unreadable — no verdict invented this pass"
|
||||
return 0
|
||||
fi
|
||||
grep -qF "$marker" <<<"$comments" && return 0
|
||||
|
||||
case "$target" in
|
||||
MALFORMED_PR)
|
||||
body="$marker
|
||||
This pull request carries \`attention\`, but that label is issue-only. Put
|
||||
the label on the assigned issue that owns the claim. The sweep cannot infer
|
||||
which issue that is, so it reports the malformed target without removing or
|
||||
retargeting the label (heavy-duty/ceremony#232)." ;;
|
||||
MALFORMED_UNASSIGNED)
|
||||
body="$marker
|
||||
This issue carries \`attention\` but has no assignee to receive the demand.
|
||||
Assign the intended builder or remove the flag. The sweep reports the board
|
||||
bug without assigning anyone or changing the label (heavy-duty/ceremony#232)." ;;
|
||||
esac
|
||||
run gh issue comment "$n" -R "$REPO" --body "$body" >/dev/null
|
||||
log "#$n: malformed attention ($surface) — commented; no label or assignee changed"
|
||||
}
|
||||
Loading…
Reference in a new issue