Remediation of coder3 independent-validation blocker on PR #547. lane-brief.sh inspected only the open-PR index/title/head fields, never the PR BODY or Gitea issue linkage. A body-only "Closes #546" was therefore invisible, so issue #546 (open, with PR #547 'Closes #546' in its body) was placed under DISPATCH CANDIDATES with work-underway count 0 — re-dispatchable in-flight work, unacceptable for a dispatch-truth tool. Fix: - Fetch open PRs as JSON including `body`; resolve PR->issue links via Gitea's closing-keyword set (close/closes/closed, fix/fixes/fixed, resolve/resolves/ resolved), case-insensitive, word-boundary anchored, `#` directly following the keyword. Any issue so linked from an OPEN PR is classified WORK UNDERWAY. - Preserve the prior title/head bare-ref heuristic and per-repo behavior; require `#` immediately after the keyword so cross-repo `owner/repo#N` forms don't leak. - Bare `#N` prose mentions in a body are intentionally NOT links (e.g. "#538 line of work") to avoid marking live, dispatchable issues as in-flight. Tests (committed, RED-on-revert non-vacuity): - test-lane-brief-pr-linkage.sh: open-PR-with-'Closes #546'-in-body excludes #546 from candidates (and a reverted copy with the body-scan removed regresses #546 to a candidate — RED proof); bare #777 and substring 'hotfix #999' stay candidates (word-boundary + closing-keyword-only guards). - test-ci-wait-exit-matrix.sh: ci-wait.sh exit matrix 0 (all-success) / 1 (terminal-not-success: failure + error/killed) / 2 (usage) / 3 (timeout). shellcheck -x + bash -n clean on all four files; no secret values. ci-wait.sh unchanged (coder3 PASS preserved). Closed-issue exclusion unchanged. Refs #546, PR #547 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
130 lines
7.0 KiB
Bash
Executable File
130 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# lane-brief.sh — live dispatch brief for a repo "lane" (milestone/label), straight
|
|
# from current Gitea state. Defeats stale worker self-report: workers brief from
|
|
# static notes and routinely report issues "todo" that are already CLOSED, forcing
|
|
# the orchestrator to re-verify each one before dispatch. This returns the CURRENT
|
|
# open set, classified for dispatch, in one call.
|
|
#
|
|
# Usage:
|
|
# lane-brief.sh -r <owner/repo> [-m <milestone>] [-l <label>] [-L <login>] [-n <limit>]
|
|
# lane-brief.sh -r usc/uconnect -m "M2M Part Search (0.0.45)"
|
|
# lane-brief.sh -r usc/uconnect -l domain/6-security
|
|
#
|
|
# Reliable signals (closed issues are excluded by definition — that's the point):
|
|
# - open-vs-closed : authoritative; this is the stale-intake failure mode.
|
|
# - PR-linkage : an open PR referencing the issue = work underway.
|
|
# Assignees/dependencies are intentionally NOT trusted as "available" signals —
|
|
# fleets that track work-state out-of-band (tmux board, issue text) leave them
|
|
# empty in Gitea. Output therefore partitions by PR presence and the OPEN-NO-PR set
|
|
# is "dispatch candidates to cross-check against the live fleet", not a blind list.
|
|
#
|
|
# Login resolution order: -L flag > $GITEA_LOGIN > owner inference (usc->usc,
|
|
# mosaicstack/mosaic->mosaicstack) > detect-platform.sh default-login fallback.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=/dev/null
|
|
source "$SCRIPT_DIR/detect-platform.sh"
|
|
|
|
REPO="" MILESTONE="" LABEL="" LOGIN="" LIMIT=100
|
|
while getopts "r:m:l:L:n:h" opt; do
|
|
case "$opt" in
|
|
r) REPO="$OPTARG" ;;
|
|
m) MILESTONE="$OPTARG" ;;
|
|
l) LABEL="$OPTARG" ;;
|
|
L) LOGIN="$OPTARG" ;;
|
|
n) LIMIT="$OPTARG" ;;
|
|
h) grep '^#' "$0" | sed 's/^# \?//'; exit 0 ;;
|
|
*) echo "see -h" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
[[ -n "$REPO" ]] || { echo "FATAL: -r <owner/repo> required" >&2; exit 2; }
|
|
|
|
# Resolve login: explicit -L, then $GITEA_LOGIN, then owner inference, then the
|
|
# shared default-login resolver. Owner inference comes before the shared fallback
|
|
# because the latter is not owner-aware (picks the default tea login), which is
|
|
# wrong for cross-instance lanes.
|
|
if [[ -z "$LOGIN" ]]; then
|
|
if [[ -n "${GITEA_LOGIN:-}" ]]; then
|
|
LOGIN="$GITEA_LOGIN"
|
|
else
|
|
case "${REPO%%/*}" in
|
|
usc|USC) LOGIN=usc ;;
|
|
mosaicstack|mosaic) LOGIN=mosaicstack ;;
|
|
*) LOGIN="$(get_gitea_login_for_repo_override 2>/dev/null || true)" ;;
|
|
esac
|
|
fi
|
|
fi
|
|
[[ -n "$LOGIN" ]] || { echo "FATAL: could not resolve a Gitea login for $REPO (pass -L or set GITEA_LOGIN)" >&2; exit 2; }
|
|
|
|
command -v tea >/dev/null || { echo "FATAL: tea not found" >&2; exit 1; }
|
|
command -v jq >/dev/null || { echo "FATAL: jq not found" >&2; exit 1; }
|
|
|
|
ISSUES_JSON="$(tea issues list --repo "$REPO" --login "$LOGIN" --state open --limit "$LIMIT" \
|
|
--fields index,title,assignees,milestone,labels --output json 2>/dev/null)" || {
|
|
echo "FATAL: tea issues list failed for $REPO (login=$LOGIN)" >&2; exit 1; }
|
|
|
|
# Open PRs, to cross-ref which issues already have work in flight. An issue is
|
|
# "work underway" if an open PR links to it. Two link signals are honored:
|
|
# (a) a closing keyword in the PR BODY — Gitea's auto-close set (close/closes/
|
|
# closed, fix/fixes/fixed, resolve/resolves/resolved), case-insensitive,
|
|
# directly preceding `#N`. This is the AUTHORITATIVE link Gitea itself uses
|
|
# to associate a PR with the issue it resolves; a body-only "Closes #546"
|
|
# is the common case and MUST count. The earlier version inspected only the
|
|
# PR index/title/head TSV (never the body or Gitea linkage), so a body-only
|
|
# reference was invisible and the linked OPEN issue was misclassified as a
|
|
# dispatch candidate — re-dispatchable in-flight work (the #546/#547 defect).
|
|
# (b) a bare #N in the PR title, or an issue number embedded in the head branch
|
|
# (feat/546-x, fix-546) — the weaker heuristic preserved from prior behavior.
|
|
# Bare #N mentions in the BODY are deliberately NOT treated as links: PR bodies
|
|
# routinely name unrelated issues in prose ("relevant to the #538 line of work"),
|
|
# and counting those would wrongly mark live, dispatchable issues as in-flight.
|
|
# Only the closing-keyword form is a commitment to resolve that issue. Requiring
|
|
# `#` to directly follow the keyword also keeps cross-repo `owner/repo#N` forms
|
|
# from leaking a foreign issue number into this per-repo lane (cross-repo lanes
|
|
# are run per-repo). JSON (not TSV) is used so multi-line bodies parse cleanly.
|
|
PRS_JSON="$(tea pulls list --repo "$REPO" --login "$LOGIN" --state open \
|
|
--fields index,title,head,body --output json 2>/dev/null || echo '[]')"
|
|
[[ -n "$PRS_JSON" ]] || PRS_JSON='[]'
|
|
|
|
# \b anchors the keyword to a word start so embedded substrings do not match
|
|
# (e.g. "prefix #5", "disclosed #7" must NOT be read as "fix #5" / "closed #7").
|
|
GITEA_CLOSE_KW='close[sd]?|fix(e[sd])?|resolve[sd]?'
|
|
PR_BODY_REFS="$(printf '%s' "$PRS_JSON" | jq -r '.[] | .body // ""' 2>/dev/null \
|
|
| grep -oiE "\\b(${GITEA_CLOSE_KW})[[:space:]:]+#[0-9]+" | grep -oE '[0-9]+' || true)"
|
|
PR_TITLE_HEAD_REFS="$(printf '%s' "$PRS_JSON" \
|
|
| jq -r '.[] | [ (.title // ""), (.head // "" | if type=="object" then (.ref // "") else . end) ] | join(" ")' 2>/dev/null \
|
|
| grep -oE '#[0-9]+|[/-][0-9]{3,}' | grep -oE '[0-9]+' || true)"
|
|
PR_ISSUE_REFS="$(printf '%s\n%s\n' "$PR_BODY_REFS" "$PR_TITLE_HEAD_REFS" | grep -E '^[0-9]+$' | sort -u || true)"
|
|
|
|
ts="$(date -u '+%Y-%m-%d %H:%MZ' 2>/dev/null || echo '?')"
|
|
filt="$REPO"; [[ -n "$MILESTONE" ]] && filt="$filt · milestone:'$MILESTONE'"; [[ -n "$LABEL" ]] && filt="$filt · label:'$LABEL'"
|
|
echo "LANE BRIEF — $filt · $ts (login=$LOGIN)"
|
|
echo "(open issues only; closed are excluded by definition — that's the point)"
|
|
echo
|
|
|
|
# Label match is exact-token against tea's space-separated labels string (so -l
|
|
# "security" does NOT match label "domain/6-security"). Caveat: label names that
|
|
# themselves contain spaces aren't distinguishable in tea's string form.
|
|
printf '%s' "$ISSUES_JSON" | jq -r --arg ms "$MILESTONE" --arg lb "$LABEL" --arg prs "$PR_ISSUE_REFS" '
|
|
($prs | split("\n") | map(select(length>0))) as $prrefs
|
|
| map(
|
|
select( ($ms=="" or .milestone==$ms)
|
|
and ($lb=="" or ((.labels//"") | split(" ") | index($lb) != null)) )
|
|
| . + { assigned: ((.assignees//"")|length>0),
|
|
haspr: (.index as $ix | ($prrefs | index($ix)) != null) }
|
|
)
|
|
| (map(select(.haspr|not))) as $candidates
|
|
| (map(select(.haspr))) as $inflight
|
|
| "DISPATCH CANDIDATES (open · no open PR) — \($candidates|length) [cross-check vs live fleet]:",
|
|
( $candidates[] | " #\(.index) \(.title[0:90])\(if .assigned then " (gitea-assignee set)" else "" end)" ),
|
|
"",
|
|
"WORK UNDERWAY (open · PR in flight) — \($inflight|length):",
|
|
( $inflight[] | " #\(.index) \(.title[0:80]) [PR open]" )
|
|
'
|
|
echo
|
|
echo "Closed issues are excluded — do NOT take a worker's self-reported 'todo' on faith."
|
|
echo "Candidates = open + no PR; confirm against the live fleet before dispatch"
|
|
echo "(fleets that don't self-assign in Gitea leave 'unassigned' meaningless)."
|