#!/usr/bin/env python3 """Build a gated, reproducible Timmy review release from committed main.""" from __future__ import annotations import datetime as dt import hashlib import json import os from pathlib import Path import re import shutil import subprocess import sys import tarfile import tempfile ROOT = Path(__file__).resolve().parents[1] OUT_ROOT = Path(os.environ.get("TIMMY_RELEASE_DIR", "/root/timmy-releases")) PUBLIC_REPO = "https://forge.alexanderwhitestone.com/git/stackchain/timmy-talking-turd" EXCLUDED_PREFIXES = ("video/", "research/source-pages/", "artifacts/") EXCLUDED_SUFFIXES = (".gguf", ".bin", ".safetensors", ".onnx", ".pyc") SECRET_PATTERNS = ( re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"), re.compile(r"(?i)(?:api[_-]?key|access[_-]?token|authorization)\s*[:=]\s*['\"](?!\.\.\.|top-secret|test-|example|placeholder)[A-Za-z0-9_./+:-]{16,}"), ) def run(args: list[str], cwd: Path, *, capture: bool = False) -> str: print("+", " ".join(args), flush=True) result = subprocess.run(args, cwd=cwd, check=True, text=True, stdout=subprocess.PIPE if capture else None) return result.stdout.strip() if capture else "" def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def tracked_files(tree: Path) -> list[str]: names = run(["git", "ls-files"], tree, capture=True).splitlines() return sorted(name for name in names if name and not name.startswith(EXCLUDED_PREFIXES) and not name.endswith(EXCLUDED_SUFFIXES)) def scan_secrets(tree: Path, names: list[str]) -> None: forbidden_names = [name for name in names if Path(name).name == ".env" or name.endswith((".pem", ".key", ".p12"))] findings: list[str] = list(forbidden_names) for name in names: path = tree / name if not path.is_file() or path.stat().st_size > 2_000_000: continue try: text = path.read_text(encoding="utf-8") except UnicodeDecodeError: continue for pattern in SECRET_PATTERNS: if pattern.search(text): findings.append(name) break if findings: raise SystemExit("Secret scan failed: " + ", ".join(sorted(set(findings)))) def main() -> int: branch = run(["git", "branch", "--show-current"], ROOT, capture=True) if branch != "main": raise SystemExit(f"Release must build from main, not {branch!r}") status = run(["git", "status", "--porcelain"], ROOT, capture=True) if status: raise SystemExit("Release requires a clean committed tree") commit = run(["git", "rev-parse", "HEAD"], ROOT, capture=True) short = commit[:12] commit_epoch = int(run(["git", "show", "-s", "--format=%ct", commit], ROOT, capture=True)) source_date = dt.datetime.fromtimestamp(commit_epoch, dt.timezone.utc) release_date = dt.datetime.now(dt.timezone.utc).date().isoformat() version = f"{release_date}-{short}" release_dir = OUT_ROOT / version if release_dir.exists(): shutil.rmtree(release_dir) release_dir.mkdir(parents=True) with tempfile.TemporaryDirectory(prefix="timmy-release-") as tmp: tree = Path(tmp) / "source" run(["git", "clone", "--quiet", "--no-local", str(ROOT), str(tree)], ROOT) run(["git", "checkout", "--quiet", commit], tree) run(["npm", "ci", "--ignore-scripts"], tree) run(["npm", "test"], tree) run(["npm", "run", "test:ui"], tree) run(["npm", "run", "test:photo"], tree) run(["npm", "audit", "--audit-level=high"], tree) for file in ("app.js", "server.mjs", "service-worker.js", "src/analysis.js", "src/domain.js", "src/vision-config.js", "src/vision-service.js"): run(["node", "--check", file], tree) run(["bash", "-n", "scripts/run_selfhost_smolvlm.sh"], tree) run(["python3", "-m", "py_compile", "scripts/ingest_training_photo.py", "scripts/build_release.py"], tree) run(["git", "diff", "--check", commit], tree) names = tracked_files(tree) scan_secrets(tree, names) if any(name.startswith(EXCLUDED_PREFIXES) or name.endswith(EXCLUDED_SUFFIXES) for name in names): raise SystemExit("Release allowlist included a forbidden artifact") package_root = Path(tmp) / f"timmy-talking-turd-{version}" for name in names: src = tree / name dst = package_root / name dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dst) notes = ( f"# Timmy review release {version}\n\n" f"- Source commit: [{commit}]({PUBLIC_REPO}/commit/{commit})\n" f"- Build date (UTC): {release_date}\n" "- Vision profiles: hosted and self-hosted SmolVLM2 bootstrap\n" "- Safety: suggestions are observable-field assistance, never diagnosis; user confirmation is required\n" "- Known limitation: open-weight VLM acceptance is proven, Bristol accuracy is not clinically validated\n" "- Package excludes model weights, medical-image corpora, raw research pages, generated screenshots, and video production assets\n" ) (package_root / "RELEASE-NOTES.md").write_text(notes, encoding="utf-8") archive = release_dir / f"timmy-talking-turd-{version}.tar.gz" with archive.open("wb") as raw: import gzip with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=commit_epoch) as gz: with tarfile.open(fileobj=gz, mode="w") as tar: for path in sorted(package_root.rglob("*")): arcname = path.relative_to(package_root.parent) info = tar.gettarinfo(str(path), arcname=str(arcname)) info.uid = info.gid = 0 info.uname = info.gname = "" info.mtime = commit_epoch if path.is_file(): with path.open("rb") as handle: tar.addfile(info, handle) else: tar.addfile(info) digest = sha256(archive) manifest = { "schema_version": 1, "project": "Timmy the Talking Turd", "version": version, "commit": commit, "source_date_utc": source_date.isoformat(), "built_at_utc": dt.datetime.now(dt.timezone.utc).isoformat(), "artifact": archive.name, "bytes": archive.stat().st_size, "sha256": digest, "gates": { "unit_security": "passed", "mobile_green_path": "passed", "photo_first_acceptance": "passed", "dependency_audit_high": "passed", "syntax": "passed", "secret_scan": "passed", "forbidden_artifacts": "passed", }, } manifest_path = release_dir / "manifest.json" manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") (release_dir / "SHA256SUMS").write_text(f"{digest} {archive.name}\n", encoding="utf-8") print(json.dumps({"release_dir": str(release_dir), "archive": str(archive), "manifest": str(manifest_path), "sha256": digest}, indent=2)) return 0 if __name__ == "__main__": sys.exit(main())