main became a protected branch on 2026-09-29, so the direct push that 37 of 39 runbooks instructed is REJECTED. The wording was merely stale before; it is now actively wrong, and it is the line an operator reads mid-incident. Every occurrence now describes the real path: push a branch, open a PR, merge once the gate is green -- that merge is the deploy. 35 files shared one identical block; flux-reconcile.md, README.md and plex-claim.md each said it their own way and were corrected individually. plex-claim.md is rewritten around the delivery loop and records that THE SERVER IS NOW CLAIMED (PlexOnlineToken present in /config), so PLEX_CLAIM is a permanent no-op and the page only applies after a /config wipe. Adds infra/plex/claim-ship.sh, which drives branch -> commit -> push -> PR -> gate -> merge -> reconcile -> pod roll -> verdict with a stopwatch. Three steps are easy to lose under time pressure and two are SILENT when skipped: --with-source (without it the Kustomization applies the previously-fetched revision), the mandatory rollout restart (only the ciphertext changes, so the pod template is byte-identical and nothing restarts itself), and the feat/ branch prefix (act_runner capacity is 1, and a prefix inside gate.yml's push trigger fires a redundant run that serialises ahead of the PR run). Drops the "~4-minute TTL" the page asserted in four places. It was uncited, and upstream pms-docker documents no expiry at all. The countdown on plex.tv/claim is the authority. Measured loop: ~2m45s. DECISIONS-LOG carries the PM handoff: both halves of the 2026-08-31 CI claim were false, the stale-.runner root cause, the four-deep Flux dependency cascade, and the correction that a skipped Gitea job reports success rather than hanging. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
94 lines
4.6 KiB
Bash
Executable File
94 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# plex-claim-ship.sh — run this IMMEDIATELY AFTER editing the SOPS secret with a fresh
|
|
# plex.tv claim token. It drives the whole CI/CD loop end to end and prints a running
|
|
# stopwatch so you can see the budget being spent.
|
|
#
|
|
# branch -> commit -> push -> PR -> gate -> merge -> reconcile -> pod roll -> verify
|
|
#
|
|
# It never sees, prints, or handles the token: the file is already encrypted by sops.
|
|
set -euo pipefail
|
|
HOST=gitea.home.keithjackson.dev
|
|
OWNER=Home-Network
|
|
REPO=home-network-k3s-impl
|
|
SECRET=apps/wave-0/plex-claim-secret.sops.yaml
|
|
T0=$(date +%s)
|
|
el() { printf '\n[T+%02d:%02d] %s\n' $(( ($(date +%s)-T0)/60 )) $(( ($(date +%s)-T0)%60 )) "$1"; }
|
|
|
|
cd "$(git rev-parse --show-toplevel)"
|
|
|
|
# --- preflight: refuse to ship a file that is not what we think it is -------------
|
|
git diff --quiet -- "$SECRET" && { echo "ERROR: $SECRET is unchanged. Edit it first:"; \
|
|
echo " infra/devcontainer/dev.sh run sops $SECRET"; exit 1; }
|
|
grep -q 'PLEX_CLAIM: ENC\[' "$SECRET" || { echo "ERROR: $SECRET is not encrypted. REFUSING."; exit 1; }
|
|
[ -z "$(git status --porcelain | grep -v " $SECRET\$")" ] || echo "NOTE: other files are dirty; only $SECRET will be committed."
|
|
|
|
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") "$@"; }
|
|
|
|
# feat/ prefix ON PURPOSE. act_runner capacity is 1, and gate.yml's PUSH trigger covers
|
|
# feature/** fix/** docs/** chore/** but NOT feat/** — so a chore/ branch would fire a
|
|
# second, redundant run that SERIALISES ahead of the PR run and doubles the wall clock.
|
|
BRANCH="feat/plex-claim-token-$(date +%H%M%S)"
|
|
|
|
el "committing on $BRANCH"
|
|
git checkout -q -b "$BRANCH"
|
|
git add "$SECRET"
|
|
git commit -q -m 'chore(plex): fresh plex.tv claim token
|
|
|
|
Injected inside the plex.tv claim window per runbooks/plex-claim.md.'
|
|
SHA=$(git rev-parse HEAD)
|
|
|
|
el "pushing"
|
|
git push -q -u origin "$BRANCH"
|
|
|
|
el "opening PR"
|
|
PR=$(jq -n --arg h "$BRANCH" --arg b main --arg t 'chore(plex): fresh plex.tv claim token' \
|
|
'{head:$h,base:$b,title:$t,body:"Token injection inside the claim window. Wiring already reviewed in #1."}' \
|
|
| api -X POST -H 'Content-Type: application/json' --data @- \
|
|
"https://${HOST}/api/v1/repos/${OWNER}/${REPO}/pulls" | jq -r '.number')
|
|
[ -n "$PR" ] && [ "$PR" != "null" ] || { echo "PR creation failed"; exit 1; }
|
|
echo " PR #$PR"
|
|
|
|
el "waiting for the gate (expect ~1m40s)"
|
|
for i in $(seq 1 60); do
|
|
st=$(api "https://${HOST}/api/v1/repos/${OWNER}/${REPO}/commits/${SHA}/status" | jq -r '.state')
|
|
printf '\r gate=%s [T+%ss] ' "$st" "$(( $(date +%s)-T0 ))"
|
|
case "$st" in success) echo; break;; failure|error) echo; echo "GATE FAILED — stopping."; exit 1;; esac
|
|
sleep 5
|
|
done
|
|
[ "$st" = "success" ] || { echo; echo "gate did not finish in time"; exit 1; }
|
|
|
|
el "merging PR #$PR"
|
|
api -X POST -H 'Content-Type: application/json' -d '{"Do":"merge"}' \
|
|
"https://${HOST}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR}/merge" >/dev/null
|
|
git checkout -q main && git pull -q --ff-only origin main
|
|
|
|
el "forcing the Flux reconcile (--with-source, or it applies the OLD revision)"
|
|
./infra/devcontainer/dev.sh run bash -c \
|
|
'flux --context k3s-prod -n flux-system reconcile kustomization apps-wave-0 --with-source'
|
|
|
|
# MANDATORY here. Only the Secret ciphertext changed, so the Deployment pod template is
|
|
# byte-identical and NOTHING restarts on its own. PLEX_CLAIM is read once, at container
|
|
# start. Skipping this is failure mode #2 in runbooks/plex-claim.md.
|
|
el "rolling the pod (mandatory — a changed Secret restarts nothing)"
|
|
./infra/devcontainer/dev.sh run bash -c \
|
|
'kubectl --context k3s-prod -n plex rollout restart deployment/plex && \
|
|
kubectl --context k3s-prod -n plex rollout status deployment/plex --timeout=120s'
|
|
|
|
el "verdict"
|
|
./infra/devcontainer/dev.sh run bash -c \
|
|
'kubectl --context k3s-prod -n plex logs deploy/plex --since=3m | grep -i -e claim -e token || true'
|
|
cat <<'NOTE'
|
|
|
|
"Attempting to obtain server token..." + "Token obtained successfully" -> CLAIMED
|
|
"Attempting..." with no success line -> token expired; retry, nothing is broken
|
|
neither line -> already claimed, or env did not reach the container
|
|
|
|
Confirm it persisted (prints the attribute NAME only, never its value):
|
|
|
|
infra/devcontainer/dev.sh run bash -c \
|
|
'kubectl --context k3s-prod -n plex exec deploy/plex -- sh -c \
|
|
"grep -o PlexOnlineToken= \"/config/Library/Application Support/Plex Media Server/Preferences.xml\" | head -1"'
|
|
NOTE
|