Files
home-network-k3s-impl/.gitea/workflows/merge-gate.yaml
T
jakman85andClaude Opus 5 e72050c690
gate / gate (push) Successful in 1m37s
gate / gate (pull_request) Successful in 1m36s
gate / conformance (push) Has been skipped
gate / conformance (pull_request) Has been skipped
docs: correct CI/platform documentation that was stale, wrong, or load-bearing
The 2026-09-29 session found the merge gate did not exist -- no branch protection, no
runner -- while the tree asserted both. This corrects the documents that carried, and in
several cases CAUSED, that belief.

CLAUDE.md gains section 2b: never infer platform state from this repository. It carries
the specific API and kubectl checks for protection, runners, CI status, workloads and
Flux revisions, and the rule that a green tick is not proof anything ran (a skipped Gitea
job reports success). This is the rule whose absence produced every other finding.

.gitea/workflows/README.md and merge-gate.yaml both said "there is no remote git host,
this file is dormant". Gitea had been live for weeks. Both were written as plans and never
revisited once the plan completed. The Transition section is now marked COMPLETE and says
what is genuinely still outstanding (CLUSTER_AVAILABLE unset, so L2/L3 never runs in CI).

DECISIONS-LOG 2026-08-31 is annotated IN PLACE. Anyone reading chronologically previously
hit "required status check; runner incluster-dind online" with nothing marking it false
until a month later. The correction now sits on the bullet itself.

ci/README.md, clusters/README.md and both conformance READMEs carry a status banner: they
speak as though no cluster exists, which makes a live L2/L3 coverage gap read as an
expected phase. That framing is why the gap went unexamined.

apps/wave-1/gitea-actions-runner-secret.sops.yaml: fixed a wrong path, replaced advice to
`sops -d` into a terminal (which CLAUDE.md forbids) with the rotation script, and recorded
that a fresh token alone does not re-register a runner holding a stale /data/.runner.

apps/wave-0/plex-claim-secret.sops.yaml: the header claimed the file ships the placeholder.
It has not since the claim -- it holds the spent token, which is inert and deliberately
left in place.

catalog/plex.yaml and the plex secret both asserted a "~4-minute TTL" as fact. It was
uncited, upstream documents no expiry, and it was the basis for a since-disproven claim
that the pipeline was too slow to fit inside it. Measured: ~2m45s.

Both .sops.yaml files were verified to still decrypt after the comment edits
(`sops -d > /dev/null`), and render==golden confirms the catalog comment does not
propagate into the rendered manifest.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-29 02:14:26 -05:00

171 lines
7.5 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]
#
# STATUS (corrected 2026-09-29): Gitea IS hosted and Gitea Actions IS enabled; the runner
# `incluster-dind` is online and gate.yml is the live PR gate. The text that used to sit here
# ("Today there is no Gitea host, so this file is dormant") was stale by weeks -- it was written
# as a plan and never revisited once the plan completed.
#
# THIS file is still dormant, but for the reason in the DORMANT block below -- its spec checkout
# and L2/L3 seams are unwired -- NOT because there is no host. Do not read a stale plan as a
# statement about the live platform; query the API instead (see infra/gitea/).
#
# 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