From 1d4abca54ac1c65b2fac9fe6959918d0a840861d Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 25 Oct 2026 12:59:14 +0100 Subject: [PATCH] Release run 2026-10-26: hub CHANGELOG names v0.170.0 (R-1254) for the build; the run's evidence folder (fix A/B red-proofs, probe) --- .../redproof/fixA-first-sample.txt | 13 ++ .../redproof/fixA-own-restore-race.txt | 8 + .../redproof/fixA-page-verdict.txt | 11 ++ .../redproof/fixB-no-prepull.txt | 8 + .../release-2026-10-26/tools/fixprobe.py | 152 ++++++++++++++++++ hub/CHANGELOG.md | 2 +- 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 documentation/audits/release-2026-10-26/redproof/fixA-first-sample.txt create mode 100644 documentation/audits/release-2026-10-26/redproof/fixA-own-restore-race.txt create mode 100644 documentation/audits/release-2026-10-26/redproof/fixA-page-verdict.txt create mode 100644 documentation/audits/release-2026-10-26/redproof/fixB-no-prepull.txt create mode 100644 documentation/audits/release-2026-10-26/tools/fixprobe.py diff --git a/documentation/audits/release-2026-10-26/redproof/fixA-first-sample.txt b/documentation/audits/release-2026-10-26/redproof/fixA-first-sample.txt new file mode 100644 index 00000000..86418161 --- /dev/null +++ b/documentation/audits/release-2026-10-26/redproof/fixA-first-sample.txt @@ -0,0 +1,13 @@ +# Fix A red-proof: the first running sample counts as started (= e8d2151). 2026-10-25T10:50:56Z + stable_start_test.go:70: a flapping app must keep its way back, found [] +--- FAIL: TestFixA_AFlappingAppKeepsTheCopy (0.60s) +--- PASS: TestFixA_AStableAppReleasesTheCopyAfterTheWindow (0.00s) + stable_start_test.go:106: a healthcheck still starting must keep the copy, found [] +--- FAIL: TestFixA_AHealthcheckStillStartingKeepsTheCopy (0.50s) + stable_start_test.go:121: a new restore owns the app: the watch ends and the copy stays, found [] +--- FAIL: TestFixA_ANewRestoreDuringTheWatchLeavesTheCopy (0.70s) + stable_start_test.go:142: a flapping app must never turn the record into a success +--- FAIL: TestFixA_TheLateStartWatcherNeedsAStableStart (0.01s) +FAIL +FAIL gitea.dooplex.hu/admin/felhom-controller/internal/backup 1.831s +FAIL diff --git a/documentation/audits/release-2026-10-26/redproof/fixA-own-restore-race.txt b/documentation/audits/release-2026-10-26/redproof/fixA-own-restore-race.txt new file mode 100644 index 00000000..bd73b880 --- /dev/null +++ b/documentation/audits/release-2026-10-26/redproof/fixA-own-restore-race.txt @@ -0,0 +1,8 @@ +# Fix A (own-restore race) red-proof: the watch ends on ANY running restore of the app, its own included. 2026-10-25T11:35:34Z + restore_verdict_test.go:103: a late start must turn the record into a success: &{Op:restore Stack:app OK:false Message:A(z) app: az adatokat visszatöltöttük, de az alkalmazás még nem indult el. Várj pár percet, majd nyisd meg újra. Ha így marad, szólj nekünk. FinishedAt:2026-10-25 12:35:38.16229144 +0100 CET m=+0.067010370 Interrupted:false PutBack:false PutBackWaiting:false NotStarted:true} +--- FAIL: TestR1270_ALateStartTurnsTheRecordIntoASuccess (2.06s) + stable_start_test.go:92: a stable app releases the copy after the window, found [/tmp/TestFixA_AStableAppReleasesTheCopyAfterTheWindow3012085805/001/drive/backups/restore-undo/app/20261025T113540Z/app_data.tar] +--- FAIL: TestFixA_AStableAppReleasesTheCopyAfterTheWindow (3.01s) +FAIL +FAIL gitea.dooplex.hu/admin/felhom-controller/internal/backup 5.086s +FAIL diff --git a/documentation/audits/release-2026-10-26/redproof/fixA-page-verdict.txt b/documentation/audits/release-2026-10-26/redproof/fixA-page-verdict.txt new file mode 100644 index 00000000..b7a0715b --- /dev/null +++ b/documentation/audits/release-2026-10-26/redproof/fixA-page-verdict.txt @@ -0,0 +1,11 @@ +# Fix A (page verdict) red-proof: the first verdict is the one-sample health wait alone (= e8d2151). 2026-10-25T11:47:36Z + stable_start_test.go:71: a running → restarting → running app is „not started yet" on the page too, got +--- FAIL: TestFixA_AFlappingAppKeepsTheCopy (0.00s) +--- PASS: TestFixA_AStableAppReleasesTheCopyAfterTheWindow (0.16s) + stable_start_test.go:114: a healthcheck still starting is not started, got +--- FAIL: TestFixA_AHealthcheckStillStartingKeepsTheCopy (0.00s) +--- PASS: TestFixA_ANewRestoreDuringTheWatchLeavesTheCopy (0.70s) +--- PASS: TestFixA_TheLateStartWatcherNeedsAStableStart (0.40s) +FAIL +FAIL gitea.dooplex.hu/admin/felhom-controller/internal/backup 1.272s +FAIL diff --git a/documentation/audits/release-2026-10-26/redproof/fixB-no-prepull.txt b/documentation/audits/release-2026-10-26/redproof/fixB-no-prepull.txt new file mode 100644 index 00000000..269b93ca --- /dev/null +++ b/documentation/audits/release-2026-10-26/redproof/fixB-no-prepull.txt @@ -0,0 +1,8 @@ +# Fix B red-proof: the import does not ask the D48 helper (= e8d2151). 2026-10-25T10:52:50Z + r1279_pull_first_test.go:60: the helper must be asked about the BUNDLE's definition (the one that will run), got "" +--- FAIL: TestR1279_AnUnpullableImageRefusesTheImportBeforeAnythingIsRemoved (0.05s) + r1279_pull_first_test.go:89: the pull must come BEFORE the running app's volumes are removed: removed at ask -1, after 1 +--- FAIL: TestR1279_APullableImageIsAskedFirstAndTheImportGoesOn (0.05s) +FAIL +FAIL gitea.dooplex.hu/admin/felhom-controller/internal/appexport 0.110s +FAIL diff --git a/documentation/audits/release-2026-10-26/tools/fixprobe.py b/documentation/audits/release-2026-10-26/tools/fixprobe.py new file mode 100644 index 00000000..6bb5ba54 --- /dev/null +++ b/documentation/audits/release-2026-10-26/tools/fixprobe.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""fixprobe.py — fixes A and B live on a throwaway Vikunja this run installs (never a household's data). + +A (the undo copy only after a stable start): the app's unit edited to run `busybox` with `sleep 20; exit 1` (restart +policy on, no healthcheck — it reads „running" for 20 s, then crashes and restarts) → „Visszaállítás": the page says +not started yet, the undo copy stays, the operator line is logged once; the original unit back → a normal restore: +the copy is there right after, gone after the 3-minute window. +B (the `.fab` import downloads first): the app exported through the page's API, a COPY of the bundle under its own name +with the image tag edited to one that does not exist → import: refused, the containers never restarted (StartedAt), the +volumes untouched (their CreatedAt), the item still there. The test bundle and the export are removed after. +The unit / bundle edits are the only hand-made changes (`pct exec`, files only). + +usage: SC=… SWEEP_EV=… TARGET=9202 DRIVE_ROOT=… python3 fixprobe.py +""" +import json, os, re, sys, time + +TOOLS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../catalog-sweep-2026-10-24/tools") +sys.path.insert(0, TOOLS) +import sweep as sw # noqa: E402 + +w, fxm = sw.w, sw.fxm +APP, REPO = "vikunja", "vikunja/vikunja" +NS = "/mnt/sys_drive/felhom-data" +UNIT = f"{NS}/backups/primary/{APP}" +ORIG = f"{NS}/backups/fix-a-test-original/{APP}" +UNDO = f"{NS}/backups/restore-undo/{APP}" +DRIVE = os.environ.get("DRIVE_ROOT", "/mnt/felhom-drives/scratch_hdd") + + +def started(): + return sw.g(f"for c in $(docker ps -a --filter label=com.docker.compose.project={APP} -q); do " + "docker inspect -f '{{.Name}} {{.State.StartedAt}} {{.RestartCount}} {{.State.Status}}' $c; done").strip() + + +def vols(): + return sw.g(f"for v in $(docker volume ls -q --filter label=com.docker.compose.project={APP}); do " + "docker volume inspect -f '{{.Name}} {{.CreatedAt}}' $v; done").strip() + + +def sums(p): + return sw.g(f"cd {p} 2>/dev/null && find . -type f | sort | xargs -r sha256sum") + + +def main(): + d = os.path.join(sw.EV, "fixprobe") + os.makedirs(d, exist_ok=True) + log = open(os.path.join(d, "walk.log"), "a") + + def say(*a): + line = time.strftime("%H:%M:%SZ", time.gmtime()) + " " + " ".join(str(x) for x in a) + print(line, flush=True) + log.write(sw.scrub(line) + "\n") + log.flush() + + w.say = say + fx = fxm.FIXTURES[APP] + sub = getattr(fx, "sub", APP) + rec = {} + t_start = int(time.time()) + w.login() + test_fab = "" + try: + rec["install"] = w.deploy(APP, sub) and w.wait_app(sub, "/", tries=90, delay=5) + tok = fx.seed(w, sub, say) + rec["seed"] = bool(tok) + sw.backup_now(rec, say) + sw.g(f"rm -rf {ORIG}; mkdir -p {NS}/backups/fix-a-test-original && cp -a {UNIT} {ORIG}") + s0 = sums(UNIT) + + # --- A: an app that runs 20 s, crashes, restarts + out = sw.g(f"cd {UNIT} && sed -i -E 's#{REPO}:[^\"[:space:]]*#busybox:1.36.1#g' compose/docker-compose.yml manifest.json data-stamps.json && " + "sed -i -E 's#^( image: busybox:1.36.1)$#\\1\\n command: [\"sh\", \"-c\", \"sleep 20; exit 1\"]#' compose/docker-compose.yml && " + "grep -n 'image:\\|command:' compose/docker-compose.yml") + say(" [A] the unit now runs: " + out.replace("\n", " | ")) + sw.restore_unit(APP, rec, say) + rec["a_restore"] = rec.pop("restore") + say(" [A] restore: " + rec["a_restore"].get("status", "")[:400]) + rec["a_containers"] = started() + rec["a_undo"] = sw.g(f"ls -1 {UNDO} 2>&1").strip() + say(f" [A] containers: {rec['a_containers']} | kept undo copy: {rec['a_undo']}") + time.sleep(60) + rec["a_containers_60s_later"] = started() + say(f" [A] 60 s later: {rec['a_containers_60s_later']}") + # --- A: the original unit back, a normal restore, the copy released after the window + sw.g(f"rm -rf {UNIT} && cp -a {ORIG} {UNIT}") + rec["original_back"] = sums(UNIT) == s0 + sw.g(f"rm -rf {UNDO}") # the copy the not-started run kept (mine), so the next count is clean + sw.restore_unit(APP, rec, say) + rec["a_normal_restore"] = rec.pop("restore") + say(" [A] normal restore: " + rec["a_normal_restore"].get("status", "")[:300]) + rec["a_undo_right_after"] = sw.g(f"ls -1 {UNDO} 2>&1").strip() + say(f" [A] the copy right after the success: {rec['a_undo_right_after']}") + for _ in range(30): + time.sleep(10) + left = sw.g(f"ls -1 {UNDO} 2>&1").strip() + if "No such file" in left or not left: + break + rec["a_undo_after_window"] = sw.g(f"ls -1 {UNDO} 2>&1").strip() + rec["a_item_after"] = bool(tok) and bool(fx.verify(w, sub, tok, say)) + say(f" [A] after the window: {rec['a_undo_after_window'] or '(gone)'}; item {rec['a_item_after']}") + + # --- B: export, a copy with a missing tag, import + c, r = w.ctl("POST", "/api/export/start", {"stack_name": APP, "dest_drive": DRIVE, "password": "", "stop_app": False}) + say(f" [B] export start -> {c} {str(r)[:160]}") + for _ in range(60): + time.sleep(5) + c, st = w.ctl("GET", "/api/export/status") + stt = json.dumps(st, ensure_ascii=False) + if '"running": false' in stt or '"state": "done"' in stt or '"done"' in stt: + break + c, b = w.ctl("GET", "/api/export/bundles") + paths = re.findall(r'"path": "([^"]+vikunja[^"]*\.fab)"', json.dumps(b)) + rec["b_bundle"] = paths[:1] + if not paths: + say(" [B] no bundle found: " + json.dumps(b)[:300]) + return + src = paths[0] + test_fab = os.path.join(os.path.dirname(src), "vikunja-r1279-test.fab") + out = sw.g(f"set -e; t=$(mktemp -d); cd $t; f={src}; if gzip -t $f 2>/dev/null; then tar xzf $f; z=z; else tar xf $f; z=; fi; " + f"sed -i -E 's#{REPO}:[^\"[:space:]]*#{REPO}:0.0.0-felhom-r1279-missing#g' config/docker-compose.yml; " + f"grep -n 'image:' config/docker-compose.yml; tar c${{z}}f {test_fab} .; cd /; rm -rf $t; ls -la {test_fab}") + say(" [B] the test bundle: " + out.replace("\n", " | ")) + before_c, before_v = started(), vols() + c, r = w.ctl("POST", "/api/export/import", {"path": test_fab, "password": ""}) + say(f" [B] import -> {c} {str(r)[:160]}") + last = None + for _ in range(60): + time.sleep(5) + c, st = w.ctl("GET", "/api/export/import/status") + last = json.dumps(st, ensure_ascii=False) + if "nem lehet letölteni" in last or '"error"' in last and '"running": false' in last or '"done"' in last: + break + rec["b_import_status"] = last[:900] if last else "" + after_c, after_v = started(), vols() + rec["b_containers_before"], rec["b_containers_after"] = before_c, after_c + rec["b_volumes_before"], rec["b_volumes_after"] = before_v, after_v + rec["b_untouched"] = bool(before_c) and before_c == after_c and before_v == after_v + rec["b_item_still_there"] = bool(tok) and bool(fx.verify(w, sub, tok, say)) + say(f" [B] import status: {rec['b_import_status'][:400]}") + say(f" [B] containers and volumes untouched: {rec['b_untouched']}; item {rec['b_item_still_there']}") + finally: + rec["controller_lines"] = sw.scrub(sw.g( + f"docker logs --since {t_start} felhom-controller 2>&1 | grep -E '{APP}' | grep -iE 'R-1270|R-1279|D48|stabl|not start|released|REFUSED|Import' | cut -c1-320")) + sw.g(f"rm -f {test_fab} {os.path.dirname(test_fab)}/vikunja*.fab" if test_fab else "true") + sw.remove(APP, keep=False, say=say) + rec["after_remove"] = sw.leftovers(APP, sub) + rec["cleanup"] = sw.g(f"rm -rf {ORIG} {UNDO}; rmdir {NS}/backups/fix-a-test-original 2>/dev/null; ls -d {ORIG} {UNDO} 2>&1").strip() + json.dump(rec, open(os.path.join(d, "record.json"), "w"), ensure_ascii=False, indent=1) + + +if __name__ == "__main__": + main() diff --git a/hub/CHANGELOG.md b/hub/CHANGELOG.md index a305ace7..e3385ddb 100644 --- a/hub/CHANGELOG.md +++ b/hub/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased (next: v0.170.0) — the escrow package in one transaction (R-1254) (2026-10-24 night) — committed, NOT built for release, NOT deployed +## v0.170.0 (2026-10-25) — the escrow package in one transaction (R-1254) — built for release in the attended release run; deployed only on the operator's yes - **The new escrow key, its sealed-password hash and the identity blob are stored in ONE transaction** (`Store.SaveHostEscrowWithIdentity`; `SaveHostEscrow` is it with no identity blob). Until now the escrow PUT committed