# infra/gitea — platform settings that are NOT GitOps-managed Gitea's own repo settings (branch protection, required status checks, approvals) live in Gitea's database, not in a manifest Flux reconciles. They are therefore **drift-prone and invisible to the gate** — exactly how the state below went unnoticed. ## The gap this closes Verified against the live API on 2026-09-29, against a `DECISIONS-LOG.md` entry (2026-08-31) asserting the opposite: | Claimed | Measured | |---|---| | `main` is a protected branch, gate is a required status check | `branch_protections` → `[]`; `protected:false`, `enable_status_check:false`, `required_approvals:0`, `user_can_push:true` | | Runner `incluster-dind` online | **zero** runners registered at repo, org and site level | So `main` accepted direct pushes, and the gate could not run at all. `CLAUDE.md` is the rule; this directory is how the rule gets enforced by the machine. ## `branch-protection.sh` Blocks direct pushes to `main`. Run it, then run the push probe it prints — **verify, do not trust the API response**, since Gitea may let site admins bypass protection. ./infra/gitea/branch-protection.sh It authenticates with the credential `git` already has stored for this host, and never prints the secret. ## `runner-token-rotate.sh` Mints a fresh instance-level registration token from the Gitea API and writes it into `apps/wave-1/gitea-actions-runner-secret.sops.yaml` via `sops set`. **The token value is never printed.** ./infra/gitea/runner-token-rotate.sh A fresh token is usually **not sufficient on its own**. `act_runner` registers exactly once and persists its identity in `/data/.runner` on the `act-runner-data` PVC. If Gitea no longer holds a matching runner row, the pod starts, sees `.runner`, **skips registration**, and polls with an identity Gitea does not recognise — so it never appears in the runner list. That is precisely the observed symptom: zero runners, pod running, no error anyone looks at. Clearing the PVC is what forces a real re-registration; the script prints the commands. ## Sequence — order matters 1. **Get an Actions runner registered** — `runner-token-rotate.sh`, then clear the PVC so the runner actually re-registers, then confirm it shows `online` in the API. Do not skip the confirmation: the whole reason this went unnoticed is that a runner was *recorded* as online without anyone checking. 2. **Watch the gate actually report on a real PR**, and note the context name it produced. 3. **Only then set `enable_status_check: true`** with that exact context. 4. Add the human reviewer as a repo collaborator, then raise `required_approvals` to 1. > **Do not do step 3 before step 1.** A required status check with no runner leaves every > PR permanently pending — including the PR that would fix it. That half is measured: PR #1 > sat `pending` for 30.6 minutes across 122 polls with no runner registered, and never > reported. ### Measured, once the runner came up (2026-09-29) The first CI jobs this repo ever ran, on PR #1 and PR #2: | Context | Result | Time | |---|---|---| | `gate / gate (pull_request)` | success | 1m58s (PR #1), 1m34s (PR #2) | | `gate / gate (push)` | success | 1m32s (PR #2 only) | | `gate / conformance (pull_request)` | **success — "Has been skipped"** | — | **Require `gate / gate (pull_request)`. Do NOT require either `conformance` context — but not for the reason an earlier draft of this file gave.** A skipped Gitea Actions job does not hang a status check: Gitea reports it as **`success`** with the description "Has been skipped". So requiring it would not deadlock anything — it would pass *vacuously*, every time, forever, while nothing ran. Under HARD RULE 4 that is worse than a deadlock: a deadlock is loud, a permanently-green check that verifies nothing is exactly the fail-open shape this repo keeps finding. The `(push)` contexts appear only for branches matching `gate.yml`'s push trigger (`feature/**`, `fix/**`, `docs/**`, `chore/**`). `feat/**` is **not** in that list, which is why PR #1's `feat/` branch produced only `pull_request` contexts. Require only the `pull_request` context, or PRs from non-matching prefixes will never satisfy the rule.