22439b0e43
gates / gates (push) Successful in 30s
A (R-630, the P1): paperless-ngx, same app same button - failed at +313.0s with the app stopped under v0.261.0, done at +53.4s now, with no "no probe container" warning because the explicit healthcheck.container resolved the target. B (R-633): the busy guard fires - RemoveStack REFUSED (busy): a backup or restore is running - and the live proof caught it answering HTTP 500, because router.go maps remove errors by grepping the error TEXT. v0.262.1 makes it a typed error and a 409; re-proven live. C (R-634 half): a half-state with app.yaml on disk and deployed=false answered 200, leftovers NONE. Under v0.261.0 the same call said "not deployed". F (R-614): phase done before the remove, no phase at all after redeploying the same name. Also: the first B run proved NOTHING and nearly went down as a pass - the refusal came from the pre-existing "still running" check, not the new guard. Recorded. 09 6.1 and 8.8, the capability map, and STATUS updated. R-625 and R-634's mechanism are named as owed, not half-done. Register 325. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
86 lines
3.8 KiB
Python
86 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""B (R-633) and C (R-634) live on 9202, against v0.262.0."""
|
|
import json, os, sys, time
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, os.path.join(os.path.dirname(HERE), "update-night-2026-09-21"))
|
|
import walk as w # noqa: E402
|
|
|
|
OUT = json.load(open(os.path.join(HERE, "live262.json"))) if os.path.exists(
|
|
os.path.join(HERE, "live262.json")) else {}
|
|
|
|
|
|
def save(tag, rec):
|
|
OUT[tag] = rec
|
|
json.dump(OUT, open(os.path.join(HERE, "live262.json"), "w"), ensure_ascii=False, indent=2)
|
|
print("\n== " + tag + "\n" + json.dumps(rec, ensure_ascii=False, indent=2)[:1500], flush=True)
|
|
|
|
|
|
def B():
|
|
"""A remove sent DURING a restore must be refused; a remove after it must be VERIFIED."""
|
|
# gokapi is crash-looping from this morning's R-633 artefact — its config volume was removed, so
|
|
# its binary can never start and the stack never settles. A subject that cannot reach a steady
|
|
# state proves nothing about a guard that fires between steady states. privatebin is light,
|
|
# reliable and was walked all night.
|
|
app, sub = "privatebin", "paste"
|
|
rec = {"scenario": "B (R-633/R-626)", "app": app}
|
|
w.deploy(app, sub)
|
|
w.wait_app(sub, "/", tries=40)
|
|
w.backup_now(app)
|
|
snaps = w.snapshots(app)
|
|
rec["snapshots"] = len(snaps)
|
|
# start a restore and, while it runs, ask to remove
|
|
r = w.restore(app)
|
|
rec["restore_started"] = str(r)[:200]
|
|
time.sleep(2)
|
|
code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": False, "remove_backups": False})
|
|
rec["remove_during_restore"] = {"http": code, "answer": d}
|
|
rec["refused_as_expected"] = code not in ("200", "202")
|
|
# let the restore settle, then remove properly
|
|
for _ in range(60):
|
|
st = w.stack(app)
|
|
if st.get("state") in ("running", "unhealthy", "stopped", "not_deployed"):
|
|
break
|
|
time.sleep(5)
|
|
time.sleep(10)
|
|
code, d = w.ctl("POST", f"/api/stacks/{app}/stop")
|
|
time.sleep(8)
|
|
code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": True, "remove_backups": True})
|
|
rec["remove_after_restore"] = {"http": code, "verified": (d.get("data") or {}).get("verified"),
|
|
"reappeared": (d.get("data") or {}).get("reappeared_removed")}
|
|
time.sleep(30)
|
|
rec["containers_60s_later"] = w.guest(
|
|
f"docker ps -a --filter label=com.docker.compose.project={app} --format '{{{{.Names}}}}'").strip()
|
|
save("B", rec)
|
|
|
|
|
|
def C():
|
|
"""A half-state — app.yaml on disk, deployed=false, no containers — must be REMOVABLE."""
|
|
app = "sparkyfitness"
|
|
rec = {"scenario": "C (R-634)", "app": app}
|
|
# build the exact measured shape by hand on the scratch guest: the stack dir with an app.yaml
|
|
# and no containers, which is what two failed deploys left behind twice.
|
|
rec["setup"] = w.guest(f'''set -e
|
|
mkdir -p /opt/docker/stacks/{app}
|
|
cp /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache/templates/{app}/docker-compose.yml /opt/docker/stacks/{app}/ 2>/dev/null || true
|
|
printf 'desired_state: stopped\\ninstalled_images:\\n' > /opt/docker/stacks/{app}/app.yaml
|
|
ls -la /opt/docker/stacks/{app}/''').strip()[-300:]
|
|
w.ctl("POST", "/api/stacks/rescan")
|
|
time.sleep(6)
|
|
st = w.stack(app)
|
|
rec["before"] = {"deployed": st.get("deployed"), "state": st.get("state")}
|
|
code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": False, "remove_backups": True})
|
|
rec["remove"] = {"http": code, "answer": str(d)[:300]}
|
|
rec["accepted"] = code == "200"
|
|
time.sleep(5)
|
|
rec["leftovers"] = w.guest(f"ls /opt/docker/stacks/{app}/app.yaml 2>/dev/null || echo NONE").strip()
|
|
save("C", rec)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
w.login()
|
|
for fn in (B, C):
|
|
try:
|
|
fn()
|
|
except Exception as e:
|
|
save(fn.__name__ + "-ERROR", {"error": f"{type(e).__name__}: {e}"})
|