#!/usr/bin/env python3 """Sync byte-identical workflows from workflows-src/ to .gitea/ + .github/ (P8, REQ-172). Three workflow pairs are byte-identical Gitea + GitHub mirrors: ci.yml, deploy.yml, modules-lifecycle.yml. This generator reads the single source from ``workflows-src/`` and writes byte-identical copies to both ``.gitea/workflows/`` and ``.github/workflows/``. Use ``--check`` to verify the committed files match the generated output (CI gate); use ``--write`` to regenerate the committed files from the sources. The 4 GitHub-only workflows (platform-test.yml, primitives-plan.yml, patterns-plan.yml, release.yml) have no Gitea mirror (act_runner feature gaps) and are NOT touched by this generator. """ from __future__ import annotations import argparse import filecmp import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent SRC_DIR = ROOT / "workflows-src" GITEA_DIR = ROOT / ".gitea" / "workflows" GITHUB_DIR = ROOT / ".github" / "workflows" PAIRS = ["ci.yml", "deploy.yml", "modules-lifecycle.yml"] def _read_source(name: str) -> str: src = SRC_DIR / name if not src.is_file(): raise FileNotFoundError(f"source {src} missing") return src.read_text() def check() -> int: """Verify committed files match the sources. Exit 0 if clean, 1 if drift.""" drift = [] for name in PAIRS: content = _read_source(name) for dest_dir in (GITEA_DIR, GITHUB_DIR): dest = dest_dir / name if not dest.is_file(): drift.append(f"{dest} MISSING (expected from workflows-src/{name})") continue if dest.read_text() != content: drift.append(f"{dest} DRIFTED from workflows-src/{name}") if drift: for d in drift: print(f"DRIFT: {d}", file=sys.stderr) print("\nRun: python3 scripts/sync_workflows.py --write", file=sys.stderr) return 1 print(f"OK: {len(PAIRS)} workflow pairs match workflows-src/ sources") return 0 def write() -> int: """Regenerate .gitea/ + .github/ from workflows-src/ sources.""" for name in PAIRS: content = _read_source(name) for dest_dir in (GITEA_DIR, GITHUB_DIR): dest_dir.mkdir(parents=True, exist_ok=True) (dest_dir / name).write_text(content) print(f"wrote: .gitea/workflows/{name} + .github/workflows/{name}") return 0 def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Sync byte-identical workflow pairs.") group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--check", action="store_true", help="verify committed files match sources (CI gate)") group.add_argument("--write", action="store_true", help="regenerate committed files from sources") args = parser.parse_args(argv) if args.check: return check() return write() if __name__ == "__main__": sys.exit(main())