Gitea repo settings live in its own database, not in a manifest Flux reconciles, so they are drift-prone and invisible to the conformance gate -- which is how main came to accept direct pushes while the log recorded it as protected. Adds infra/gitea/branch-protection.sh (blocks direct pushes to main) and a README carrying the measured before-state and the ORDER the remaining steps must happen in. The script deliberately leaves enable_status_check:false and required_approvals:0. Both would deadlock today: there is no runner to satisfy a required context, and the only human account is not yet a repo collaborator, so no one could supply an approval. The README sequences both. It prints a push probe to run afterwards. The API response is not proof -- Gitea may let site admins bypass protection, and that has to be observed, not assumed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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.
Sequence — order matters
- Get an Actions runner registered. It is GitOps-managed
(
apps/wave-1/gitea-actions.yaml,apps/wave-1/gitea-actions-runner-secret.sops.yaml), so it is deployed but not registering — suspect a stale registration token in that SOPS secret. - Watch the gate actually report on a real PR, and note the context name it produced.
- Only then set
enable_status_check: truewith that exact context. - Add the human reviewer as a repo collaborator, then raise
required_approvalsto 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. The same applies to
gate.yml'sconformancejob: it isif: falseand never runs, yet still publishes agate / conformance (pull_request)context. Never list that one as required.