Files
endorphin-tabularium-docs/scripts/ci.py
T
codex-bot 585c602000
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 4m6s
Use coordinated docs port with conflict checks and deployment-independent cache keys
2026-10-26 20:20:50 +08:00

108 lines
4.8 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:
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",
"--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()