Files
home-network-k3s-impl/infra/gitea
jakman85andClaude Opus 5 b2d9883958
gate / gate (pull_request) Successful in 2m2s
gate / conformance (pull_request) Has been skipped
docs(ci): close out the hardening -- land the scripts, correct a claim that shipped wrong
The branch-protection + PR-gate + sidecar sequence is complete and verified. This records
the finished state and fixes what the sequence itself proved wrong.

CORRECTS A FALSE CLAIM I SHIPPED. PR #7 said the sidecar conversion would remove the
one-per-start dockerd line "http: TLS handshake error from 127.0.0.1: EOF". It does NOT.
Exactly one still appears, immediately after "API listen on [::]:2376" -- it is now the
startupProbe's FIRST attempt, landing microseconds after dockerd begins listening and cut
off by timeoutSeconds: 1; the next poll succeeds. Cosmetic, and deliberately not chased:
raising the timeout to hide one benign startup line would slow every subsequent poll. The
manifest comment now states this instead of the wrong prediction.

Lands infra/gitea/require-gate.sh and verify-then-enforce.sh, which drove the rollout from
a session temp dir. verify-then-enforce.sh merges nothing -- merging is a human's reviewed
act -- but forces the reconcile and ABORTS unless the pod is healthy, a runner is online,
and a real workflow goes green on it, before enabling enforcement and probing the push.
Re-run it after any future act-runner change.

Trims the now-obsolete feat/ rationale from claim-ship.sh: per-branch push triggers were
deleted, so no prefix produces a duplicate run any more.

CLAUDE.md's enforcement table flips from "weaker than this rule" to machine-enforced, with
all four rows marked verified and the push rejection noted as PROVOKED rather than assumed.
It also now states what a green PR does NOT prove: CLUSTER_AVAILABLE is unset, so L2/L3
never runs in CI and only the L0/L1 offline subset is covered.

infra/gitea/README.md marks the sequence done and names the root cause that remains
unfixed: Gitea repo settings live in its own database, invisible to Flux and unreadable by
the conformance gate, so NOTHING detects drift from what this directory declares. These
scripts assert desired state; they are not a detector. The check that reads the live API
and fails on divergence is the highest-value work left, and it is the difference between
this being fixed and this being unable to recur.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-29 02:28:55 -05:00
..

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 — DONE 2026-09-29

All four steps are complete and each was verified, not assumed:

Step State
1. Runner registered ✅ incluster-dind online. Root cause of its absence was a stale /data/.runner identity; clearing the PVC forced re-registration
2. Gate observed green on a real PR ✅ gate / gate (pull_request) ~1m35s
3. enable_status_check: true ✅ context gate / gate (pull_request) — that one ONLY
4. Collaborator + approvals ✅ jakman85 is owner; required_approvals: 1

Direct push to main was provoked and rejected (pre-receive hook declined) as gitea-admin, a site admin — so protection is not merely declared, it holds for the account agents authenticate as.

require-gate.sh / verify-then-enforce.sh

verify-then-enforce.sh is the re-runnable form of the whole safe order. It merges nothing — merging is a human's reviewed act — but it forces the Flux reconcile and then aborts unless the runner pod is healthy, a runner is online, and a real workflow goes green on it, before enabling enforcement and probing the push. require-gate.sh is the enforcement step alone.

Re-run verify-then-enforce.sh after any future change to the act-runner Deployment.

Why these scripts exist at all, and what is still missing

Gitea's repo settings live in its own database, not in anything Flux reconciles or the conformance gate can read. Nothing detects drift from what this directory declares. That is the root cause of the entire 2026-09-29 session: branch protection was off and the runner was gone for a month while the log recorded both as working, and no check existed that could have caught it.

These scripts encode the desired state and can re-assert it, but they are not a detector. The durable fix is a conformance check that reads the live Gitea API and fails when it diverges from what is declared here — turning a class of silent, invisible failure into a loud one. That check does not exist yet and is the highest-value remaining work in this directory.

Historical sequence (kept for context)

Original ordering notes

  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.