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

@ -19,6 +19,19 @@ class AbraError(RuntimeError):
pass
def _run_pty(args: list[str], timeout: int = 900, check: bool = True) -> subprocess.CompletedProcess:
"""Run abra under a pseudo-TTY (via util-linux `script`). Needed for commands that exec into
a container interactively (backup create / restore: 'the input device is not a TTY')."""
cmd = "abra " + " ".join(args)
proc = subprocess.run(
["script", "-qec", cmd, "/dev/null"],
capture_output=True, text=True, timeout=timeout,
)
if check and proc.returncode != 0:
raise AbraError(f"[pty] {cmd} failed ({proc.returncode}):\n{proc.stdout}\n{proc.stderr}")
return proc
def _run(args: list[str], timeout: int = 300, check: bool = True) -> subprocess.CompletedProcess:
proc = subprocess.run(
[ABRA, *args],
@ -64,7 +77,9 @@ def env_set(domain: str, key: str, value: str) -> None:
def secret_generate(domain: str, timeout: int = 300) -> None:
_run(["app", "secret", "generate", domain, "--all", "-n"], timeout=timeout, check=False)
# -m avoids the TTY/table (ioctl) path; output (which contains the generated values) is
# captured by _run and never logged. check=False: recipes with no secrets are a no-op.
_run(["app", "secret", "generate", domain, "--all", "-m", "-n"], timeout=timeout, check=False)
def deploy(domain: str, chaos: bool = True, timeout: int = 900) -> None:
@ -74,6 +89,34 @@ def deploy(domain: str, chaos: bool = True, timeout: int = 900) -> None:
_run(args, timeout=timeout)
def upgrade(domain: str, version: Optional[str] = None, timeout: int = 900) -> None:
args = ["app", "upgrade", domain]
if version:
args.append(version)
# -f no prompt, -D skip public-DNS checks (our per-run domains route via the gateway).
# (upgrade has no --chaos flag.)
args += ["-f", "-D", "-n"]
_run(args, timeout=timeout)
def backup_create(domain: str, timeout: int = 900) -> None:
_run_pty(["app", "backup", "create", domain, "-n"], timeout=timeout)
def restore(domain: str, timeout: int = 900) -> None:
_run_pty(["app", "restore", domain, "-n"], timeout=timeout)
def recipe_versions(recipe: str) -> list[str]:
"""Published versions of a recipe, oldest→newest (from the recipe git tags)."""
import os
import subprocess
path = os.path.expanduser(f"~/.abra/recipes/{recipe}")
proc = subprocess.run(["git", "-C", path, "tag", "--sort=creatordate"],
capture_output=True, text=True)
return [t for t in proc.stdout.split("\n") if t.strip()]
def undeploy(domain: str, timeout: int = 600) -> None:
# NB: no --chaos here (unsupported).
_run(["app", "undeploy", domain, "-n"], timeout=timeout, check=False)