The runner came up on 2026-09-29 and the first CI jobs this repo has ever run went green, which falsified a claim merged in #2. MEASURED: 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" #2 said listing the conformance context as a required check would deadlock every PR, because the job is `if: false` and never runs. That is wrong. Gitea reports a skipped job as SUCCESS. Requiring it would not deadlock anything -- it would pass VACUOUSLY, forever, while nothing ran. The conclusion is unchanged (never require it) but the reason inverts, and the corrected reason matters more: under HARD RULE 4 a permanently-green check that verifies nothing is worse than a deadlock. A deadlock is loud. This is the exact fail-open shape the 2026-09-18 audit and this one both keep finding. Also records that the (push) contexts only appear for branches matching gate.yml's push trigger -- feature/**, fix/**, docs/**, chore/** -- and NOT feat/**, which is why PR 1's feat/ branch produced only pull_request contexts. Require the pull_request context alone, or PRs from non-matching prefixes never satisfy the rule. CLAUDE.md's enforcement table is updated: the runner half is genuinely fixed and observed green, the branch-protection half is not. Adds the warning that a green tick is not by itself evidence that anything ran. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
4.2 KiB
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
- Get an Actions runner registered —
runner-token-rotate.sh, then clear the PVC so the runner actually re-registers, then confirm it showsonlinein the API. Do not skip the confirmation: the whole reason this went unnoticed is that a runner was recorded as online without anyone checking. - 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. That half is measured: PR #1 sat
pendingfor 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.