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]>
83 lines
4.2 KiB
Markdown
83 lines
4.2 KiB
Markdown
# 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.
|