Files
endorphin-tabularium-docs/scripts/ci.py
T
codex-bot 9fcfbdb694
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m28s
Protect Tabularium with persistent authenticator-code login
2026-10-27 21:48:16 +08:00

116 lines
5.5 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/check-container.sh", "scripts/ci.py"]
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:
run("docker", "run", "-d", "--name", name, "--mount", "type=volume,dst=/auth", "-p", "127.0.0.1::3000", image)
try:
run("docker", "exec", name, "node", "apps/docs/auth-cli.cjs", "init")
for _ in range(60):
state = json.loads(run("docker", "inspect", name, capture=True))[0]["State"]
if state.get("Health", {}).get("Status") == "healthy":
run("docker", "exec", name, "node", "apps/docs/auth-cli.cjs", "exercise")
run("docker", "restart", name)
# Counter/replay state is on the same durable mount after restart.
run("docker", "exec", name, "node", "apps/docs/auth-cli.cjs", "state-check")
for _ in range(30):
state = json.loads(run("docker", "inspect", name, capture=True))[0]["State"]
if state.get("Health", {}).get("Status") == "healthy":
run("docker", "exec", name, "node", "apps/docs/auth-cli.cjs", "verify")
return
time.sleep(1)
raise RuntimeError("Authenticated image failed restart verification")
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", "-fv", 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",
"--mount", f"type=volume,src={APP}-{scope}-swc,dst=/root/.cache/next-swc",
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()