#!/usr/bin/env bash # Let the sole administrator merge their own green PR: required_approvals 1 -> 0 on `main`. # # WHY. `required_approvals: 1` plus Gitea's refusal to let an author approve their own pull # request plus exactly ONE collaborator (`jakman85`, permission `owner`) is a lock-out, not a # policy. Every PR the operator opened was permanently unmergeable, including the PR that would # fix it. Measured 2026-10-01: PR #35 — `gate / gate (pull_request)` success in 1m41s, zero # reviews, `required_approvals: 1`, no account in existence able to satisfy it. # # The approvals field was also never what the design said it was. `ci/approval-policy.sh`'s # header and `ci/gate.sh:194` both already described the model as "required_approvals: 0 and the # GATE decides", and `require-gate.sh`'s own closing note named this exact failure and this exact # remedy. decisions/2026-09-30-flux-deploy-notification-inhibited.md flagged the 1-vs-0 # disagreement as unresolved. This resolves it at 0. # # WHAT THIS DOES *NOT* DO, and the distinction is the whole point. # # * It does NOT touch `block_admin_merge_override`, which stays TRUE. The required status check # still binds ABSOLUTELY — a red gate is unmergeable by everyone, administrators included. # Setting `block_admin_merge_override: false` would have been the other way to unblock an # owner, and it would have re-opened precisely what block-admin-override.sh was written to # close: PRs #5-#9 merged with zero approvals while the settings read as enforced. # * It does NOT stop reviewing security-relevant changes. Those are gated by # ci/approval-policy.sh against infra/gitea/approval-policy.json `sensitive_paths`, which an # administrator can waive only with an explicit, attributed `/approve-override ` # comment on the PR. # # ORDER MATTERS — READ THIS BEFORE RUNNING IT. # # ci/gitea-settings.sh fails the merge gate on ANY divergence between the live rule and # infra/gitea/expected-branch-protection.json, and `gate.yml` sets REQUIRE_GITEA_SETTINGS=true. # The declaration in git now says 0. So between running this and merging that declaration, the # live value and the declared value must be compared from a branch that CARRIES the new # declaration — otherwise the stage goes red, correctly, for drift. # # 1. MERGE any other open PRs FIRST, while approvals still work for them (their branches # declare 1, and a PR you did not author can still be approved normally). Skipping this # only costs a rebase later; it does not break anything. # 2. Run this script. # 3. Re-run the gate on the branch that carries the new declaration. `gitea-settings` goes # green because live 0 == declared 0. # 4. Merge that branch. # 5. Any PR still open from before step 2 now declares 1 against a live 0 and will be RED # until it is rebased onto the new main. That is the detector doing its job, not a fault. # # Recoverable: `required_approvals` can be PATCHed back to 1 by an owner at any time. If you do, # change expected-branch-protection.json in the same breath or every PR goes red on drift. set -euo pipefail HOST=gitea.home.keithjackson.dev; OWNER=Home-Network; REPO=home-network-k3s-impl DECL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/expected-branch-protection.json" command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; } [ -f "$DECL" ] || { echo "declaration missing: ${DECL}" >&2; exit 1; } # Take the target from the DECLARATION, not from a literal here. If the two could disagree, # this script would be one more thing re-asserting a value nothing checks — which is the # failure mode the whole of infra/gitea/ exists to stop. WANT="$(jq -r '.protection.required_approvals' "$DECL")" [[ "$WANT" =~ ^[0-9]+$ ]] || { echo "declaration has no numeric required_approvals" >&2; exit 1; } 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)" api() { curl -sS --config <(printf 'user = "%s:%s"\n' "$U" "$P") "$@"; } B="https://${HOST}/api/v1/repos/${OWNER}/${REPO}/branch_protections/main" # Read the RULE OBJECT, never /branches/main: the branch view does not expose # block_admin_merge_override and showed all-green for hours while the rule was overridable. echo "==> before" api "$B" | jq -r '{required_approvals, block_admin_merge_override, enable_status_check, status_check_contexts}' echo "==> setting required_approvals = ${WANT} (declared in expected-branch-protection.json)" api -X PATCH -H 'Content-Type: application/json' \ -d "{\"required_approvals\": ${WANT}}" "$B" >/dev/null echo "==> after" AFTER_JSON="$(api "$B")" jq -r '{required_approvals, block_admin_merge_override, enable_status_check, status_check_contexts}' <<<"$AFTER_JSON" GOT="$(jq -r '.required_approvals' <<<"$AFTER_JSON")" ADMIN_BLOCK="$(jq -r '.block_admin_merge_override' <<<"$AFTER_JSON")" STATUS="$(jq -r '.enable_status_check' <<<"$AFTER_JSON")" fail=0 [ "$GOT" = "$WANT" ] || { echo "PATCH did not take: required_approvals is ${GOT}, wanted ${WANT}" >&2; fail=1; } # These two are what keeps this change narrow. If the PATCH disturbed either, the gate has # stopped binding and that must be loud, not discovered later. [ "$ADMIN_BLOCK" = true ] || { echo "block_admin_merge_override is ${ADMIN_BLOCK}, MUST be true — the gate is now bypassable" >&2; fail=1; } [ "$STATUS" = true ] || { echo "enable_status_check is ${STATUS}, MUST be true — the gate is no longer required" >&2; fail=1; } [ "$fail" -eq 0 ] || { echo "do NOT record this as applied." >&2; exit 1; } cat <<'NOTE' NOW PROVE IT -- the setting is not the evidence. Two things to see, not assume: 1. An APPROVAL IS NO LONGER NEEDED. On your own PR with a green gate, the merge button is live with zero reviews on it. 2. THE GATE STILL BINDS. On a PR whose gate is RED or still pending, Gitea must REFUSE the merge -- for you, as owner. If it lets you through, block_admin_merge_override is not doing its job and this change has traded a lock-out for a bypass. Then confirm the detector agrees: run ci/gate.sh from a branch carrying the new expected-branch-protection.json and check that `gitea-settings` is green rather than DRIFT. NOTE