|
| 1 | +#!/usr/bin/env bash |
| 2 | +# health_release.sh — release-prep run dashboard. |
| 3 | +# |
| 4 | +# Defines `_health_release` (run via `health release`) that reads the latest |
| 5 | +# PyAutoBuild full release-prep run (the one symlinked from |
| 6 | +# PyAutoBuild/test_results/latest/) and prints a dashboard: |
| 7 | +# |
| 8 | +# - Run timestamp + path + ready/not-ready verdict + total duration |
| 9 | +# - Per-workspace pass / fail / skipped / timeout / duration table |
| 10 | +# - Failure counts grouped by classification |
| 11 | +# - Top-25 slowest scripts (any status) — surfaces timing regressions |
| 12 | +# before they cross the timeout threshold |
| 13 | +# - Slow-skip / needs-fix banner counts |
| 14 | +# - Pointer to triage.md if present (free-form analytical clustering) |
| 15 | +# |
| 16 | +# Usage (normally sourced via ~/.bashrc, run through the `health` dispatcher): |
| 17 | +# source ~/Code/PyAutoLabs/PyAutoMind/scripts/health_release.sh |
| 18 | +# health release |
| 19 | +# |
| 20 | +# Override the run path (e.g. to inspect a specific historical run) by passing |
| 21 | +# it as the first argument: |
| 22 | +# health release ~/Code/PyAutoLabs/PyAutoBuild/test_results/runs/2026-04-29T14-48-47Z |
| 23 | +# |
| 24 | +# Sibling helpers: health-report / health-json / health-triage open the last |
| 25 | +# run's report.md / report.json / triage.md. |
| 26 | +# |
| 27 | +# Note: distinct from the Claude `/health full` command (the conversational |
| 28 | +# layer over the same run artefacts). This function prints straight to stdout, |
| 29 | +# no Claude needed. |
| 30 | + |
| 31 | +PYAUTO_STATUS_FULL_DEFAULT="${PYAUTO_STATUS_FULL_DEFAULT:-$HOME/Code/PyAutoLabs/PyAutoBuild/test_results/latest}" |
| 32 | + |
| 33 | +_health_release() { |
| 34 | + local run_dir="${1:-$PYAUTO_STATUS_FULL_DEFAULT}" |
| 35 | + |
| 36 | + if [[ ! -e "$run_dir" ]]; then |
| 37 | + cat >&2 <<EOF |
| 38 | +health release: no run found at $run_dir |
| 39 | +
|
| 40 | +To produce one, from PyAutoBuild root: |
| 41 | + source ../activate.sh |
| 42 | + python autobuild/run_all.py |
| 43 | +EOF |
| 44 | + return 1 |
| 45 | + fi |
| 46 | + |
| 47 | + # Resolve symlink so the printed path is the actual run dir. |
| 48 | + run_dir="$(readlink -f "$run_dir")" |
| 49 | + |
| 50 | + local report_json="$run_dir/report.json" |
| 51 | + if [[ ! -f "$report_json" ]]; then |
| 52 | + echo "health release: $report_json missing — run incomplete?" >&2 |
| 53 | + return 1 |
| 54 | + fi |
| 55 | + |
| 56 | + python3 - "$run_dir" <<'PY' |
| 57 | +import json |
| 58 | +import sys |
| 59 | +from pathlib import Path |
| 60 | +
|
| 61 | +run_dir = Path(sys.argv[1]) |
| 62 | +with open(run_dir / "report.json") as f: |
| 63 | + r = json.load(f) |
| 64 | +
|
| 65 | +ready = r.get("ready") |
| 66 | +total = float(r.get("total_duration_seconds", 0.0) or 0.0) |
| 67 | +summary = r.get("summary", {}) or {} |
| 68 | +n_pass = summary.get("passed", 0) |
| 69 | +n_fail = summary.get("failed", 0) |
| 70 | +n_skip = summary.get("skipped", 0) |
| 71 | +n_to = summary.get("timeout", 0) |
| 72 | +
|
| 73 | +GREEN = "\033[32m" |
| 74 | +RED = "\033[31m" |
| 75 | +YEL = "\033[33m" |
| 76 | +DIM = "\033[2m" |
| 77 | +RST = "\033[0m" |
| 78 | +
|
| 79 | +verdict = f"{GREEN}READY{RST}" if ready else f"{RED}NOT READY{RST}" |
| 80 | +print(f"{'=' * 76}") |
| 81 | +print(f" PyAuto Status Full") |
| 82 | +print(f"{'=' * 76}") |
| 83 | +print(f"Run: {r.get('run_label','')}") |
| 84 | +print(f"Path: {run_dir}") |
| 85 | +print(f"Status: {verdict} (passed: {n_pass}, failed: {n_fail}, skipped: {n_skip}, timeout: {n_to})") |
| 86 | +print(f"Total: {total:.1f}s ({total/60:.1f} min)") |
| 87 | +print() |
| 88 | +
|
| 89 | +# Per-workspace |
| 90 | +print("Per-workspace") |
| 91 | +print("-" * 76) |
| 92 | +print(f"{'Workspace':<22} {'Passed':>6} {'Failed':>6} {'Skipped':>7} {'Timeout':>7} {'Duration':>10}") |
| 93 | +pp = r.get("per_project", {}) or {} |
| 94 | +ppd = r.get("per_project_duration_seconds", {}) or {} |
| 95 | +for proj in sorted(pp.keys()): |
| 96 | + c = pp[proj] |
| 97 | + f = c.get("failed", 0) |
| 98 | + t = c.get("timeout", 0) |
| 99 | + color = GREEN if (f == 0 and t == 0) else RED |
| 100 | + print( |
| 101 | + f"{color}{proj:<22}{RST} " |
| 102 | + f"{c.get('passed',0):>6} {f:>6} " |
| 103 | + f"{c.get('skipped',0):>7} {t:>7} " |
| 104 | + f"{ppd.get(proj,0):>9.1f}s" |
| 105 | + ) |
| 106 | +print() |
| 107 | +
|
| 108 | +# Failures by classification |
| 109 | +failures = r.get("failures", []) or [] |
| 110 | +if failures: |
| 111 | + by_class = {} |
| 112 | + for fr in failures: |
| 113 | + cls = fr.get("classification", "unknown") |
| 114 | + by_class.setdefault(cls, []).append(fr) |
| 115 | + labels = { |
| 116 | + "source_code_bug": "Source code bugs", |
| 117 | + "workspace_issue": "Workspace issues", |
| 118 | + "workspace_data": "Missing data files", |
| 119 | + "environment": "Environment issues", |
| 120 | + "timeout": "Timeouts", |
| 121 | + "known_numerical": "Known numerical", |
| 122 | + "unknown": "Unclassified", |
| 123 | + } |
| 124 | + print(f"Failures by classification ({len(failures)} total)") |
| 125 | + print("-" * 76) |
| 126 | + for cls in sorted(by_class.keys(), key=lambda c: -len(by_class[c])): |
| 127 | + items = by_class[cls] |
| 128 | + print(f" {labels.get(cls, cls):<22} {len(items)}") |
| 129 | + print() |
| 130 | +
|
| 131 | +# Slowest 25 |
| 132 | +slowest = r.get("slowest", []) or [] |
| 133 | +if slowest: |
| 134 | + print(f"Slowest {len(slowest)} scripts") |
| 135 | + print("-" * 76) |
| 136 | + print(f"{'Duration':>9} {'Status':<8} {'Project':<16} Script") |
| 137 | + for s in slowest: |
| 138 | + proj = s.get("project", "") |
| 139 | + stat = s.get("status", "") |
| 140 | + fil = s.get("file", "") |
| 141 | + # Trim absolute paths to last 3 segments for readability. |
| 142 | + short = "/".join(fil.split("/")[-3:]) |
| 143 | + dur = float(s.get("duration_seconds", 0.0) or 0.0) |
| 144 | + color = RED if stat in ("failed", "timeout") else (YEL if dur > 180 else "") |
| 145 | + print(f"{color}{dur:>8.1f}s {stat:<8} {proj:<16} {short}{RST}") |
| 146 | + print() |
| 147 | +
|
| 148 | +# Parked scripts banners |
| 149 | +slow_skips = r.get("slow_skips") or [] |
| 150 | +nf_skips = r.get("needs_fix_skips") or [] |
| 151 | +if slow_skips or nf_skips: |
| 152 | + print("Parked scripts (workspace no_run.yaml banners)") |
| 153 | + print("-" * 76) |
| 154 | + if slow_skips: |
| 155 | + print(f" SLOW skips: {len(slow_skips)} (need performance fix)") |
| 156 | + if nf_skips: |
| 157 | + print(f" NEEDS_FIX skips: {len(nf_skips)} (parked broken)") |
| 158 | + print() |
| 159 | +
|
| 160 | +# Pointers |
| 161 | +print("Pointers") |
| 162 | +print("-" * 76) |
| 163 | +print(f" Markdown report: {run_dir}/report.md {DIM}(health-report){RST}") |
| 164 | +print(f" Run JSON: {run_dir}/report.json {DIM}(health-json){RST}") |
| 165 | +triage = run_dir / "triage.md" |
| 166 | +if triage.exists(): |
| 167 | + print(f" {GREEN}Triage notes: {triage}{RST} {DIM}(health-triage){RST}") |
| 168 | +PY |
| 169 | +} |
| 170 | + |
| 171 | +# _pyauto_run_file <subpath> [run-dir-arg] — resolve a file inside the latest |
| 172 | +# (or supplied) run directory. Used by the pyauto-{report,json,triage} viewers. |
| 173 | +_pyauto_run_file() { |
| 174 | + local subpath="$1" |
| 175 | + local run_dir="${2:-$PYAUTO_STATUS_FULL_DEFAULT}" |
| 176 | + |
| 177 | + if [[ ! -e "$run_dir" ]]; then |
| 178 | + echo "pyauto: no run found at $run_dir" >&2 |
| 179 | + return 1 |
| 180 | + fi |
| 181 | + run_dir="$(readlink -f "$run_dir")" |
| 182 | + |
| 183 | + local target="$run_dir/$subpath" |
| 184 | + if [[ ! -f "$target" ]]; then |
| 185 | + echo "pyauto: $target missing" >&2 |
| 186 | + return 1 |
| 187 | + fi |
| 188 | + printf '%s' "$target" |
| 189 | +} |
| 190 | + |
| 191 | +# health-report [run-dir] — view report.md in the pager. |
| 192 | +health-report() { |
| 193 | + local f |
| 194 | + f="$(_pyauto_run_file report.md "$1")" || return 1 |
| 195 | + "${PAGER:-less}" "$f" |
| 196 | +} |
| 197 | + |
| 198 | +# health-json [run-dir] — view report.json. Uses jq for color + paging when |
| 199 | +# available, falls back to plain cat otherwise. |
| 200 | +health-json() { |
| 201 | + local f |
| 202 | + f="$(_pyauto_run_file report.json "$1")" || return 1 |
| 203 | + if command -v jq >/dev/null 2>&1; then |
| 204 | + jq -C . "$f" | "${PAGER:-less}" -R |
| 205 | + else |
| 206 | + "${PAGER:-less}" "$f" |
| 207 | + fi |
| 208 | +} |
| 209 | + |
| 210 | +# health-triage [run-dir] — view triage.md in the pager. |
| 211 | +health-triage() { |
| 212 | + local f |
| 213 | + f="$(_pyauto_run_file triage.md "$1")" || return 1 |
| 214 | + "${PAGER:-less}" "$f" |
| 215 | +} |
0 commit comments