Files
home-network-k3s-impl/.gitea/workflows/README.md
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

116 lines
6.7 KiB
Markdown

# CI merge gate — `.gitea/workflows/`
**Task T-0.2 · Requirements R-GITOPS-01, R-TEST-02, R-TEST-03 · Owner WS-GITOPS (CI).**
This is the **merge gate** for the home k3s + Longhorn platform. It enforces the gating rules in the
spec ([`docs/14-testing-and-conformance.md`](../../../home-network-k3s/docs/14-testing-and-conformance.md),
[ADR-0010](../../../home-network-k3s/decisions/adr-0010-conformance-testing.md),
[ADR-0011](../../../home-network-k3s/decisions/adr-0011-test-tooling-kyverno-chainsaw.md)) as a
Gitea Actions pipeline: **no change lands on `main` unless the conformance subset for the requirements
it touches is green** (R-TEST-03).
> **Done is a green, reproducible, requirement-tagged test — never "merged," never a demo.** This gate
> is the machine that enforces it.
## The pipeline
```
lint ─▶ l0 (schema + Kyverno) ─▶ render-golden ─▶ l1 (rendering) ─▶ conformance (L2/L3) ─▶ merge-gate decision
└──────────────── run in CI, NO cluster ────────────────┘ └── in-cluster Chainsaw Job ──┘
live now cluster-gated (SKIPS in Phase 0)
```
| Stage | Job | Layer | Verifies | Cluster? |
|---|---|---|---|---|
| yaml/lint | `lint` | — | YAML well-formed (`ci/yamllint.yaml`) | no — **live now** |
| schema | `l0` | L0 | ServiceDefinitions valid against `schemas/` | no — **live now** |
| Kyverno L0 | `l0` | L0 | `kyverno test platform/kyverno/`: default-deny baseline present, no plaintext secrets, required labels/egress-class, schema conformance | no — **live now** |
| L1 rendering | `l1` | L1 | ServiceDefinition → rendered resources match golden expectations (Chainsaw L1 via `conformance/`) | no — **live now** |
| conformance | `conformance` | L2+L3 | Real runtime behavior + fault-injection, as an **in-cluster Chainsaw Job** CI triggers and awaits | **yes — cluster-gated** |
| decision | `merge-gate` | — | Applies R-TEST-03 over the above; blocks merge on any failure | no |
Each stage is a thin wrapper over a `ci/*.sh` script, so **CI and a laptop run the identical gate**
(R-TEST-02). See [`../../ci/README.md`](../../ci/README.md).
### Why L2/L3 is an in-cluster Job (not run by CI directly)
Per [`docs/14` §"Where each layer runs"](../../../home-network-k3s/docs/14-testing-and-conformance.md)
and ADR-0011: **CI holds no broad kubeconfig.** L2/L3 run as an in-cluster **Chainsaw Job** whose own
**scoped test ServiceAccount** holds cluster access (R-SEC-02). CI only *triggers* the Job and
*collects* its artifacts, using a narrow trigger credential referenced **by secret name only**
(`secrets.CONFORMANCE_TRIGGER_KUBECONFIG`) — never a plaintext secret, never a cluster-admin kubeconfig.
## Honest SKIP — never fake green (Phase 0)
**No cluster exists in Phase 0.** The gate is deliberately built so unavailable checks **SKIP loudly,
they do not pass**:
- The `conformance` job is `if: ${{ vars.CLUSTER_AVAILABLE == 'true' }}` — with no cluster it is
**skipped** by Gitea (shown as skipped, not green). The `merge-gate` decision then reports any
touched requirement that needs L2/L3 as **UNVERIFIED**, not satisfied.
- Stages whose owning subtree is still a placeholder (`schemas/`, `platform/kyverno/`, `conformance/`)
report **SKIPPED — unwired**, distinct from a pass, until those seams are delivered.
The decision matrix (`scripts/decide.sh`):
| Situation | Outcome |
|---|---|
| Any of lint/l0/l1 **fails** | **BLOCK** |
| conformance **fails/cancelled** | **BLOCK** |
| conformance **skipped** while `CLUSTER_AVAILABLE=true` | **BLOCK** (it should have run) |
| conformance **skipped**, no cluster, touched reqs need L2/L3 | **UNVERIFIED** — reported, not green; blocks only if `REQUIRE_CLUSTER_TESTS=true` |
| lint/l0/l1 green, no cluster-needing reqs touched | **PASS** |
`REQUIRE_CLUSTER_TESTS` is `false` in Phase 0 (bootstrap: seams and cluster don't exist yet) and flips
to `true` from Phase 1 once a dev/test cluster is declared — from then on, any touched requirement that
can't be verified is a hard block.
## Artifacts (R-TEST-04 traceability)
Every layer emits machine-readable output collected by the `merge-gate`:
- **JUnit XML** → `results/junit/*.xml` (uploaded as `l1-junit-coverage` / `conformance-junit-coverage`).
- **Requirement-coverage report** → `reports/coverage/**` — maps every `R-*` to its verifying test(s);
uncovered requirements are visible gaps. Produced by the conformance runner (`conformance/`, WS-TEST).
Both dirs are git-ignored (see [`.gitignore`](../../.gitignore)).
## Triggers & branch model (CONTRIBUTING.md)
- `pull_request` → `main`: **PRs are the enforced merge gate into `main`.**
- `push` → `feature/**`, `fix/**`, `docs/**`, `chore/**`: fast feedback on the branch. **No work on `main`.**
## Transition: generic-CI-now → Gitea-Actions-after-Gitea-is-up
Per [`CONTRIBUTING.md`](../../../home-network-k3s/CONTRIBUTING.md) and
[`docs/05` §"local bootstrap → self-hosted Gitea"](../../../home-network-k3s/docs/05-gitops-and-delivery.md):
1. **Now (interim):** there is **no remote git host** on this network. This workflow file is dormant —
but the **identical gate runs locally today** via `make -C ci gate` (R-TEST-02). Development is
local feature-branch → `main`, the gate enforced by convention + the local runner.
2. **Gitea comes online early** (one of the first services onboarded) at `gitea.home.keithjackson.dev`.
3. **CI moves to Gitea Actions:** this workflow activates unchanged. The only operational steps are
enabling a Gitea Actions runner and setting `vars.CLUSTER_AVAILABLE=true` +
`secrets.CONFORMANCE_TRIGGER_KUBECONFIG` once a dev/test cluster exists. PRs then become the
**platform-enforced** merge gate — the same no-work-in-main rule, now enforced by machine.
## Configuration (set in Gitea; referenced by name only)
| Kind | Name | Purpose |
|---|---|---|
| variable | `CLUSTER_AVAILABLE` | `'true'` once a dev/test cluster exists → L2/L3 run and are required; else they SKIP |
| variable | `REQUIRE_CLUSTER_TESTS` | `'true'` (Phase 1+) makes unverified cluster-needing touched reqs a hard block |
| secret | `CONFORMANCE_TRIGGER_KUBECONFIG` | narrow creds to create/await the in-cluster Chainsaw Job (not cluster-admin) |
## Files
```
.gitea/workflows/
merge-gate.yaml the pipeline (lint → l0 → l1 → conformance → decision)
scripts/
install-tools.sh installs the pinned tools per stage (from ci/versions.env)
decide.sh the R-TEST-03 merge decision over job results
README.md this file
```
The stage logic itself lives in [`../../ci/`](../../ci/) so it is reusable locally and in CI.