Files
felhom.eu/documentation/audits/more-night-apps-2026-09-30/tools/boxstep.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

77 lines
4.4 KiB
Python

"""boxstep.py <app> <sub> <svc>=<to-ref> [...] — a WITHIN-A-MAJOR step's BOX venue on 9202 (drill catalog), Part F.
prep is boxmove.py's (deploy, seed through the app's own route, C1); this presses the step:
a DRILL-only commit moves the named services and adds a ladder entry (no conversion mark), sync, rescan,
the product's guarded Update, the seed read back → box verdict JSON beside boxmove's.
The live catalog's entry is written later ONLY by `upgrade-test.py --write-ladder` from both verdicts.
Evidence: box/<app>/step.txt + box/<app>/box-verdict-<app>.json."""
import json, os, re, subprocess, 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 = sys.argv[1:3]
moves = dict(a.split("=", 1) for a in sys.argv[3:])
EVD = f"{w.EV}/box/{app}"; os.makedirs(EVD, exist_ok=True)
TOK = w.SC + "/seed-tokens-box.json"
log = open(f"{EVD}/step.txt", "a", buffering=1)
def say(*a):
w.say(*a); log.write(" ".join(map(str, a)) + "\n")
D = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill"
w.login()
toks = json.load(open(TOK))
if isinstance(toks.get(app), dict) and toks[app].get("__generated"):
w.GENERATED[app] = toks[app]["__generated"]
st = w.stack(app); before = (st.get("app_config") or {}).get("pinned_images")
say(f"before: pinned={before}")
if not os.environ.get("SKIPDRILL"):
subprocess.run(["git", "-C", D, "pull", "-q", "--rebase", "origin", "main"], check=True)
comp = f"{D}/templates/{app}/docker-compose.yml"; fy = f"{D}/templates/{app}/.felhom.yml"
s = open(comp).read(); pins, cur = {}, None
for line in s.splitlines():
m = re.match(r"^ ([a-z0-9-]+):\s*$", line)
if m: cur = m.group(1)
m = re.match(r"^\s+image:\s*(\S+)", line)
if m and cur: pins[cur] = m.group(1)
out, cur = [], None
for line in s.splitlines():
m = re.match(r"^ ([a-z0-9-]+):\s*$", line)
if m: cur = m.group(1)
mi = re.match(r"^(\s+image:\s*)(\S+)\s*$", line)
if mi and cur in moves:
line = mi.group(1) + moves[cur]
out.append(line)
open(comp, "w").write("\n".join(out) + "\n")
top = dict(pins); top.update(moves)
entry = {"from": pins, "to": top, "verdict": "proven", "tested_at": "DRILL", "harness_version": 4,
"evidence": "DRILL (box proof in progress)", "marks": {"files_may_change": False, "needs_person": None, "memory_tight": False}}
f = open(fy).read()
f = f.rstrip("\n") + "\n - " + json.dumps(entry) + "\n" if "update_ladder:" in f else f.rstrip("\n") + "\nupdate_ladder:\n - " + json.dumps(entry) + "\n"
open(fy, "w").write(f)
subprocess.run(["git", "-C", D, "commit", "-q", "-am", f"DRILL {app}: {moves} (box proof)"], check=True)
subprocess.run(["git", "-C", D, "push", "-q", "origin", "main"], check=True, capture_output=True)
say("drill:", subprocess.run(["git", "-C", D, "log", "--oneline", "-1"], capture_output=True, text=True).stdout.strip())
w.sync_rescan(app, next(iter(moves.values())))
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", []):
log.write(f" phase +{p['t']}s {p['phase']} | err={p['error']}\n")
time.sleep(10)
read = FX[app].verify(w, sub, toks[app], say)
lines = w.guest(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'update {app}' | grep -v DEBUG | cut -c1-400")
log.write(lines + "\n")
st = w.stack(app); after = (st.get("app_config") or {}).get("pinned_images")
verdict = {"app": app, "venue": "box 9202 (drill catalog, controller 0.283.1), the product's guarded Update",
"from": before, "to": after,
"verdict": "proven" if (res.get("final_phase") == "done" and read and all(after.get(k) == v for k, v in moves.items())) else "failed",
"seed_read_before": True, "seed_read_after": read, "healthy_after": st.get("state") == "running",
"duration_s": res.get("duration_s"), "final_phase": res.get("final_phase"), "measured_at": since,
"evidence": f"felhom.eu/documentation/audits/more-night-apps-2026-09-30/box/{app}/step.txt"}
json.dump(verdict, open(f"{EVD}/box-verdict-{app}.json", "w"), indent=2)
say(f"RESULT final_phase={res.get('final_phase')} after={after} seed_after={read} verdict={verdict['verdict']} ({res.get('duration_s')} s)")