Files
felhom.eu/documentation/audits/more-night-apps-2026-09-30/tools/undocase.py
T
admin 42bf40bd2c
gates / gates (push) Successful in 29s
More apps update themselves at night (30+2 -> 35+3); the same-name security-fix gap measured (R-740, decision for the operator)
- Twelve steps published on both venues (catalog): first ladders for calibre-web, gitea, wger,
  crafty-controller, uptime-kuma, zipline (two steps); within-major emby, ghost, home-assistant,
  outline, rallly. immich's step v3.0.3 -> v3.2.2 re-proven at 768M (Part D).
- Part C: the night leg skips a digest-only change AND the catalog never records a same-tag re-test,
  so a same-name upstream fix reaches no box. Row R-740; the decision in STATUS; `09` decision 30
  carries a dated note (the decision itself unchanged).
- Rows: 369 -> 377. Opened R-735..R-742; closed R-735, R-738, R-742; narrowed R-462, R-624, R-446,
  R-440, R-734, R-732. The currency audit gains §1b (and corrects its 31+1 to 30+2).

Evidence: documentation/audits/more-night-apps-2026-09-30/. Report: REPORT-more-night-apps-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 20:27:28 +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}")