Files
rune/scripts/check_version_surfaces.py

81 lines
3.0 KiB
Python

#!/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