feat(framework/tools): orchestration helpers — lane-brief.sh + ci-wait.sh (#547)
This commit was merged in pull request #547.
This commit is contained in:
@@ -31,6 +31,7 @@ A Woodpecker API token is required. To configure:
|
||||
| `pipeline-list.sh` | List recent pipelines for a repo |
|
||||
| `pipeline-status.sh` | Get status of a specific or latest pipeline |
|
||||
| `pipeline-trigger.sh` | Trigger a new pipeline build |
|
||||
| `ci-wait.sh` | Block until pipeline(s) reach terminal state |
|
||||
|
||||
## Common Options
|
||||
|
||||
@@ -55,4 +56,7 @@ A Woodpecker API token is required. To configure:
|
||||
|
||||
# Trigger a build on a specific branch
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-trigger.sh -b feature/my-branch
|
||||
|
||||
# Block until one or more pipelines finish (event-driven CI wait)
|
||||
~/.config/mosaic/tools/woodpecker/ci-wait.sh -r usc/uconnect -n 3917 -n 3918
|
||||
```
|
||||
|
||||
86
packages/mosaic/framework/tools/woodpecker/ci-wait.sh
Executable file
86
packages/mosaic/framework/tools/woodpecker/ci-wait.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# ci-wait.sh — block until one or more Woodpecker pipelines reach terminal state.
|
||||
#
|
||||
# Problem it solves: orchestrators hand-author a `while true; curl .../repos/1/pipelines/$n
|
||||
# ...; sleep` loop for every CI wait. Those loops HARDCODE Woodpecker repo id 1 (only
|
||||
# correct for whichever repo happens to be id 1), re-implement URL building with raw
|
||||
# curl, and tend to get armed as tight <300s ScheduleWakeup polls (each poll = a full
|
||||
# wake+reload+recheck cycle). This encapsulates the loop once, on top of the existing
|
||||
# `pipeline-status.sh` wrapper (which resolves repo->id correctly and is instance-aware),
|
||||
# so a CI wait becomes a one-liner.
|
||||
#
|
||||
# Intended use: as the COMMAND of a Monitor / event-driven re-invoke (primary), paired
|
||||
# with a single long (>=1500s) timed fallback — NOT as a tight standalone poll.
|
||||
#
|
||||
# Usage:
|
||||
# ci-wait.sh -r <owner/repo> -n <num> [-n <num> ...] [-a <instance>] [-i <interval>] [-t <timeout>]
|
||||
# ci-wait.sh -r usc/uconnect -n 3917 -n 3918 # wait for both, infer instance
|
||||
# ci-wait.sh -r usc/uconnect -n 3922 -a usc -i 30 -t 2400
|
||||
#
|
||||
# Instance is inferred from the owner (usc->usc, mosaicstack/mosaic->mosaic) unless -a given.
|
||||
# Exit: 0 = all pipelines terminal AND all 'success'; 1 = >=1 terminal non-success;
|
||||
# 2 = usage/precondition error; 3 = timeout before all terminal.
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve pipeline-status.sh as a sibling, matching how the woodpecker tools source
|
||||
# _lib.sh — works under the installed runtime AND an in-repo checkout, no MOSAIC_HOME dep.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PS="$SCRIPT_DIR/pipeline-status.sh"
|
||||
|
||||
REPO="" INSTANCE="" INTERVAL=30 TIMEOUT=3600
|
||||
NUMS=()
|
||||
while getopts "r:n:a:i:t:h" opt; do
|
||||
case "$opt" in
|
||||
r) REPO="$OPTARG" ;;
|
||||
n) NUMS+=("$OPTARG") ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
i) INTERVAL="$OPTARG" ;;
|
||||
t) TIMEOUT="$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; }
|
||||
[[ ${#NUMS[@]} -gt 0 ]] || { echo "FATAL: at least one -n <pipeline-number> required" >&2; exit 2; }
|
||||
[[ -x "$PS" ]] || { echo "FATAL: pipeline-status.sh not found/executable at $PS" >&2; exit 2; }
|
||||
|
||||
# Infer Woodpecker instance from owner unless overridden (matches the git-wrapper convention).
|
||||
if [[ -z "$INSTANCE" ]]; then
|
||||
case "${REPO%%/*}" in
|
||||
usc|USC) INSTANCE=usc ;;
|
||||
mosaicstack|mosaic) INSTANCE=mosaic ;;
|
||||
*) echo "FATAL: cannot infer Woodpecker instance for owner '${REPO%%/*}' — pass -a <instance>" >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
command -v jq >/dev/null || { echo "FATAL: jq not found" >&2; exit 2; }
|
||||
|
||||
TERMINAL_RE='^(success|failure|error|killed|declined|blocked)$'
|
||||
declare -A STATE=() # num -> terminal status, once reached
|
||||
start=$(date +%s 2>/dev/null || echo 0)
|
||||
|
||||
echo "ci-wait: $REPO pipelines [${NUMS[*]}] (instance=$INSTANCE, every ${INTERVAL}s, timeout ${TIMEOUT}s)"
|
||||
while true; do
|
||||
for n in "${NUMS[@]}"; do
|
||||
[[ -n "${STATE[$n]:-}" ]] && continue
|
||||
s=$("$PS" -r "$REPO" -n "$n" -a "$INSTANCE" -f json 2>/dev/null | jq -r '.status // empty' 2>/dev/null || true)
|
||||
if [[ "$s" =~ $TERMINAL_RE ]]; then
|
||||
STATE[$n]="$s"
|
||||
echo " pipeline $n TERMINAL: $s"
|
||||
fi
|
||||
done
|
||||
# all terminal?
|
||||
if [[ ${#STATE[@]} -eq ${#NUMS[@]} ]]; then
|
||||
bad=0
|
||||
for n in "${NUMS[@]}"; do [[ "${STATE[$n]}" == "success" ]] || bad=1; done
|
||||
if [[ $bad -eq 0 ]]; then echo "ci-wait: ALL SUCCESS"; exit 0; fi
|
||||
echo "ci-wait: all terminal, NOT all success — $(for n in "${NUMS[@]}"; do printf '%s=%s ' "$n" "${STATE[$n]}"; done)"
|
||||
exit 1
|
||||
fi
|
||||
now=$(date +%s 2>/dev/null || echo 0)
|
||||
if [[ "$start" != 0 && $((now - start)) -ge $TIMEOUT ]]; then
|
||||
echo "ci-wait: TIMEOUT after ${TIMEOUT}s — pending: $(for n in "${NUMS[@]}"; do [[ -z "${STATE[$n]:-}" ]] && printf '%s ' "$n"; done)"
|
||||
exit 3
|
||||
fi
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
@@ -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":"<s>"} where <s> comes from env
|
||||
# CIW_STATUS_<num> (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"
|
||||
Reference in New Issue
Block a user