verify(P8): workflow-generator-dedup — 4-layer verify PASS + ship
VERIFY: structural — generator + sources; behavioral — 98 tests + CI PASS; quality — ~20KB dedup, single source of truth. ---ci--- project: acdl phase: 8 milestone: v1.16 status: complete phase_role: execution requirements: covered: [REQ-172] partial: [] ---/ci---
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
#!/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/<name>`` and
|
||||
writes byte-identical copies to both ``.gitea/workflows/<name>`` and
|
||||
``.github/workflows/<name>``. 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())
|
||||
Reference in New Issue
Block a user