From 1d87b76e1f903991a17fdb3ce2ddfdc28447dcc0 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 18 Jun 2026 16:49:13 -0500 Subject: [PATCH] =?UTF-8?q?fix(framework/tools):=20lane-brief.sh=20?= =?UTF-8?q?=E2=80=94=20classify=20PR-body-linked=20issues=20as=20work-unde?= =?UTF-8?q?rway=20(#546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../mosaic/framework/tools/git/lane-brief.sh | 36 +++++- .../tools/git/test-lane-brief-pr-linkage.sh | 114 ++++++++++++++++++ .../woodpecker/test-ci-wait-exit-matrix.sh | 76 ++++++++++++ 3 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh create mode 100644 packages/mosaic/framework/tools/woodpecker/test-ci-wait-exit-matrix.sh diff --git a/packages/mosaic/framework/tools/git/lane-brief.sh b/packages/mosaic/framework/tools/git/lane-brief.sh index b1ff7b7..336a51a 100755 --- a/packages/mosaic/framework/tools/git/lane-brief.sh +++ b/packages/mosaic/framework/tools/git/lane-brief.sh @@ -65,10 +65,38 @@ ISSUES_JSON="$(tea issues list --repo "$REPO" --login "$LOGIN" --state open --li --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. -PRS_TSV="$(tea pulls list --repo "$REPO" --login "$LOGIN" --state open \ - --fields index,title,head --output tsv 2>/dev/null || true)" -PR_ISSUE_REFS="$(printf '%s\n' "$PRS_TSV" | grep -oE '#[0-9]+|[/-][0-9]{3,}' | grep -oE '[0-9]+' | sort -u || true)" +# 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'" diff --git a/packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh b/packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh new file mode 100644 index 0000000..92baf52 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Regression harness for lane-brief.sh PR->issue linkage classification. +# +# Covers the #546/#547 defect: lane-brief.sh inspected only the PR index/title/head +# fields and never the PR BODY, so an open PR whose body says "Closes #546" did not +# mark issue #546 as work-underway — #546 was listed as a DISPATCH CANDIDATE and was +# re-dispatchable in-flight work. +# +# Asserts: +# 1. an open issue closed-keyword-linked from a PR BODY ("Closes #546") is +# classified WORK UNDERWAY, not a dispatch candidate. +# 2. a BARE "#777" prose mention in a PR body does NOT classify #777 as +# work-underway (only Gitea closing keywords are a real link) — #777 stays a +# dispatch candidate. +# 3. NON-VACUITY / RED-ON-REVERT: a copy of the script with the body-scan removed +# misclassifies #546 as a dispatch candidate — proving the body-scan is exactly +# what fixes the defect and that assertion 1 fails if the fix is reverted. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LANE_BRIEF="$SCRIPT_DIR/lane-brief.sh" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/lane-brief-pr-linkage}" +BIN_DIR="$WORK_DIR/bin" + +rm -rf "$WORK_DIR" +mkdir -p "$BIN_DIR" + +# --- fake `tea`: serves a fixed open-issue set and one open PR. ---------------- +# PR #547 body uses a closing keyword for #546 ("Closes #546") and a BARE mention +# of #777 ("the #777 line of work"). #777 must NOT be treated as linked. +cat > "$BIN_DIR/tea" <<'SH' +#!/usr/bin/env bash +set -euo pipefail +case "${1:-} ${2:-}" in + "issues list") + cat <<'JSON' +[ + {"index":"546","title":"lane-brief + ci-wait orchestration tooling","assignees":[],"milestone":null,"labels":""}, + {"index":"777","title":"unrelated downstream item","assignees":[],"milestone":null,"labels":""}, + {"index":"999","title":"item only named inside the word hotfix","assignees":[],"milestone":null,"labels":""} +] +JSON + ;; + "pulls list") + cat <<'JSON' +[ + {"index":"547","title":"feat(framework/tools): orchestration helpers","head":"feat/orchestration-tools-lane-brief-ci-wait","body":"Two additive orchestration tools.\n\nCloses #546.\n\nLogin resolution is relevant to the #777 line of work but does not touch it.\nThis shipped as a hotfix #999 earlier — that bare reference must not link it.\n\nFixes #546\n"} +] +JSON + ;; + *) + echo "fake-tea: unhandled: $*" >&2; exit 1 ;; +esac +SH +chmod +x "$BIN_DIR/tea" + +run_brief() { # $1 = script path + PATH="$BIN_DIR:$PATH" "$1" -r mosaic/stack -L test-login 2>/dev/null +} + +# Extract the issue numbers under a named section header until the next blank line. +section_nums() { # $1 = output $2 = header-prefix + printf '%s\n' "$1" | awk -v h="$2" ' + index($0,h)==1 {grab=1; next} + grab && /^[[:space:]]*$/ {grab=0} + grab && match($0, /#[0-9]+/) { print substr($0, RSTART+1, RLENGTH-1) } + ' +} + +fail() { echo "FAIL: $1" >&2; exit 1; } +contains() { printf '%s\n' "$1" | grep -qx "$2"; } + +# --------------------------------------------------------------------------- +# Fixed (current) script behavior +# --------------------------------------------------------------------------- +OUT="$(run_brief "$LANE_BRIEF")" +CAND="$(section_nums "$OUT" 'DISPATCH CANDIDATES')" +UNDER="$(section_nums "$OUT" 'WORK UNDERWAY')" + +echo "--- lane-brief output (fixed) ---"; printf '%s\n' "$OUT" +echo "--- candidates: [$(printf '%s' "$CAND" | tr '\n' ' ')] underway: [$(printf '%s' "$UNDER" | tr '\n' ' ')] ---" + +contains "$UNDER" 546 || fail "#546 (PR body 'Closes #546') should be WORK UNDERWAY" +contains "$CAND" 546 && fail "#546 must NOT be a dispatch candidate (it has an open PR)" +contains "$CAND" 777 || fail "#777 (only a bare prose mention) should remain a dispatch candidate" +contains "$UNDER" 777 && fail "#777 must NOT be work-underway — bare body mentions are not links" +contains "$CAND" 999 || fail "#999 ('hotfix #999' — keyword is a substring) should remain a candidate" +contains "$UNDER" 999 && fail "#999 must NOT be work-underway — word-boundary must reject 'hotfix'" +echo "PASS: body closing-keyword link classifies #546 underway; bare #777 / substring #999 stay candidates" + +# --------------------------------------------------------------------------- +# NON-VACUITY: revert the body-scan and prove #546 regresses to a candidate. +# --------------------------------------------------------------------------- +REVERTED="$SCRIPT_DIR/.lane-brief.reverted.$$.sh" +trap 'rm -f "$REVERTED"' EXIT +# Drop the PR_BODY_REFS contribution from the union (simulates the pre-fix script +# that only looked at index/title/head). Sibling `source detect-platform.sh` still +# resolves because the copy lives in the same dir. +# shellcheck disable=SC2016 # single-quoted on purpose: sed needs the literal $PR_BODY_REFS +sed 's/"\$PR_BODY_REFS"/""/' "$LANE_BRIEF" > "$REVERTED" +chmod +x "$REVERTED" +grep -q 'PR_BODY_REFS' "$REVERTED" || fail "revert sed anchor not found — test is stale" + +ROUT="$(run_brief "$REVERTED")" +RCAND="$(section_nums "$ROUT" 'DISPATCH CANDIDATES')" +RUNDER="$(section_nums "$ROUT" 'WORK UNDERWAY')" +echo "--- candidates(reverted): [$(printf '%s' "$RCAND" | tr '\n' ' ')] underway: [$(printf '%s' "$RUNDER" | tr '\n' ' ')] ---" + +contains "$RCAND" 546 || fail "non-vacuity broken: reverted script should misclassify #546 as a candidate" +contains "$RUNDER" 546 && fail "non-vacuity broken: reverted script should NOT mark #546 underway" +echo "PASS (RED-on-revert): without the body-scan, #546 regresses to a dispatch candidate" + +echo "ALL PASS: test-lane-brief-pr-linkage.sh" diff --git a/packages/mosaic/framework/tools/woodpecker/test-ci-wait-exit-matrix.sh b/packages/mosaic/framework/tools/woodpecker/test-ci-wait-exit-matrix.sh new file mode 100644 index 0000000..a3923e8 --- /dev/null +++ b/packages/mosaic/framework/tools/woodpecker/test-ci-wait-exit-matrix.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Regression harness for ci-wait.sh terminal-state aggregation and exit codes. +# +# ci-wait.sh wraps pipeline-status.sh and blocks until every requested pipeline +# reaches a terminal Woodpecker state, then maps the aggregate to an exit code. +# That contract is what callers arm a Monitor/timed-fallback around, so it must be +# exact. This harness drives ci-wait.sh against a stub pipeline-status.sh whose +# per-pipeline status is fixture-controlled, and asserts the full exit matrix: +# +# 0 = every pipeline terminal AND all 'success' +# 1 = every pipeline terminal, at least one non-success +# 2 = usage/precondition error (missing -n) +# 3 = timeout before all pipelines terminal +# +# Non-vacuity: each case pins a DISTINCT exit code to a distinct fixture, so a +# regression in success-aggregation (case 0 vs 1), terminal detection (case 3), +# or arg validation (case 2) flips exactly one assertion RED. + +set -euo pipefail + +CIW_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ci-wait.sh" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/ci-wait-exit-matrix}" +TOOL_DIR="$WORK_DIR/tool" + +rm -rf "$WORK_DIR" +mkdir -p "$TOOL_DIR" + +# ci-wait.sh resolves pipeline-status.sh as a sibling ($SCRIPT_DIR/pipeline-status.sh), +# so we run a COPY of ci-wait.sh next to a stub sibling we control. +cp "$CIW_SRC" "$TOOL_DIR/ci-wait.sh" +chmod +x "$TOOL_DIR/ci-wait.sh" + +# Stub pipeline-status.sh: emits {"status":""} where comes from env +# CIW_STATUS_ (default "running" = non-terminal, drives the timeout path). +cat > "$TOOL_DIR/pipeline-status.sh" <<'SH' +#!/usr/bin/env bash +set -euo pipefail +num="" +while getopts "r:n:a:f:" opt; do case "$opt" in n) num="$OPTARG" ;; *) : ;; esac; done +var="CIW_STATUS_${num}" +printf '{"status":"%s"}\n' "${!var:-running}" +SH +chmod +x "$TOOL_DIR/pipeline-status.sh" + +CIW="$TOOL_DIR/ci-wait.sh" + +run_expect() { # $1 = expected exit $2 = label ; rest = args + local want="$1" label="$2"; shift 2 + local rc=0 + "$CIW" "$@" >/dev/null 2>&1 || rc=$? + if [[ "$rc" -ne "$want" ]]; then + echo "FAIL [$label]: expected exit $want, got $rc" >&2; exit 1 + fi + echo "PASS [$label]: exit $rc" +} + +# 0 — both pipelines terminal + success +CIW_STATUS_100=success CIW_STATUS_101=success \ + run_expect 0 "all-success" -r mosaic/stack -n 100 -n 101 -a mosaic -i 1 -t 30 + +# 1 — both terminal, one failure +CIW_STATUS_100=success CIW_STATUS_101=failure \ + run_expect 1 "terminal-not-success" -r mosaic/stack -n 100 -n 101 -a mosaic -i 1 -t 30 + +# 1 — other terminal non-success states still map to 1 (error/killed) +CIW_STATUS_100=error CIW_STATUS_101=killed \ + run_expect 1 "terminal-error-killed" -r mosaic/stack -n 100 -n 101 -a mosaic -i 1 -t 30 + +# 3 — a pipeline never reaches terminal state before timeout +CIW_STATUS_100=success CIW_STATUS_101=running \ + run_expect 3 "timeout-pending" -r mosaic/stack -n 100 -n 101 -a mosaic -i 1 -t 0 + +# 2 — usage error: no -n +run_expect 2 "usage-missing-n" -r mosaic/stack -a mosaic + +echo "ALL PASS: test-ci-wait-exit-matrix.sh"