Files
home-network-k3s-impl/.gitea/workflows/merge-gate.yaml
T
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

168 lines
7.3 KiB
YAML

# .gitea/workflows/merge-gate.yaml
# CI merge gate (T-0.2, R-GITOPS-01, R-TEST-02/03) for the home k3s + Longhorn
# platform. Enforces docs/14 (Testing & Conformance) gating rules as Gitea Actions.
#
# Pipeline (docs/14 "Where each layer runs"):
# lint -> schema -> kyverno-L0 -> render==golden -> L1-render [no cluster; live now]
# |
# v
# conformance-L2-L3 (in-cluster Chainsaw Job)
# | [cluster-gated; SKIPS in Phase 0]
# v
# merge-gate decision [R-TEST-03: touched subset green]
#
# Transition (CONTRIBUTING.md / docs/05 §"local bootstrap -> self-hosted Gitea"):
# Today there is no Gitea host, so this file is dormant. The SAME gate is runnable
# now via `make -C ci gate` (R-TEST-02). Once Gitea is self-hosted and Gitea
# Actions is enabled, this workflow becomes the ENFORCED PR merge gate into main —
# no code change required, only turning on the runner and setting CLUSTER_AVAILABLE.
#
# CI holds NO broad kubeconfig. L2/L3 run as an in-cluster Chainsaw Job whose own
# scoped ServiceAccount holds cluster access (docs/14, R-SEC-02); CI uses only a
# narrow trigger credential referenced BY SECRET NAME below.
#
# ─────────────────────────────────────────────────────────────────────────────
# DORMANT (Phase 6, WS-GITOSS): the LIVE enforced gate is now gate.yml. This file is
# retained as the full-pipeline BLUEPRINT (spec-checkout via SPEC_REPO_SLUG + the L2/L3
# conformance job + the R-TEST-03 decision job) for when those seams are wired:
# - SPEC_REPO_SLUG must be set as a Gitea repo/org variable AND the spec repo hosted on
# this Gitea, or spec-immutability's second checkout fails the whole run.
# - vars.CLUSTER_AVAILABLE + a scoped conformance trigger credential must exist.
# Until then it must NOT auto-run (its spec checkout would fail with SPEC_REPO_SLUG unset),
# so its triggers are reduced to manual `workflow_dispatch`. Re-enable by restoring the
# pull_request/push triggers once the seams above are wired.
# ─────────────────────────────────────────────────────────────────────────────
---
name: merge-gate
on:
workflow_dispatch: {} # DORMANT blueprint — manual-only; gate.yml is the live gate
concurrency:
group: merge-gate-${{ github.ref }}
cancel-in-progress: true
# Repo/org variables & secrets (set in Gitea, referenced by NAME only — no plaintext):
# vars.CLUSTER_AVAILABLE 'true' once a dev/test cluster exists (Phase 1+); else L2/L3 SKIP
# secrets.CONFORMANCE_TRIGGER_KUBECONFIG narrow creds to CREATE/AWAIT the in-cluster Chainsaw Job only
env:
CLUSTER_AVAILABLE: ${{ vars.CLUSTER_AVAILABLE }}
jobs:
# R-TEST-06 hard gate: the specification is read-only to agents. Fail the whole run
# if the spec repo is modified or has drifted from ci/SPEC_PIN. Runs first; everything
# else depends on it. Requires the spec repo checked out beside the impl repo — wired
# for when Gitea hosts both. If the spec repo is unavailable the guard SKIPS (exit 3),
# tolerated here (surfaced, never a fake green); a real violation (exit 1) blocks.
spec-immutability:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # the implementation repo
- name: Check out the read-only spec (sibling path)
uses: actions/checkout@v4
with:
repository: ${{ vars.SPEC_REPO_SLUG }} # e.g. Home-Network/home-network-k3s (set in Gitea)
path: ../home-network-k3s
fetch-depth: 0
- name: Verify spec immutability (R-TEST-06)
run: make -C ci spec-immutability || [ $? -eq 3 ]
lint:
runs-on: ubuntu-latest
needs: [spec-immutability]
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # full history so touched-requirement diff works
- name: Install pinned tools
run: ./.gitea/workflows/scripts/install-tools.sh lint
- name: yaml/lint
run: make -C ci lint
l0:
runs-on: ubuntu-latest
needs: [lint]
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Install pinned tools
run: ./.gitea/workflows/scripts/install-tools.sh l0
- name: Schema validation + Kyverno L0
run: make -C ci l0
render-golden:
runs-on: ubuntu-latest
needs: [l0]
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Install pinned tools
run: ./.gitea/workflows/scripts/install-tools.sh l1
- name: render==golden reproducibility (canonical render vs committed apps/)
run: make -C ci render-golden
l1:
runs-on: ubuntu-latest
needs: [render-golden]
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Install pinned tools
run: ./.gitea/workflows/scripts/install-tools.sh l1
- name: L1 rendering checks
run: make -C ci l1
- name: Upload L1 artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: l1-junit-coverage
path: |
results/junit/*.xml
reports/coverage/**
if-no-files-found: ignore
# L2/L3 behavioral + fault-injection. Runs ONLY when a cluster is declared.
# In Phase 0 (CLUSTER_AVAILABLE unset) this job is SKIPPED by Gitea — shown as
# skipped, never as passed. It never fakes green (HARD RULE 4).
conformance:
runs-on: ubuntu-latest
needs: [l1]
if: ${{ vars.CLUSTER_AVAILABLE == 'true' }}
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Install pinned tools
run: ./.gitea/workflows/scripts/install-tools.sh conformance
- name: Trigger in-cluster Chainsaw Job (L2/L3) and collect artifacts
env:
# Narrow trigger credential; the Job's scoped SA holds real cluster access.
CONFORMANCE_TRIGGER_KUBECONFIG: ${{ secrets.CONFORMANCE_TRIGGER_KUBECONFIG }}
run: make -C ci conformance
- name: Upload L2/L3 artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: conformance-junit-coverage
path: |
results/junit/*.xml
reports/coverage/**
if-no-files-found: ignore
# Final decision. Applies R-TEST-03 honestly over the upstream job results:
# any L0/L1 failure -> BLOCK
# conformance failed -> BLOCK
# conformance skipped + cluster available -> BLOCK (should have run)
# conformance skipped + no cluster -> report UNVERIFIED (Phase 0), do not fake green
merge-gate:
runs-on: ubuntu-latest
needs: [lint, l0, render-golden, l1, conformance]
if: always()
steps:
- uses: actions/checkout@v4
- name: Decide
env:
R_LINT: ${{ needs.lint.result }}
R_L0: ${{ needs.l0.result }}
R_L1: ${{ needs.l1.result }}
R_CONF: ${{ needs.conformance.result }}
run: ./.gitea/workflows/scripts/decide.sh