Files
endorphin-tabularium-docs/scripts/deploy.py
T
codex-bot 1eabfcfb21
Verify and deploy Tabularium / verify-and-deploy (push) Waiting to run
Deploy Tabularium on ces-control with verified image reuse and persistent caches
2026-10-26 20:06:30 +08:00

85 lines
3.6 KiB
Python

"""Deploy a verified image, never rebuild during deployment or rollback."""
import fcntl
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import urllib.request
from ci import APP, ROOT, fingerprint, run
from smoke import verify
DEPLOY_ROOT = Path("/opt/endorphin-tabularium-docs")
CONFIG = Path("/etc/endorphin-tabularium-docs/production.env")
def latest_main() -> str:
base = os.environ["GITEA_SERVER_URL"].rstrip("/")
if base != "https://gitea.entroia.com":
raise ValueError("Unexpected Gitea API origin")
request = urllib.request.Request(base + "/api/v1/repos/Zayden-Jung/endorphin-tabularium-docs/branches/main",
headers={"Authorization": "token " + os.environ["GITEA_TOKEN"]})
with urllib.request.urlopen(request, timeout=25) as response:
return json.load(response)["commit"]["id"]
def compose(release: Path, *args: str) -> None:
run("docker", "compose", "-p", APP, "--env-file", str(CONFIG),
"--env-file", str(release / "release.env"), "-f", str(release / "compose.yml"), *args)
def main() -> None:
if os.geteuid() != 0:
raise RuntimeError("Deployment requires root on ces-control")
sha = os.environ["TABULARIUM_RELEASE_SHA"]
if not re.fullmatch(r"[0-9a-f]{40}", sha) or sha != run("git", "rev-parse", "HEAD", capture=True):
raise ValueError("Release SHA does not match checkout")
DEPLOY_ROOT.mkdir(parents=True, exist_ok=True)
with (DEPLOY_ROOT / ".deploy.lock").open("w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
if latest_main() != sha:
print("Skipping stale release: main has advanced", flush=True)
return
expected = f"{APP}:verified-main-{fingerprint()}"
actual = (ROOT / ".artifacts/verified-image").read_text().strip()
if actual != expected:
raise RuntimeError("Verified image does not match current source")
run("docker", "image", "inspect", actual, capture=True)
CONFIG.parent.mkdir(parents=True, exist_ok=True)
if not CONFIG.exists():
CONFIG.write_text("TABULARIUM_BIND=0.0.0.0\nTABULARIUM_PORT=18132\n")
CONFIG.chmod(0o600)
settings = dict(line.split("=", 1) for line in CONFIG.read_text().splitlines()
if line and not line.startswith("#"))
port = int(settings.get("TABULARIUM_PORT", "18132"))
if not 1024 <= port <= 65535:
raise ValueError("Invalid port")
current = DEPLOY_ROOT / "current"
previous = current.resolve() if current.is_symlink() else None
release = DEPLOY_ROOT / "releases" / sha
release.mkdir(parents=True, exist_ok=True)
shutil.copyfile(ROOT / "docker-compose.ces-control.yml", release / "compose.yml")
(release / "release.env").write_text(f"TABULARIUM_IMAGE={actual}\n")
try:
compose(release, "up", "-d", "--no-build", "--wait", "--wait-timeout", "120")
verify(f"http://127.0.0.1:{port}")
except Exception:
if previous is not None:
print("Deployment failed; restoring previous verified release", flush=True)
compose(previous, "up", "-d", "--no-build", "--wait", "--wait-timeout", "120")
else:
compose(release, "stop", "docs")
raise
pending = DEPLOY_ROOT / "current.next"
if pending.is_symlink():
pending.unlink()
pending.symlink_to(release)
pending.replace(current)
compose(release, "ps")
print(f"Deployed release={sha} image={actual} port={port}", flush=True)
if __name__ == "__main__":
main()