diff --git a/infra/gitea/README.md b/infra/gitea/README.md new file mode 100644 index 0000000..411de5c --- /dev/null +++ b/infra/gitea/README.md @@ -0,0 +1,43 @@ +# 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 + +1. **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. +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. The same applies to +> `gate.yml`'s `conformance` job: it is `if: false` and never runs, yet still publishes a +> `gate / conformance (pull_request)` context. Never list that one as required. diff --git a/infra/gitea/branch-protection.sh b/infra/gitea/branch-protection.sh new file mode 100755 index 0000000..fe0d739 --- /dev/null +++ b/infra/gitea/branch-protection.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Enable branch protection on main for home-network-k3s-impl. +# +# Sets enable_push:false -> no direct pushes to main; changes land only by merging a PR. +# +# enable_status_check is deliberately FALSE. There is currently NO Gitea Actions runner +# registered (verified 2026-09-29: zero at repo, org and site level), so requiring the +# gate context would deadlock EVERY PR permanently -- including the PR that fixes it. +# Turn it on only after a runner is live and you have seen the gate actually report on +# a real PR, using the context name CI itself produced. +# +# required_approvals is 0 for the same class of reason: jakman85 is a site admin but NOT +# a repo collaborator, so requiring an approval now could leave nobody able to approve. +# Add the collaborator first, then raise this to 1. +set -euo pipefail +HOST=gitea.home.keithjackson.dev +OWNER=Home-Network +REPO=home-network-k3s-impl + +creds="$(printf 'protocol=https\nhost=%s\n\n' "$HOST" | git credential fill)" +U="$(sed -n 's/^username=//p' <<<"$creds")" +P="$(sed -n 's/^password=//p' <<<"$creds")" +[ -n "$U" ] && [ -n "$P" ] || { echo "no stored credential for $HOST" >&2; exit 1; } +echo "authenticating as '${U}' (secret not shown)" + +echo "==> creating branch protection on 'main'" +jq -n '{ + branch_name: "main", + enable_push: false, + enable_status_check: false, + status_check_contexts: [], + required_approvals: 0, + block_on_rejected_reviews: true, + block_on_outdated_branch: false, + dismiss_stale_approvals: true +}' | curl -sS --config <(printf 'user = "%s:%s"\n' "$U" "$P") \ + -X POST -H 'Content-Type: application/json' --data @- \ + "https://${HOST}/api/v1/repos/${OWNER}/${REPO}/branch_protections" \ +| jq -r 'if .branch_name then "created" else "API said: \(.message // tostring)" end' + +echo "==> verifying (this is the part that matters)" +curl -sS --config <(printf 'user = "%s:%s"\n' "$U" "$P") \ + "https://${HOST}/api/v1/repos/${OWNER}/${REPO}/branches/main" \ +| jq -r '{protected, user_can_push, required_approvals, enable_status_check, status_check_contexts}' + +cat <<'NOTE' + +Now prove it actually blocks you -- do not take the API's word for it. From a clean +checkout on main, with nothing to lose: + + git commit --allow-empty -m 'protection probe' + git push origin main # EXPECT: rejected by the remote + git reset --hard HEAD~1 # tidy up either way + +If that push SUCCEEDS, Gitea is letting site admins bypass protection and this is not +yet doing what you want. +NOTE