M5: upgrade + backup/restore stages green (custom-html); backup-bot-two oneshot
All checks were successful
continuous-integration/drone/push Build is passing

3-stage run green (install/upgrade/backup), clean teardown. backupbot deployed
via reconcile oneshot; PTY (script) for abra backup/restore; -m for secret generate
(no value leak). M5 CLAIMED.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 00:53:13 +01:00
parent 0fe3d7cda7
commit 7eb0dd3c77
12 changed files with 266 additions and 17 deletions

View File

@ -87,6 +87,52 @@ def wait_healthy(domain: str, ok_codes=(200, 301, 302), deploy_timeout: int = 60
raise TimeoutError(f"{domain}: not healthy over HTTPS (last status {last})")
def upgrade_app(domain: str, version: str | None = None) -> None:
abra.upgrade(domain, version=version)
def backup_app(domain: str) -> None:
abra.backup_create(domain)
def restore_app(domain: str) -> None:
abra.restore(domain)
def previous_version(recipe: str) -> str | None:
"""The second-newest published version (to deploy before upgrading to latest)."""
vers = abra.recipe_versions(recipe)
return vers[-2] if len(vers) >= 2 else None
def _app_container(domain: str, service: str = "app") -> str:
"""The running container id for <stack>_<service>."""
name = f"{_stack_name(domain)}_{service}"
proc = subprocess.run(
["docker", "ps", "--filter", f"name={name}", "--format", "{{.ID}}"],
capture_output=True, text=True,
)
cid = proc.stdout.strip().split("\n")[0]
if not cid:
raise RuntimeError(f"no running container for {name}")
return cid
def exec_in_app(domain: str, cmd: list[str], service: str = "app") -> str:
cid = _app_container(domain, service)
proc = subprocess.run(["docker", "exec", cid, *cmd], capture_output=True, text=True)
return proc.stdout
def http_body(domain: str, path: str = "/", timeout: int = 15) -> str:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(f"https://{domain}{path}", method="GET")
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return resp.read().decode(errors="replace")
def teardown_app(domain: str) -> None:
"""Idempotent, best-effort full teardown. Never raises (finalizer-safe)."""
abra.undeploy(domain)