Files
codex-bot d57e9b77d7
Verify database-backed documents / verify (pull_request) Failing after 6m6s
feat: make the site database authoritative for documents and catalog
2026-10-30 16:23:54 +08:00

131 lines
6.7 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", "scripts/summarize-ai-income.py",
"scripts/audit-ai-research.py", "scripts/export-legacy-documents.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", "-e", "DOCUMENT_SMOKE_TEST=1", "-p", "127.0.0.1::3000", image)
try:
run("docker", "exec", name, "node", "apps/docs/auth-cli.cjs", "init")
subprocess.run(["docker", "exec", "-i", name, "node", "apps/docs/docs-cli.cjs", "import"],
input=(ROOT / ".artifacts/documents-migration.json").read_bytes(), check=True)
run("docker", "exec", name, "node", "apps/docs/docs-cli.cjs", "export", "/auth/migrated-documents.json")
migrated = json.loads(run("docker", "exec", name, "cat", "/auth/migrated-documents.json", capture=True))
expected = json.loads((ROOT / ".artifacts/documents-migration.json").read_text())
if migrated != expected:
raise RuntimeError("Migration changed document content or catalog metadata")
run("docker", "exec", name, "node", "apps/docs/docs-cli.cjs", "backup", "/auth/smoke-backup.sqlite")
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)
migration = ROOT / ".artifacts/documents-migration.json"
migration.parent.mkdir(exist_ok=True)
migration.unlink(missing_ok=True)
run(sys.executable, "scripts/export-legacy-documents.py", "--output", str(migration))
run(sys.executable, "scripts/summarize-ai-income.py", "--check")
run(sys.executable, "scripts/audit-ai-research.py", "--check")
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()