#!/usr/bin/env bash # Run a sequence of tasks through pi, verifying after each one. # # Usage: # scripts/run-all.sh # run the default local-verifiable sequence # scripts/run-all.sh 1.1 1.2 2.1 # run a specific list, in the order given # scripts/run-all.sh --list # show the default sequence and exit # scripts/run-all.sh --resume # rerun the default sequence, skipping tasks # # already recorded in .task-runs/completed.txt # # Each task gets a fresh zero-context pi session. After each task the full gate runs: # # go test ./... -race # CGO_ENABLED=0 go build ./... # go vet ./... # # The loop HALTS on the first gate failure. That is deliberate. These tasks build on # each other, so continuing past a broken foundation produces a pile of work that all # has to be redone. A halted loop is cheap; a cascade is not. set -uo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" # Default sequence: dependency-ordered, and limited to tasks that can be verified # locally with no cluster and no credentials. # # Deliberately EXCLUDED from the default run: # 0.6 CI pipeline -- needs a CI runner, not verifiable here # 2.8 Kong parity -- requires the live Kong to diff against # 3.x Authentication -- needs a real Authentik provider and tokens # 5.3 ServiceMonitor -- needs Prometheus in-cluster # 6.x Deploy and cutover -- touches live cluster state; run these by hand # 7.x New prefixes -- deliberately after cutover DEFAULT_TASKS=( 0.2 0.3 0.4 0.5 1.1 1.2 1.3 1.4 1.5 1.6 1.7 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.9 2.10 2.11 2.12 2.13 2.14 2.15 4.1 4.2 4.3 5.1 5.2 ) if [[ "${1:-}" == "--list" ]]; then printf '%s\n' "${DEFAULT_TASKS[@]}" exit 0 fi LOG_DIR=".task-runs" mkdir -p "$LOG_DIR" # Tasks that have passed the gate, one id per line, appended as they finish and # carried across invocations. This is what makes --resume possible after a halt. DONE_FILE="$LOG_DIR/completed.txt" touch "$DONE_FILE" if [[ "${1:-}" == "--resume" ]]; then shift # Resume the default sequence minus everything already recorded as passing. # Order is preserved: DEFAULT_TASKS drives the loop, DONE_FILE only filters. TASKS=() for t in "${DEFAULT_TASKS[@]}"; do grep -qxF "$t" "$DONE_FILE" || TASKS+=("$t") done SKIPPED=$(( ${#DEFAULT_TASKS[@]} - ${#TASKS[@]} )) echo "resuming : skipping $SKIPPED already-passed task(s) recorded in $DONE_FILE" if [[ ${#TASKS[@]} -eq 0 ]]; then echo "nothing left to run." exit 0 fi elif [[ $# -gt 0 ]]; then TASKS=("$@") else TASKS=("${DEFAULT_TASKS[@]}") fi gate() { # Returns 0 if the repository is in a good state, 1 otherwise. # # The gate is uniformly strict, including for RED tasks. `Stage: RED` in this board # means "write the failing test first, then implement until it passes" -- the task # ends green. Every RED task's Verify block says `expected: passes`. So a failing # suite is a failure at any stage, and relaxing the gate for RED would let # half-finished work through. local task_id="$1" local stage stage=$(grep -m1 '^Stage:' tasks/"$task_id"-*.md 2>/dev/null | awk '{print $2}') CGO_ENABLED=0 go build ./... >"$LOG_DIR/gate-build.log" 2>&1 || { echo " FAIL: go build"; tail -20 "$LOG_DIR/gate-build.log"; return 1; } go vet ./... >"$LOG_DIR/gate-vet.log" 2>&1 || { echo " FAIL: go vet"; tail -20 "$LOG_DIR/gate-vet.log"; return 1; } go clean -testcache >/dev/null 2>&1 if ! go test ./... -race >"$LOG_DIR/gate-test.log" 2>&1; then echo " FAIL: go test -race (stage=${stage:-?})" grep -E '^(---|\s+---) FAIL|^FAIL' "$LOG_DIR/gate-test.log" | head -8 | sed 's/^/ /' return 1 fi echo " build/vet/test all pass (stage=${stage:-?})" return 0 } echo "started : $(date '+%Y-%m-%d %H:%M:%S')" echo "tasks : ${#TASKS[@]}" echo "model : ${PI_PROVIDER:-homelab-ornith}/${PI_MODEL:-ornith:35b}" echo "logs : $LOG_DIR/" echo "gate : build + vet + full race suite after every task; halts on failure" echo "pid : $$" echo COMPLETED=() for TASK_ID in "${TASKS[@]}"; do echo "════════════════════════════════════════════" echo "TASK $TASK_ID" echo "════════════════════════════════════════════" if ! ./scripts/run-task.sh "$TASK_ID" >"$LOG_DIR/$TASK_ID.log" 2>&1; then echo " pi exited non-zero. Last output:" tail -25 "$LOG_DIR/$TASK_ID.log" echo echo "HALTED at $TASK_ID (pi failure). Completed: ${COMPLETED[*]:-none}" echo "Fix, then: scripts/run-all.sh --resume" exit 1 fi tail -12 "$LOG_DIR/$TASK_ID.log" | sed 's/^/ | /' echo echo " verifying..." if ! gate "$TASK_ID"; then echo echo "HALTED at $TASK_ID (gate failure). Completed: ${COMPLETED[*]:-none}" echo "Full pi output: $LOG_DIR/$TASK_ID.log" echo "Fix, then: scripts/run-all.sh --resume" exit 1 fi # grep -c prints 0 AND exits 1 when nothing matches, so a `|| echo 0` fallback # would append a second line and make these two-line strings. Swallow the exit # status instead and keep the single count grep already printed. TICKED=$(grep -c '^- \[x\]' tasks/"$TASK_ID"-*.md 2>/dev/null) || true TOTAL=$(grep -c '^- \[' tasks/"$TASK_ID"-*.md 2>/dev/null) || true echo " gate OK checkboxes ticked: ${TICKED:-0}/${TOTAL:-0}" [[ "$TICKED" != "$TOTAL" ]] && echo " NOTE: not all criteria ticked -- review before trusting this task" COMPLETED+=("$TASK_ID") grep -qxF "$TASK_ID" "$DONE_FILE" || echo "$TASK_ID" >>"$DONE_FILE" echo done echo "════════════════════════════════════════════" echo "All ${#COMPLETED[@]} tasks passed the gate." echo "Completed: ${COMPLETED[*]}" echo echo "Not run by this script (need a cluster, credentials, or live Kong):" echo " 0.6 CI, 2.8 Kong parity, 3.x auth, 5.3 ServiceMonitor, 6.x deploy, 7.x prefixes"