#!/usr/bin/env bash # verify-then-enforce.sh — run this AFTER you have merged PRs 8, 5, 6, 7 in that order. # # It does NOT merge anything. Merging is your reviewed act (CLAUDE.md: a human approves and # merges). This script does the mechanical half: prove the converted runner actually works, # and only then turn on enforcement. # # WHY THIS ORDER. The act-runner Deployment IS the merge gate for this repository. Enabling a # required status check while the runner is broken means NO PR can merge -- including the # revert. So PR #7 lands while the gate is still advisory, is PROVEN green on a real run, and # only then does enforcement go on. # # Aborts at the first checkpoint that does not pass. Safe to re-run. set -euo pipefail HOST=gitea.home.keithjackson.dev; OWNER=Home-Network; REPO=home-network-k3s-impl REPO_ROOT=/var/home/keith/source/home-network-k3s-impl D="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$REPO_ROOT" 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")" api() { curl -sS --config <(printf 'user = "%s:%s"\n' "$U" "$P") "$@"; } A="https://${HOST}/api/v1/repos/${OWNER}/${REPO}" say() { printf '\n\033[1m==> %s\033[0m\n' "$1"; } die() { printf '\n\033[31mABORT: %s\033[0m\n' "$1" >&2; exit 1; } say "PRECHECK — all four PRs must be merged first" for n in 8 5 6 7; do m=$(api "$A/pulls/$n" | jq -r .merged) printf ' PR #%s merged=%s\n' "$n" "$m" [ "$m" = true ] || die "PR #$n is not merged yet. Merge 8, 5, 6, 7 in that order, then re-run." done say "STEP 1 — force Flux to apply the sidecar now, not on the 10m interval" ./infra/devcontainer/dev.sh run bash -c \ 'flux --context k3s-prod -n flux-system reconcile kustomization apps-wave-1 --with-source' say "STEP 2 — CHECKPOINT: the converted pod must come up healthy" ./infra/devcontainer/dev.sh run bash -c ' kubectl --context k3s-prod -n gitea-actions rollout status deploy/act-runner --timeout=180s echo "--- sidecar (initContainers) ---" kubectl --context k3s-prod -n gitea-actions get deploy act-runner \ -o jsonpath="{range .spec.template.spec.initContainers[*]}{.name} restartPolicy={.restartPolicy}{\"\n\"}{end}" kubectl --context k3s-prod -n gitea-actions get pod -l app.kubernetes.io/name=act-runner' \ || die "converted act-runner pod is not healthy -- REVERT PR #7 before anything else" say "STEP 3 — CHECKPOINT: a runner must be ONLINE again" n=0 for i in $(seq 1 40); do n=$(api "https://${HOST}/api/v1/admin/actions/runners" | jq -r '[(.runners//[])[]|select(.status=="online")]|length') printf '\r online runners: %s (%ss) ' "$n" "$((i*5))" [ "${n:-0}" -ge 1 ] && { echo; break; } sleep 5 done [ "${n:-0}" -ge 1 ] || die "no runner online after the conversion -- REVERT PR #7" say "STEP 4 — CHECKPOINT: a REAL workflow must go green ON THE CONVERTED POD" MAIN_SHA=$(api "$A/branches/main" | jq -r .commit.id) echo " watching the post-merge push run on main (${MAIN_SHA:0:8})..." st="" for i in $(seq 1 60); do st=$(api "$A/commits/$MAIN_SHA/status" | jq -r .state) printf '\r main gate: %-8s (%ss) ' "$st" "$((i*10))" case "$st" in success) echo; break;; failure|error) die "gate FAILED on the converted pod -- REVERT PR #7";; esac sleep 10 done [ "$st" = success ] || die "gate never completed on the converted pod -- REVERT PR #7; do NOT enable enforcement" echo " the converted sidecar ran a real job green. Enforcement is now safe." say "STEP 5 — require the gate + one approval" "$D/require-gate.sh" say "STEP 6 — prove a direct push is rejected (do not trust the API's word)" git fetch -q origin && git checkout -q main && git reset -q --hard origin/main git commit -q --allow-empty -m 'protection probe' if git push origin main 2>&1 | grep -q 'protected branch'; then echo " CONFIRMED: push to main rejected by the pre-receive hook." else echo " WARNING: the push was NOT rejected -- protection is not doing what it claims." fi git reset -q --hard origin/main say "FINAL STATE" api "$A/branches/main" | jq -r '{protected,user_can_push,enable_status_check,status_check_contexts,required_approvals}'