#!/usr/bin/env python3 """check_version_surfaces.py — the LIGHTWEIGHT version-bump gate. Nick's policy (2026-09-08): "version bumps don't need to do the full pipeline — just a check that the code that updates the TUI version is correct." The TUI displays the version via VersionLabel.render() which reads rune.__version__ AT CALL TIME (src/rune/rune_tui.py:990) — so __init__.py IS what the footer shows. The gate proves every recorded version surface agrees with the bump target: src/rune/__init__.py __version__ = "X.Y.Z" (canonical) src/rune/rune_tui.py Version: vX.Y.Z (header) src/rune/canary_gitea_automation.py '__version__ = "X.Y.Z"' pyproject.toml version = "X.Y.Z" README.md vX.Y.Z Exit 0 = all surfaces agree; exit 1 = named drifts. This replaces the full CI pipeline on bump PRs: the version surfaces are the only code a bump contributes, so they are the only thing worth re-checking. """ from __future__ import annotations import argparse import re import sys from pathlib import Path VER = r"\d+\.\d+\.\d+" def _surface(root: Path, rel: str, pattern: str, description: str, version: str) -> str | None: path = root / rel if not path.exists(): return f"missing surface: {rel} ({description})" text = path.read_text(encoding="utf-8", errors="replace") hits = re.findall(pattern, text) if not hits: return f"{rel}: pattern not found (expected {description})" if version not in hits: return f"{rel}: found {sorted(set(hits))} — expected {version} ({description})" return None def check(root: Path, version: str) -> list[str]: errs = [] checks = [ ("src/rune/__init__.py", rf'__version__ = "({VER})"', "__version__ assignment"), ("src/rune/rune_tui.py", rf"Version: v({VER})", "TUI header comment"), ("src/rune/canary_gitea_automation.py", rf"'__version__ = \"({VER})\"'", "canary assertion literal"), ("pyproject.toml", rf'version = "({VER})"', "wheel metadata"), ("README.md", rf"v({VER})", "README badge"), ] for rel, pattern, desc in checks: err = _surface(root, rel, pattern, desc, version) if err: errs.append(err) return errs def main_with(argv: list[str]) -> int: parser = argparse.ArgumentParser(description="Lightweight version-surface gate for bump PRs") parser.add_argument("cmd", choices=["check"]) parser.add_argument("--root", required=True) parser.add_argument("--version", required=True) args = parser.parse_args(argv) # caller passes args directly (no prog prefix) errs = check(Path(args.root), args.version) if errs: print("version-surface drift:", file=sys.stderr) for e in errs: print(f" - {e}", file=sys.stderr) return 1 print(f"version surfaces agree: {args.version} (TUI footer will show v{args.version})") return 0 if __name__ == "__main__": sys.exit(main_with(sys.argv[1:])) # python strips script name; keep that convention