#!/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 import time import urllib.request 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, env: dict[str, str] | None = None) -> str: print("+", " ".join(args), flush=True) result = subprocess.run(args, cwd=cwd, check=True, text=True, env=env, 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) server_env = dict(os.environ) server_env.update({"HOST": "127.0.0.1", "PORT": "4173", "TIMMY_VISION_ENABLED": "false"}) server_log = (release_dir / "acceptance-server.log").open("w", encoding="utf-8") server = subprocess.Popen(["node", "server.mjs"], cwd=tree, env=server_env, stdout=server_log, stderr=subprocess.STDOUT, text=True) try: for _ in range(50): if server.poll() is not None: raise SystemExit("Acceptance server exited before readiness") try: urllib.request.urlopen("http://127.0.0.1:4173/", timeout=1).read(1) break except Exception: time.sleep(0.1) else: raise SystemExit("Acceptance server did not become ready") run(["npm", "run", "test:ui"], tree) run(["npm", "run", "test:photo"], tree) run(["npm", "run", "test:sleek"], tree) demo_raw = release_dir / f"timmy-talking-turd-{version}-demo.raw.webm" demo_env = dict(server_env) demo_env["TIMMY_RELEASE_VERSION"] = version run(["node", "scripts/record_release_demo.mjs", str(demo_raw)], tree, env=demo_env) finally: server.terminate() try: server.wait(timeout=5) except subprocess.TimeoutExpired: server.kill() server.wait(timeout=5) server_log.close() demo = release_dir / f"timmy-talking-turd-{version}-demo.mp4" run(["ffmpeg", "-y", "-v", "error", "-i", str(demo_raw), "-vf", "scale=720:1280:flags=lanczos", "-an", "-c:v", "libx264", "-preset", "fast", "-crf", "20", "-pix_fmt", "yuv420p", "-movflags", "+faststart", str(demo)], tree) demo_raw.unlink() run(["ffmpeg", "-v", "error", "-i", str(demo), "-f", "null", "-"], tree) probe = json.loads(run(["ffprobe", "-v", "error", "-print_format", "json", "-show_streams", "-show_format", str(demo)], tree, capture=True)) video_streams = [stream for stream in probe.get("streams", []) if stream.get("codec_type") == "video"] if len(video_streams) != 1 or video_streams[0].get("codec_name") != "h264" or video_streams[0].get("pix_fmt") != "yuv420p" or video_streams[0].get("width") != 720 or video_streams[0].get("height") != 1280: raise SystemExit("Release demo media probe failed") contact_sheet = release_dir / f"timmy-talking-turd-{version}-demo-contact-sheet.jpg" run(["ffmpeg", "-y", "-v", "error", "-i", str(demo), "-vf", "fps=1/2,scale=180:-1,tile=4x3:padding=4:margin=4", "-frames:v", "1", str(contact_sheet)], 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/hermes-agent-service.js", "src/vision-config.js", "src/vision-service.js", "tests/staging.acceptance.mjs"): run(["node", "--check", file], tree) run(["bash", "-n", "scripts/bootstrap_selfhost_smolvlm.sh"], 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" "- Sleek three-destination shell with one dominant photo action and quiet manual fallback\n" "- Smart free-text Timmy chat backed by authenticated server-side Hermes session continuity\n" "- Agent safety: exact-origin HttpOnly session, bounded text-only ledger context, no browser credentials or session IDs\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) demo_digest = sha256(demo) 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, "feature_demo": { "artifact": demo.name, "bytes": demo.stat().st_size, "sha256": demo_digest, "width": 720, "height": 1280, "codec": "h264", "pixel_format": "yuv420p", "recorded_from_working_app": True, "fixture_data": "synthetic-Type-4, deterministic vision suggestion, and deterministic Hermes chat response", }, "gates": { "unit_security": "passed", "mobile_green_path": "passed", "photo_first_acceptance": "passed", "sleek_hermes_chat_acceptance": "passed", "dependency_audit_high": "passed", "syntax": "passed", "secret_scan": "passed", "forbidden_artifacts": "passed", "feature_demo_full_decode": "passed", "feature_demo_media_probe": "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{demo_digest} {demo.name}\n", encoding="utf-8") print(json.dumps({"release_dir": str(release_dir), "archive": str(archive), "feature_demo": str(demo), "manifest": str(manifest_path), "sha256": digest, "feature_demo_sha256": demo_digest}, indent=2)) return 0 if __name__ == "__main__": sys.exit(main())