# .gitea/workflows/gate.yml — the ENFORCED PR merge gate (Phase 6, WS-GITOSS). # # This is the LIVE gate wired to the in-cluster Gitea Actions runner (ns gitea-actions, # Docker executor + DinD). It invokes the AGGREGATE merge gate — `./ci/gate.sh`, which is # exactly what `make -C ci gate` runs, so CI and the devcontainer gate the same way # (R-TEST-02): the runner just installs the pinned tools from ci/versions.env first (and see # below for why the script, not `make`, is invoked here). It runs every stage in order — # spec-immutability, lint, Kyverno L0, render==golden, L1 offline render — and then applies the # R-TEST-03 decision, printing # "MERGE GATE: PASS" (0) / "BLOCKED" (1) / "UNDETERMINED" (3) / "NOT APPLICABLE" (0). It is the # single required status check on `main` (see branch protection). A non-zero exit fails the job, # so a BLOCKED gate blocks the merge. # # WHY THIS STEP DECLARES GATE_STRICT_SKIPS AND TOLERATES EXIT 3 (2026-09-13). A SKIPPED stage is # never green, and since 2026-09-13 gate.sh refuses to print PASS when one did not run (it used to # print "— not green" and then "MERGE GATE: PASS", exit 0 — fail-open in the gate itself). Two # kinds of non-run must be told apart HERE, in the caller, because only the caller knows which # environment it is: # * a pinned tool missing or a seam unwired ON THIS RUNNER is a DEFECT — the toolchain is # installed for us two steps up. GATE_STRICT_SKIPS=true DECLARES that (no env sniffing, no # "does this look like CI?" heuristic) and makes those skips exit 1: untolerated, job fails. # * spec-immutability CANNOT be verified here at all: the spec repo is a separate repo and is # not checked out into this runner (SPEC_REPO_SLUG unwired; blueprint in merge-gate.yaml). It # is the ONLY skip that can still reach exit 3 under strict skips, which is what makes # `|| [ "$?" -eq 3 ]` a narrow, reviewed tolerance rather than a blanket one. The log still # says UNDETERMINED and names R-TEST-06 as unverified. Wire the spec checkout (or set # REQUIRE_SPEC_VERIFICATION=true) and this tolerance should be deleted. # Called DIRECTLY, not through `make`: GNU make reports any recipe failure as its own exit 2, so # `make -C ci gate || [ "$?" -eq 3 ]` would see 2 and fail the job. Same reason the # spec-immutability pre-step below calls the script directly. # # The cluster-gated L2/L3 conformance stage SKIPS honestly here (exit 3, surfaced not faked # — HARD RULE 4) because this runner declares no cluster: nothing here sets CLUSTER_AVAILABLE, # so gate.sh reports the touched requirements UNVERIFIED — and NAMES that declared gap in its # PASS headline — without blocking Phase-0/6 static work. L2/L3 run as an IN-CLUSTER Chainsaw Job (its own scoped SA; CI holds # no broad kubeconfig) driven by the separate `conformance` job below. # # NOTE (R-TEST-06 / gate-semantics): spec-immutability is ALSO surfaced as its own explicit # early step (below) so its result is a distinct, tolerant status in the Gitea UI; gate.sh # re-runs it as its first stage, which is the authoritative one. The gate step below is the # same script the laptop runs; the extra pre-step is a read-only git-diff check and is # purely additive. The full pipeline blueprint (spec-checkout, per-layer jobs, decision job) # is retained dormant in merge-gate.yaml. --- name: gate # TRIGGERS — deliberately only `pull_request` into main, plus `push` to main itself. # # There is NO per-branch push trigger. There used to be a prefix list (feature/** fix/** # docs/** chore/**) for "fast feedback on the branch", and it was a net negative here: # # * `act_runner` runs at capacity 1. A branch push and its PR fire the SAME gate over the # SAME tree, and the redundant run SERIALISES AHEAD of the PR run — doubling the wall # clock on the one result that actually gates the merge. That cost real time during the # 2026-09-29 plex claim, where the loop is racing a short-lived token. # * The list silently did not match this repo's own convention. Branches here are `feat/**`, # which was absent, so the "fast feedback" never fired for the most common prefix anyway — # a mismatch nobody noticed for a month because its only symptom was a MISSING run. # Any hand-maintained prefix list has that failure mode; deleting it removes the class. # * It is redundant. CLAUDE.md already requires `./ci/gate.sh` in the pinned toolbox before # you push (74s locally), and the PR gate re-runs it authoritatively. A third run of the # same checks buys nothing. # # `push: [main]` is KEPT: post-merge main must still be verified, and it is the only run for # which no PR context exists. on: pull_request: branches: [main] push: branches: - main concurrency: group: gate-${{ github.ref }} cancel-in-progress: true jobs: # Single required status check: the static merge gate. gate: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 # full history so ci/touched-requirements.sh can diff vs main - name: Install pinned CI toolchain (ci/versions.env) run: | ./.gitea/workflows/scripts/install-tools.sh l0 ./.gitea/workflows/scripts/install-tools.sh l1 # R-TEST-06: the spec is read-only to agents. An EXPLICIT early surfacing of the # spec-immutability check, so its result is a distinct, tolerant status line in the # Gitea UI before the (longer) aggregate gate log. The spec repo is NOT checked out # into CI here (separate repo; SPEC_REPO_SLUG not wired into this runner), so the guard # honestly SKIPS (exit 3) rather than faking green; a real drift/edit of a co-located # spec exits 1 and fails. Tolerated: exit 3 only. Call the script DIRECTLY, not via # `make spec-immutability`: GNU make masks a recipe's exit code as its own 2, so # `make spec-immutability || [ $? -eq 3 ]` would see 2 and fail. The AUTHORITATIVE # spec-immutability run is the FIRST stage of `make -C ci gate` below (gate.sh calls # the script directly, so it sees exit 3 → SKIP, not a mask); this pre-step is a cheap, # read-only git-diff duplicate that only adds early UI signal. - name: Spec immutability (R-TEST-06, skips if spec repo absent) run: ./ci/spec-immutability.sh || [ "$?" -eq 3 ] # Aggregate merge gate: the same script a human runs in the devcontainer via # `make -C ci gate`. Runs spec-immutability, lint, Kyverno L0, render==golden and L1 # render, then the R-TEST-03 decision. Exits non-zero on a BLOCKED gate, which fails this # job. The cluster-gated L2/L3 stage SKIPS here (no CLUSTER_AVAILABLE) — surfaced as # UNVERIFIED and NAMED in the PASS headline, never faked green. GATE_STRICT_SKIPS + the # tolerated exit 3 are explained in the file header; the tolerance covers exactly one # thing, the unverifiable spec repo, and nothing else can produce a 3 here. - name: Merge gate — ci/gate.sh (what `make -C ci gate` runs) env: GATE_STRICT_SKIPS: "true" run: ./ci/gate.sh || [ "$?" -eq 3 ] # NOTE: no actions/upload-artifact step. Gitea Actions rejects upload-artifact@v4 # ("not supported on GHES" — Gitea does not implement the artifact-v2 backend), which # would fail the job even on a green gate. JUnit + coverage are written under # conformance/_artifacts/ in the job workspace; wire artifact upload later via the # Gitea-compatible @v3 action or Gitea's own artifact backend if retention is wanted. # ─────────────────────────────────────────────────────────────────────────── # PLACEHOLDER — NOT WIRED (deferred; concrete plan written, awaiting operator decision). # L2/L3 conformance (Chainsaw behavioural + fault-injection) needs a LIVE cluster and is # gated behind vars.CLUSTER_AVAILABLE + a narrow secrets.CONFORMANCE_TRIGGER_KUBECONFIG. # The intended model: an in-cluster Chainsaw Job (conformance/incluster/) under its OWN # scoped ServiceAccount — CI holds NO broad kubeconfig, only a trigger credential. The # blocker is a real privilege-escalation design question (a `create jobs` credential can # create a Job running as the privileged runner SA), plus a runner image + repo-clone + # results-collection that don't exist yet. Full analysis + step sequence: # docs/plans/l2-l3-conformance-ci-wiring.md # The blueprint job shape is retained dormant in merge-gate.yaml. # Kept `if: false` so it never runs (shows skipped, never fakes green — HARD RULE 4). # ─────────────────────────────────────────────────────────────────────────── conformance: runs-on: ubuntu-latest needs: [gate] if: false steps: - name: Placeholder run: | echo "L2/L3 conformance not wired — see gate.yml header + WS-GITOSS report." >&2 exit 1