Merge pull request #71 from codex-bot-andresmgsl/build/69-offsite-stale-flag-nudge
feat: nudge resolved offsite claims
This commit is contained in:
commit
553409cad4
4 changed files with 122 additions and 3 deletions
|
|
@ -12,6 +12,7 @@ so entries say what changed, cite the issue, and stop.
|
||||||
- `actions/runner-isolated` — a `pull_request`-triggered job may never run on a self-hosted runner (#58).
|
- `actions/runner-isolated` — a `pull_request`-triggered job may never run on a self-hosted runner (#58).
|
||||||
- The sweep's `needs-ruling` invariants, one implementation for both surfaces: the issue-side staleness exemption, the bare-flag check (comment-only, the label is never removed), and the 7-day nudge to the decider (#52).
|
- The sweep's `needs-ruling` invariants, one implementation for both surfaces: the issue-side staleness exemption, the bare-flag check (comment-only, the label is never removed), and the 7-day nudge to the decider (#52).
|
||||||
- `offsite` — protect claimed issues whose PR lives in another repository from the claim-reclaim clock (#68).
|
- `offsite` — protect claimed issues whose PR lives in another repository from the claim-reclaim clock (#68).
|
||||||
|
- `issueflow-reconcile` — nudge once when an `offsite` flag outlives every visible cross-referenced PR (#69).
|
||||||
|
|
||||||
## 0.1.0 — 2026-07-22
|
## 0.1.0 — 2026-07-22
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,9 @@ the cross-repo draft link, then clears it at handoff in the same comment that
|
||||||
reports whether that PR merged or closed. The machine reads the flag and
|
reports whether that PR merged or closed. The machine reads the flag and
|
||||||
never writes it. It stops only the claim-reclaim clock: missing assignees are
|
never writes it. It stops only the claim-reclaim clock: missing assignees are
|
||||||
still flagged, queue-label conflicts and missing queue state are still
|
still flagged, queue-label conflicts and missing queue state are still
|
||||||
repaired, and epic-completion and PR-side stale behavior are unchanged.
|
repaired, and epic-completion and PR-side stale behavior are unchanged. The
|
||||||
|
sweep tells the assignee once when every visible cross-referenced PR has
|
||||||
|
closed; it only tells, and never clears the flag or changes the claim.
|
||||||
|
|
||||||
## Scope — which surface? (PRs and issues, any number)
|
## Scope — which surface? (PRs and issues, any number)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,27 @@ epic_decision() { # $1 refs, $2 states
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offsite_cross_referenced_prs() { # timeline JSON on stdin -> owner/repo#N
|
||||||
|
jq -r '
|
||||||
|
.[]
|
||||||
|
| select(.event == "cross-referenced")
|
||||||
|
| .source.issue
|
||||||
|
| select(.pull_request != null)
|
||||||
|
| select(.repository.full_name != null and .number != null)
|
||||||
|
| "\(.repository.full_name)#\(.number)"
|
||||||
|
' | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
offsite_resolved_decision() { # PR states on stdin -> NUDGE | QUIET
|
||||||
|
local states
|
||||||
|
states="$(cat)"
|
||||||
|
if [ -n "$states" ] && ! grep -Eq '^(OPEN|UNKNOWN)$' <<<"$states"; then
|
||||||
|
echo NUDGE
|
||||||
|
else
|
||||||
|
echo QUIET
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# API edge. Marker comments make warnings and nudges idempotent across sweeps.
|
# API edge. Marker comments make warnings and nudges idempotent across sweeps.
|
||||||
ensure_comment() { # $1 issue, $2 marker, $3 message
|
ensure_comment() { # $1 issue, $2 marker, $3 message
|
||||||
local n="$1" marker="$2" message="$3"
|
local n="$1" marker="$2" message="$3"
|
||||||
|
|
@ -210,6 +231,21 @@ reference_states() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offsite_pr_states() {
|
||||||
|
local ref repo number state
|
||||||
|
while IFS= read -r ref; do
|
||||||
|
[ -n "$ref" ] || continue
|
||||||
|
repo="${ref%#*}"
|
||||||
|
number="${ref##*#}"
|
||||||
|
state="$(gh api "repos/$repo/pulls/$number" --jq '.state' 2>/dev/null || echo UNKNOWN)"
|
||||||
|
case "$state" in open) echo OPEN ;; closed) echo CLOSED ;; *) echo UNKNOWN ;; esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
offsite_timeline() { # unreadable timelines are deliberately silent
|
||||||
|
gh api --paginate "repos/$REPO/issues/$1/timeline" 2>/dev/null || return 1
|
||||||
|
}
|
||||||
|
|
||||||
last_issue_activity() {
|
last_issue_activity() {
|
||||||
local n="$1" created="$2" latest
|
local n="$1" created="$2" latest
|
||||||
latest="$({
|
latest="$({
|
||||||
|
|
@ -269,6 +305,18 @@ reconcile_issue() {
|
||||||
fi
|
fi
|
||||||
log "#$n: stale claim reclaimed -> ready" ;;
|
log "#$n: stale claim reclaimed -> ready" ;;
|
||||||
esac
|
esac
|
||||||
|
if has_issue_label offsite; then
|
||||||
|
local timeline
|
||||||
|
if timeline="$(offsite_timeline "$n")"; then
|
||||||
|
refs="$(offsite_cross_referenced_prs <<<"$timeline")"
|
||||||
|
states="$(offsite_pr_states <<<"$refs")"
|
||||||
|
if [ "$(offsite_resolved_decision <<<"$states")" = NUDGE ]; then
|
||||||
|
ensure_comment "$n" offsite-resolved \
|
||||||
|
"$(tr '\n' ' ' <<<"$refs" | sed 's/[[:space:]]*$//') is closed; this issue's \`offsite\` flag is still up. Clear it and close the issue, or say what is still outstanding. @$(jq -r '.assignees[0].login' <<<"$ISSUE_JSON")"
|
||||||
|
log "#$n: resolved offsite PRs nudged"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
elif has_issue_label blocked; then
|
elif has_issue_label blocked; then
|
||||||
refs="$(blocked_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
|
refs="$(blocked_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
|
||||||
cross_refs="$(blocked_cross_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
|
cross_refs="$(blocked_cross_references <<<"$(jq -r '.body // ""' <<<"$ISSUE_JSON")")"
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,21 @@ check "both quiet flags still produce one exemption verdict" 0 "EXEMPT" \
|
||||||
check "blocked does not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt <<<"blocked"
|
check "blocked does not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt <<<"blocked"
|
||||||
check "ready does not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt <<<"ready"
|
check "ready does not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt <<<"ready"
|
||||||
check "empty labels do not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt </dev/null
|
check "empty labels do not exempt a claimed issue" 0 "SWEEP" claim_clock_exempt </dev/null
|
||||||
|
check "one closed offsite PR nudges" 0 "NUDGE" offsite_resolved_decision <<<"CLOSED"
|
||||||
|
check "two closed offsite PRs nudge" 0 "NUDGE" offsite_resolved_decision <<< $'CLOSED\nCLOSED'
|
||||||
|
check "one open offsite PR keeps quiet" 0 "QUIET" offsite_resolved_decision <<< $'CLOSED\nOPEN'
|
||||||
|
check "no visible offsite PR keeps quiet" 0 "QUIET" offsite_resolved_decision </dev/null
|
||||||
|
check "an unreadable offsite PR keeps quiet" 0 "QUIET" offsite_resolved_decision <<< $'CLOSED\nUNKNOWN'
|
||||||
|
|
||||||
|
timeline='[
|
||||||
|
{"event":"cross-referenced","source":{"issue":{"number":112,"repository":{"full_name":"heavy-duty/rig"},"pull_request":{"url":"x"}}}},
|
||||||
|
{"event":"cross-referenced","source":{"issue":{"number":7,"repository":{"full_name":"heavy-duty/rig"}}}},
|
||||||
|
{"event":"mentioned","source":{"issue":{"number":9,"repository":{"full_name":"heavy-duty/box"},"pull_request":{"url":"x"}}}},
|
||||||
|
{"event":"assigned"}
|
||||||
|
]'
|
||||||
|
check "offsite timeline extracts only cross-referenced PRs" 0 "heavy-duty/rig#112" \
|
||||||
|
offsite_cross_referenced_prs <<<"$timeline"
|
||||||
|
check "empty offsite timeline extracts nothing" 0 "" offsite_cross_referenced_prs <<<"[]"
|
||||||
|
|
||||||
# Invariant 3: blocked declarations parse and release only when all close.
|
# Invariant 3: blocked declarations parse and release only when all close.
|
||||||
refs="$(blocked_references <<< $'Context #99. Blocked by #12 (first), #7 (second). Blocks #44.')"
|
refs="$(blocked_references <<< $'Context #99. Blocked by #12 (first), #7 (second). Blocks #44.')"
|
||||||
|
|
@ -193,6 +208,8 @@ issue_stub_gh() {
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
file="$TMP/$(printf '%s' "$endpoint" | tr '/' '_').json"
|
file="$TMP/$(printf '%s' "$endpoint" | tr '/' '_').json"
|
||||||
|
printf '%s\n' "$endpoint" >>"$TMP/api-calls"
|
||||||
|
[ ! -f "$file.error" ] || return 1
|
||||||
[ -f "$file" ] || { printf '[]\n'; return 0; }
|
[ -f "$file" ] || { printf '[]\n'; return 0; }
|
||||||
if [ -n "$jqexpr" ]; then jq -r "$jqexpr" "$file"; else cat "$file"; fi
|
if [ -n "$jqexpr" ]; then jq -r "$jqexpr" "$file"; else cat "$file"; fi
|
||||||
elif [ "$1" = issue ] && [ "$2" = comment ]; then
|
elif [ "$1" = issue ] && [ "$2" = comment ]; then
|
||||||
|
|
@ -274,10 +291,61 @@ check "an offsite claim with an open PR stays claimed" 1 "" \
|
||||||
offsite_both="$(issue_probe 28 $'claimed\noffsite\nneeds-ruling')"
|
offsite_both="$(issue_probe 28 $'claimed\noffsite\nneeds-ruling')"
|
||||||
check "offsite plus needs-ruling stays claimed" 1 "" \
|
check "offsite plus needs-ruling stays claimed" 1 "" \
|
||||||
grep -q 'reclaimed' <<<"$offsite_both"
|
grep -q 'reclaimed' <<<"$offsite_both"
|
||||||
|
|
||||||
|
# -- resolved offsite work nudges once and only from complete evidence -------
|
||||||
|
jq -n --arg at "$(iso_at $((INOW - 3600)))" \
|
||||||
|
'[{"event":"assigned","created_at":$at},
|
||||||
|
{"event":"cross-referenced","source":{"issue":{"number":112,"repository":{"full_name":"heavy-duty/rig"},"pull_request":{"url":"x"}}}}]' \
|
||||||
|
>"$(tfix 29)"
|
||||||
|
printf '{"state":"closed"}\n' >"$TMP/repos_heavy-duty_rig_pulls_112.json"
|
||||||
|
printf '[]\n' >"$(cfix 29)"
|
||||||
|
resolved="$(issue_probe 29 $'claimed\noffsite')"
|
||||||
|
check "a closed cross-referenced PR nudges and names the PR" 0 "" \
|
||||||
|
grep -q 'heavy-duty/rig#112 is closed' "$TMP/posted-29"
|
||||||
|
check "the resolved nudge leaves the claim untouched" 1 "" \
|
||||||
|
grep -q 'reclaimed' <<<"$resolved"
|
||||||
|
issue_probe 29 $'claimed\noffsite' >/dev/null
|
||||||
|
check "the resolved nudge is idempotent across sweeps" 0 "1" \
|
||||||
|
grep -cF '<!-- issueflow:offsite-resolved -->' "$TMP/posted-29"
|
||||||
|
|
||||||
|
jq -n --arg at "$(iso_at $((INOW - 3600)))" \
|
||||||
|
'[{"event":"assigned","created_at":$at},
|
||||||
|
{"event":"cross-referenced","source":{"issue":{"number":112,"repository":{"full_name":"heavy-duty/rig"},"pull_request":{"url":"x"}}}},
|
||||||
|
{"event":"cross-referenced","source":{"issue":{"number":9,"repository":{"full_name":"heavy-duty/box"},"pull_request":{"url":"x"}}}}]' \
|
||||||
|
>"$(tfix 30)"
|
||||||
|
printf '{"state":"open"}\n' >"$TMP/repos_heavy-duty_box_pulls_9.json"
|
||||||
|
printf '[]\n' >"$(cfix 30)"
|
||||||
|
issue_probe 30 $'claimed\noffsite' >/dev/null
|
||||||
|
check "one open cross-referenced PR suppresses the nudge" 1 "" \
|
||||||
|
test -f "$TMP/posted-30"
|
||||||
|
|
||||||
|
printf '[]\n' >"$(tfix 31)"
|
||||||
|
printf '[]\n' >"$(cfix 31)"
|
||||||
|
issue_probe 31 $'claimed\noffsite' >/dev/null
|
||||||
|
check "no visible cross-referenced PR stays silent" 1 "" test -f "$TMP/posted-31"
|
||||||
|
|
||||||
|
: >"$(tfix 32).error"
|
||||||
|
printf '[]\n' >"$(cfix 32)"
|
||||||
|
unreadable="$(issue_probe 32 $'claimed\noffsite')"
|
||||||
|
check "an unreadable timeline stays silent" 1 "" test -f "$TMP/posted-32"
|
||||||
|
check "...and leaves the sweep running without an alarming log" 1 "" \
|
||||||
|
grep -qiE 'error|failed' <<<"$unreadable"
|
||||||
|
|
||||||
|
: >"$TMP/api-calls"
|
||||||
|
printf '[]\n' >"$(tfix 33)"
|
||||||
|
printf '[]\n' >"$(cfix 33)"
|
||||||
|
issue_probe 33 claimed >/dev/null
|
||||||
|
check "a non-offsite claim performs only the ordinary timeline read" 0 "1" \
|
||||||
|
grep -cF 'repos/owner/repo/issues/33/timeline' "$TMP/api-calls"
|
||||||
|
: >"$TMP/api-calls"
|
||||||
|
printf '[]\n' >"$(tfix 34)"
|
||||||
|
printf '[]\n' >"$(cfix 34)"
|
||||||
|
issue_probe 34 $'claimed\noffsite' >/dev/null
|
||||||
|
check "an offsite claim performs the one guarded verification read" 0 "2" \
|
||||||
|
grep -cF 'repos/owner/repo/issues/34/timeline' "$TMP/api-calls"
|
||||||
|
|
||||||
check "a one-hour claim stays claimed for the ordinary age reason" 0 "KEEP" \
|
check "a one-hour claim stays claimed for the ordinary age reason" 0 "KEEP" \
|
||||||
claim_decision 1 false 3600
|
claim_decision 1 false 3600
|
||||||
check "the offsite exemption flag is named once at its issueflow decision point" 0 "1" \
|
|
||||||
grep -c 'offsite' "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh"
|
|
||||||
check "no reconciler mutation names offsite (#68 D4)" 1 "" \
|
check "no reconciler mutation names offsite (#68 D4)" 1 "" \
|
||||||
grep -E 'gh (issue|pr) edit.*offsite' \
|
grep -E 'gh (issue|pr) edit.*offsite' \
|
||||||
"$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" \
|
"$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh" \
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue