feat(preflight): host/toolchain validation + VERSIONS pin-file — T01

- VERSIONS: 7 container images (CONTRACT_003 §3.2) + 13 host tools, KEY=value,
  source-able+greppable; images carry :PIN_DIGEST placeholders with a documented
  pin-digests procedure (D5 determinism — no real deploy until pinned).
- preflight.sh: fails closed (non-zero on any required check), bash-3.2 safe,
  composable checks/ (versions,tools,env,docker) + gated (ssh,dns) that WARN-skip
  until the stack is configured.
- env check honors D2 (passphrase presence only, never printed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Niemann 2026-06-30 18:00:26 +02:00
parent 188e30e23e
commit edc708b826
12 changed files with 763 additions and 0 deletions

82
preflight/lib/common.sh Normal file
View file

@ -0,0 +1,82 @@
# shellcheck shell=bash
# -----------------------------------------------------------------------------
# preflight/lib/common.sh — shared helpers for all preflight checks.
# Sourced (not executed). POSIX-friendly, macOS bash 3.2 compatible:
# no associative arrays, no `mapfile`, no `${var^^}`, no process substitution.
# -----------------------------------------------------------------------------
# ANSI colors (disabled if not a tty or NO_COLOR set).
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
PF_RED=$(printf '\033[31m'); PF_GRN=$(printf '\033[32m')
PF_YEL=$(printf '\033[33m'); PF_RST=$(printf '\033[0m')
else
PF_RED=""; PF_GRN=""; PF_YEL=""; PF_RST=""
fi
# Per-process result counters. Each check script reports into these and prints
# its own lines; preflight.sh sums up by parsing the exit code + a summary line.
PF_PASS=0
PF_FAIL=0
PF_WARN=0
pf_pass() { PF_PASS=$((PF_PASS + 1)); printf ' %sPASS%s %s\n' "$PF_GRN" "$PF_RST" "$*"; }
pf_fail() { PF_FAIL=$((PF_FAIL + 1)); printf ' %sFAIL%s %s\n' "$PF_RED" "$PF_RST" "$*"; }
pf_warn() { PF_WARN=$((PF_WARN + 1)); printf ' %sWARN%s %s\n' "$PF_YEL" "$PF_RST" "$*"; }
pf_info() { printf ' %s----%s %s\n' "$PF_YEL" "$PF_RST" "$*"; }
# pf_summary <check-name> : print one machine-parseable line and return an exit
# code: 0 if no failures, 1 if any failure. WARN never fails.
pf_summary() {
printf ' -> %s: %d pass, %d warn, %d fail\n' "$1" "$PF_PASS" "$PF_WARN" "$PF_FAIL"
[ "$PF_FAIL" -eq 0 ]
}
# pf_have <cmd> : true if command exists on PATH.
pf_have() { command -v "$1" >/dev/null 2>&1; }
# pf_vercmp <a> <b> : numeric dotted-version compare. Echoes:
# 0 if a == b, 1 if a > b, 2 if a < b. Missing components treated as 0.
# Non-numeric leading tokens are stripped (e.g. "v3.243.0" -> "3.243.0").
pf_vercmp() {
a=$(printf '%s' "$1" | sed -e 's/^[^0-9]*//' -e 's/[^0-9.].*$//')
b=$(printf '%s' "$2" | sed -e 's/^[^0-9]*//' -e 's/[^0-9.].*$//')
# Normalize to dot-separated fields and compare field by field.
IFS=.
# shellcheck disable=SC2086
set -- $a; a1=${1:-0}; a2=${2:-0}; a3=${3:-0}; a4=${4:-0}
# shellcheck disable=SC2086
set -- $b; b1=${1:-0}; b2=${2:-0}; b3=${3:-0}; b4=${4:-0}
unset IFS
i=1
for pair in "$a1 $b1" "$a2 $b2" "$a3 $b3" "$a4 $b4"; do
# shellcheck disable=SC2086
set -- $pair
av=$(printf '%s' "$1" | sed 's/[^0-9].*$//'); av=${av:-0}
bv=$(printf '%s' "$2" | sed 's/[^0-9].*$//'); bv=${bv:-0}
if [ "$av" -gt "$bv" ] 2>/dev/null; then echo 1; return 0; fi
if [ "$av" -lt "$bv" ] 2>/dev/null; then echo 2; return 0; fi
i=$((i + 1))
done
echo 0
}
# pf_ge <have> <min> : true (0) if version `have` >= `min`.
pf_ge() {
r=$(pf_vercmp "$1" "$2")
[ "$r" = "0" ] || [ "$r" = "1" ]
}
# pf_versions_file : absolute path to the VERSIONS pin-file (repo root).
pf_versions_file() {
# lib/ is preflight/lib, repo root is two levels up.
printf '%s/VERSIONS' "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../.." && pwd)"
}
# pf_versions_get <KEY> : echo the value of KEY from VERSIONS (greppable form).
pf_versions_get() {
vf=$(pf_versions_file)
[ -f "$vf" ] || return 1
line=$(grep "^$1=" "$vf" 2>/dev/null | head -1)
[ -n "$line" ] || return 1
printf '%s' "${line#*=}"
}