#!/usr/bin/env python3 # -*- coding: utf-8 -*- """hostinstall_harness_gate.py — runs scripts/hostinstall-mode-harness.sh as a gate (R-1146, 2026-10-18). WHY. The harness (bash -n, shellcheck, the argument-refusal matrix, the mode invariants, the --rotate-recovery-only cases R1141-*, and the vault-first order R1145-*) was RED on `main` for weeks — SC2066 and a stale GL8-F6 check — because no gate ran it: `repo_gates.py` ran only `hostinstall_gates.py`. The R-29 shape. It is fast (no network: its hub is a closed port; no container; its PVE tier skips itself off a PVE host), so it runs in `--fast` and in the pre-push hook. THE CI RUNNER HAS NO bash (python3 and git only, `.gitea/workflows/gates.yml`). There the gate says so and exits 0 — the same out-loud skip as check-onboarding.py's absent sibling — and the pre-push hook on DooPlex is where it bites. Exit 0 harness green (or no bash, said) · 1 a harness case failed. """ import os import shutil import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) HARNESS = os.path.join(ROOT, "scripts", "hostinstall-mode-harness.sh") def main(): if not os.path.isfile(HARNESS): print("hostinstall-harness: FAIL — %s is missing" % HARNESS) return 1 bash = shutil.which("bash") if not bash: print("hostinstall-harness: NOT CHECKED here — no bash on this runner; the pre-push hook on DooPlex runs it") return 0 p = subprocess.run([bash, HARNESS, os.path.join(ROOT, "scripts", "felhom-host-install.sh")], cwd=ROOT, capture_output=True, text=True, errors="replace") out = p.stdout + p.stderr fails = [l for l in out.splitlines() if l.startswith("FAIL ")] tail = [l for l in out.splitlines() if l.startswith("=== ") and "passed" in l] for l in fails: print(" " + l) print("hostinstall-harness: %s" % (tail[-1] if tail else "(no summary line)")) if p.returncode != 0 or fails or not tail: print("hostinstall-harness: CONVICTED (rc=%d, %d failing case(s))" % (p.returncode, len(fails))) return 1 print("hostinstall-harness OK") return 0 if __name__ == "__main__": sys.exit(main())