Files
home-network-k3s-impl/ci/gate.sh
jakman85andClaude Opus 4.8 fe0ee698c4 fix(apps): reconcile goldens to one canonical render + enforce render==golden gate
The committed apps/ goldens were a MIX of two render flag sets: 8 services
were rendered with `--no-image-automation` (no image CRs) while gitea and
nextcloud carried stale ImageRepository/ImagePolicy CRs from a render WITHOUT
the flag. Neither single command reproduced all goldens, and the gate did not
catch it.

Part A — canonical render:
  python3 catalog/render.py --all catalog -o apps --wave-layout --no-image-automation
`--no-image-automation` is canonical: image automation is authored-but-DISABLED
(DECISIONS-LOG; image-reflector/-automation controllers not installed), and
every app was onboarded via this flag. Re-render drops the ImageRepository +
ImagePolicy objects from gitea + nextcloud only (52 line deletions, purely the
trailing image-automation blocks). No functional content changed — the
nextcloud postStart hardening and gitea OIDC hook are untouched.

Part B — enforcement:
  ci/render-golden.sh renders every onboarded catalog service with the canonical
  command into a temp dir and FAILS on ANY diff vs committed apps/ (missing
  golden or content drift), across all 10 services (not just the 3 L1 spot-checks).
  Not-onboarded catalog entries (keycloak, vm-demo) are excluded per DESIGN-waves.
  Wired as a required no-cluster stage in ci/gate.sh, ci/Makefile, and both the
  live (.gitea/workflows/gate.yml) and dormant (merge-gate.yaml) pipelines.

Proof: tampering a golden makes the stage exit 1 (blocks merge); reverting
restores exit 0. `make -C ci gate` green in the devcontainer.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-28 11:11:52 -05:00

108 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# ci/gate.sh — the merge-gate decision. Runs every stage in order and applies the
# R-TEST-03 rule: a merge is allowed only when the touched-requirement subset is
# GREEN. Honestly distinguishes three outcomes per stage:
# pass (0) green
# fail (1) BLOCKS the merge
# skip (3) not run (cluster-gated stage w/ no cluster, or unwired seam) —
# NOT counted as green (HARD RULE 4: don't fake green)
#
# Phase 0 policy (no cluster): L0/L1 stages (lint, schema, kyverno, L1 render)
# MUST be green. L2/L3 SKIP. If the touched subset needs L2/L3 and no cluster is
# available, the gate reports those requirements UNVERIFIED and — unless
# REQUIRE_CLUSTER_TESTS=true — does not hard-block Phase-0 bootstrap work, while
# making the gap loud. Set REQUIRE_CLUSTER_TESTS=true (Phase 1+) to make any
# skipped, cluster-needing touched requirement a hard block.
set -uo pipefail # NOTE: no -e; we want to run all stages and aggregate.
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${CI_DIR}/lib.sh"
set +e # lib.sh enables -e; the gate must run ALL stages and aggregate, not abort.
: "${REQUIRE_CLUSTER_TESTS:=false}"
declare -A OUTCOME
run_stage() {
local name="$1"; shift
log "==== stage: ${name} ===="
"$@"
local rc=$?
case "$rc" in
0) OUTCOME[$name]="pass"; ok "stage ${name}: pass" ;;
3) OUTCOME[$name]="skip"; skip "stage ${name}: SKIPPED (not run)" ;;
*) OUTCOME[$name]="fail"; err "stage ${name}: FAIL" ;;
esac
}
summary "## Merge gate (R-TEST-03)"
summary ""
run_stage spec-immutability "${CI_DIR}/spec-immutability.sh"
run_stage lint "${CI_DIR}/lint.sh"
run_stage l0 "${CI_DIR}/l0.sh"
run_stage render-golden "${CI_DIR}/render-golden.sh"
run_stage l1 "${CI_DIR}/l1.sh"
run_stage conformance "${CI_DIR}/conformance.sh"
echo >&2
log "==== gate decision ===="
BLOCK=0
# spec-immutability (R-TEST-06): a FAIL always blocks (the spec was modified/drifted).
# A skip means the spec repo wasn't available to verify — surfaced, not green.
case "${OUTCOME[spec-immutability]}" in
fail) err "spec-immutability: FAILED -> blocks merge (R-TEST-06)"; BLOCK=1 ;;
skip) warn "spec-immutability: skipped (spec repo unavailable) — not verified" ;;
pass) ok "spec-immutability: green" ;;
esac
# L0/L1 stages: any failure blocks. A SKIP here means a seam is unwired — in
# Phase 0 that is tolerated (placeholders), but surfaced. render-golden is a
# no-cluster reproducibility gate (canonical render == committed apps/ goldens):
# a FAIL means a golden drifted from its ServiceDefinition source — always blocks.
for s in lint l0 render-golden l1; do
case "${OUTCOME[$s]}" in
fail) err "${s}: FAILED -> blocks merge"; BLOCK=1 ;;
skip) warn "${s}: skipped (unwired seam / missing tool) — not green" ;;
pass) ok "${s}: green" ;;
esac
done
# L2/L3 conformance: cluster-gated.
case "${OUTCOME[conformance]}" in
fail)
err "conformance (L2/L3): FAILED -> blocks merge"; BLOCK=1 ;;
pass)
ok "conformance (L2/L3): green" ;;
skip)
if cluster_available; then
err "conformance: skipped despite CLUSTER_AVAILABLE=true -> blocks merge"; BLOCK=1
else
mapfile -t reqs < <("${CI_DIR}/touched-requirements.sh" || true)
if [ "${#reqs[@]}" -gt 0 ]; then
warn "conformance L2/L3 UNVERIFIED for touched requirements: ${reqs[*]}"
warn " (no cluster — Phase 0). These are NOT certified green."
summary "- **UNVERIFIED (no cluster):** ${reqs[*]}"
if [ "$REQUIRE_CLUSTER_TESTS" = "true" ]; then
err "REQUIRE_CLUSTER_TESTS=true and cluster-needing requirements are unverified -> blocks merge"
BLOCK=1
fi
else
skip "conformance L2/L3 skipped; no cluster-needing requirements touched"
fi
fi ;;
esac
echo >&2
if [ "$BLOCK" -eq 0 ]; then
ok "MERGE GATE: PASS (touched-requirement subset green for the layers that can run)"
summary ""
summary "**Result: PASS** — L0/L1 green. L2/L3 status per cluster availability above."
exit 0
else
err "MERGE GATE: BLOCKED"
summary ""
summary "**Result: BLOCKED** — see failures above."
exit 1
fi