Files
felhom.eu/documentation/audits/pg-last-six-2026-09-30/tools/undocase.py
T
admin 25cb3eb9c1
gates / gates (push) Successful in 29s
The last six PostgreSQL apps decided; the gate's wait seen live by day; catalog currency; stale rows
- R-463 CLOSED: rallly, outline, sparkyfitness -> PostgreSQL 18 (catalog 25ffd89 / aeb0cd6 / 1666572),
  each proven on the bench and on 9202 through the guarded Update with one undo case; zipline,
  adventurelog, immich stay on 16 by 09 decision 42's rule (upstream runs 16 / 16 / 14).
- Part F: bookstack, kimai, audiobookshelf, n8n, navidrome, grafana, komga moved on both venues;
  immich not (R-732: its first start OOM-killed its database on the bench).
- R-687 item 4 PROVEN LIVE on demo-hp: two deferrals while the leg stepped two apps, the whole-guest
  backup on the first poll after, success; config + window put back and read back.
- Catalog currency audit (Part D): 25/53 behind inside a major, 19 across; night-updatable 28 -> 31 (+1).
- R-446 and R-440 narrowed (measured on demo-hp); R-624 corrected (outline, rallly, zipline have routes);
  R-548 note (demo-hp's local tier refused for space since 09-27).
- New rows: R-730 (the ISO 1.29.0 build commit cannot be proven -> no tag; installer-v* is the script's
  line), R-731 (tag-shape switches), R-732. Register 361 -> 364.
- 09 §3: the 2026-09-30 operator notes (by day; Tester-2 pre-checks done; decision 52 not needed, not
  recorded); §6.4 dated currency note. STATUS, CONTEXT, REPORT-pg-last-six-2026-09-30.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-30 14:47:12 +02:00

63 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""undocase.py <app> <sub> <svc> <pg-user> <db> — the brief's ONE undo case per app on 9202 (scratch guest).
The load failing is the case (the docmost method, `audits/night-2026-09-26/D/Da-*`): an object the OLD major
has and 18 does not — the `adminpack` extension (removed in PostgreSQL 17) — is created in the app's own
database, the guarded Update is pressed (the drill catalog's step carries the conversion mark), the load
into 18 must fail, and the BOX must undo it by itself: old pin, old datadir, `undone`, the seed read back.
The extension is dropped again afterwards, so the real move that follows converts clean data.
Evidence: box/<app>/undo.txt + box/<app>/undo-verdict.json. Never prints a secret.
"""
import json, os, sys, time
import walk as w
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
import upgrade_fixtures_box as fixtures, upgrade_fixtures_box28 as _f28
FX = dict(_f28.FIXTURES28); FX.update(fixtures.FIXTURES) # the box set wins, as upgrade_boxport.get() decides
app, sub, svc, user, db = sys.argv[1:6]
EVD = f"{w.EV}/box/{app}"; os.makedirs(EVD, exist_ok=True)
log = open(f"{EVD}/undo.txt", "a", buffering=1)
def say(*a):
w.say(*a); log.write(" ".join(map(str, a)) + "\n")
w.login()
toks = json.load(open(w.SC + "/seed-tokens-box.json"))
if isinstance(toks.get(app), dict) and toks[app].get("__generated"):
w.GENERATED[app] = toks[app]["__generated"]
psql = lambda sql: w.guest(f"docker exec {svc} psql -U {user} -d {db} -Atc \"{sql}\" 2>&1").strip()
pg = lambda: w.guest(f"docker exec {svc} sh -c 'cat $PGDATA/PG_VERSION' 2>&1").strip()
say(f"# undo case (the load fails) — {time.strftime('%Y-%m-%dT%H:%M:%S%z')}")
st = w.stack(app); before = (st.get("app_config") or {}).get("pinned_images")
say(f"before: PG_VERSION={pg()} pinned={before}")
say("before: seed reads back through the front door =", FX[app].verify(w, sub, toks[app], say))
say("setup: CREATE EXTENSION adminpack ->", psql("CREATE EXTENSION IF NOT EXISTS adminpack"))
say("setup: extensions now:", psql("select string_agg(extname, ' ' order by extname) from pg_extension"))
ctrl = w.sh(["ssh", "demo-hp", "pct exec 9202 -- docker run --rm postgres:18-alpine sh -c 'ls /usr/local/share/postgresql/extension/ | grep -c ^adminpack || true'"]).stdout.strip()
say(f"control: 18 has no adminpack: {ctrl}")
since = w.guest("date -u +%Y-%m-%dT%H:%M:%SZ").strip()
res = w.press_update(app, poll=1, cap_s=1800)
for p in res.get("phases", []):
say(f" phase +{p['t']}s {p['phase']} | err={p['error']}")
say("result:", json.dumps({k: res.get(k) for k in ("accepted", "http", "duration_s", "final_phase", "update_error", "hold_reason", "state")}))
time.sleep(10)
st = w.stack(app); after = (st.get("app_config") or {}).get("pinned_images")
after_pg = pg()
read = FX[app].verify(w, sub, toks[app], say)
say(f"after: PG_VERSION={after_pg} pinned={after} state={st.get('state')}")
say("after: seed reads back through the front door =", read)
conv = w.guest(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'update {app}|CONVERT|UNDO' | grep -v DEBUG | cut -c1-400")
log.write("controller lines:\n" + conv + "\n")
say("cleanup: DROP EXTENSION adminpack ->", psql("DROP EXTENSION IF EXISTS adminpack"))
verdict = {"app": app, "case": "the load into 18 fails (adminpack, an object 18 lacks)", "venue": "box 9202, the product's guarded Update",
"final_phase": res.get("final_phase"), "duration_s": res.get("duration_s"),
"pg_version_after": after_pg, "pinned_before": before, "pinned_after": after, "seed_read_after": read,
"undone_line": next((l.split("] ", 2)[-1] for l in conv.splitlines() if "UNDONE" in l), ""),
"ok": res.get("final_phase") == "undone" and read is True and after == before,
"measured_at": since}
json.dump(verdict, open(f"{EVD}/undo-verdict.json", "w"), indent=2)
say(f"RESULT undo case ok={verdict['ok']} final_phase={res.get('final_phase')} PG_VERSION={after_pg} seed_after={read}")