Files
endorphin-tabularium-docs/scripts/ci.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

106 lines
4.6 KiB
Python

"""Build on ces-control; reuse only verified images with identical build inputs."""
import fcntl
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time
ROOT = Path(__file__).resolve().parents[1]
APP = "endorphin-tabularium-docs"
INPUTS = ["Dockerfile", "Dockerfile.ci", ".dockerignore", ".npmrc", "package.json",
"pnpm-lock.yaml", "pnpm-workspace.yaml", "apps/docs", "content", "tools/figures", "scripts"]
def run(*args: str, capture: bool = False) -> str:
result = subprocess.run(args, cwd=ROOT, text=True, check=True,
stdout=subprocess.PIPE if capture else None)
return result.stdout.strip() if capture else ""
def fingerprint() -> str:
files = run("git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", *INPUTS, capture=True)
digest = hashlib.sha256()
for name in sorted(set(files.split("\0"))):
if not name:
continue
data = (ROOT / name).read_bytes()
digest.update(name.encode() + b"\0" + str(len(data)).encode() + b"\0" + data)
return digest.hexdigest()
def smoke_image(image: str, name: str) -> None:
from smoke import verify
run("docker", "run", "-d", "--name", name, "-p", "127.0.0.1::3000", image)
try:
port = run("docker", "port", name, "3000/tcp", capture=True).rsplit(":", 1)[1]
for _ in range(60):
state = json.loads(run("docker", "inspect", name, capture=True))[0]["State"]
if state.get("Health", {}).get("Status") == "healthy":
verify(f"http://127.0.0.1:{port}")
return
if not state["Running"]:
break
time.sleep(1)
run("docker", "logs", "--tail", "80", name)
raise RuntimeError("Production image did not become healthy")
finally:
run("docker", "rm", "-f", name)
def main() -> None:
os.chdir(ROOT)
run(sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py")
scope = os.environ.get("CI_CACHE_SCOPE", "main")
if not re.fullmatch(r"main|pr-[0-9]+", scope):
raise ValueError("Unsupported cache scope")
with open(f"/var/lock/{APP}-{scope}-build.lock", "w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
key = fingerprint()
verified = f"{APP}:verified-{scope}-{key}"
result = subprocess.run(["docker", "image", "inspect", verified],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
smoke_name = f"tabularium-smoke-{scope}-{os.getpid()}"
if result.returncode == 0:
print(f"CACHE HIT: reuse {verified}; no dependency install or next build", flush=True)
smoke_image(verified, smoke_name)
else:
print(f"CACHE MISS: {key}; dependency and Next.js caches remain persistent", flush=True)
checks = f"{APP}-checks:{scope}"
run("docker", "build", "-f", "Dockerfile.ci", "--target", "checks", "-t", checks, ".")
name = f"tabularium-checks-{scope}-{os.getpid()}"
output = ROOT / ".artifacts/runtime"
if output.exists():
shutil.rmtree(output)
output.mkdir(parents=True)
try:
run("docker", "run", "--name", name,
"--mount", f"type=volume,src={APP}-{scope}-pnpm,dst=/pnpm/store",
"--mount", f"type=volume,src={APP}-{scope}-uv,dst=/root/.cache/uv",
"--mount", f"type=volume,src={APP}-{scope}-next,dst=/workspace/apps/docs/.next/cache",
checks)
run("docker", "cp", f"{name}:/workspace/apps/docs/.next/standalone/.", str(output))
app = output / "apps/docs"
(app / ".next").mkdir(parents=True, exist_ok=True)
run("docker", "cp", f"{name}:/workspace/apps/docs/.next/static", str(app / ".next/static"))
run("docker", "cp", f"{name}:/workspace/apps/docs/public", str(app / "public"))
candidate = f"{APP}:candidate-{scope}"
run("docker", "build", "-f", str(ROOT / "Dockerfile"),
"--label", f"io.entroia.tabularium.fingerprint={key}",
"-t", candidate, str(output))
smoke_image(candidate, smoke_name)
run("docker", "tag", candidate, verified)
finally:
subprocess.run(["docker", "rm", "-f", name], check=False)
(ROOT / ".artifacts").mkdir(exist_ok=True)
(ROOT / ".artifacts/verified-image").write_text(verified + "\n")
print(f"VERIFIED IMAGE: {verified}", flush=True)
if __name__ == "__main__":
main()