#!/usr/bin/env python3 """Generate a dry-run selective GovOPlaN release plan.""" from __future__ import annotations import argparse from datetime import UTC, datetime import json from pathlib import Path from govoplan_release import build_dashboard, build_selective_release_plan from govoplan_release.model import to_jsonable from govoplan_release.workspace import DEFAULT_WORKSPACE_ROOT, META_ROOT def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--workspace-root", type=Path, default=DEFAULT_WORKSPACE_ROOT) parser.add_argument("--repo", action="append", default=[], help="Repository to include. May be repeated.") parser.add_argument("--target-version", help="Default target version for selected repositories.") parser.add_argument( "--repo-version", action="append", default=[], metavar="REPO=VERSION", help="Per-repository target version. May be repeated.", ) parser.add_argument("--channel", default="stable", help="Release channel to plan for. Defaults to stable.") parser.add_argument("--online", action="store_true", help="Check published catalog/keyring.") parser.add_argument("--remote-tags", action="store_true", help="Also check remote Git tags. This can be slow across many repositories.") parser.add_argument("--include-migrations", action="store_true", help="Run migration audits before planning.") parser.add_argument("--output", type=Path, help="Write JSON plan to this path.") args = parser.parse_args() dashboard = build_dashboard( workspace_root=args.workspace_root, target_version=args.target_version, online=args.online, check_remote_tags=args.remote_tags, check_public_catalog=args.online, include_migrations=args.include_migrations, channel=args.channel, ) plan = build_selective_release_plan( dashboard, selected_repos=tuple(args.repo), target_version=args.target_version, repo_versions=parse_repo_versions(tuple(args.repo_version)), channel=args.channel, ) payload = json.dumps(to_jsonable(plan), indent=2, sort_keys=True) + "\n" if args.output: output = args.output.expanduser() if not output.is_absolute(): output = META_ROOT / output output.parent.mkdir(parents=True, exist_ok=True) output.write_text(payload, encoding="utf-8") print(output) else: print(payload, end="") return 1 if plan.status == "blocked" else 0 def parse_repo_versions(values: tuple[str, ...]) -> dict[str, str]: result: dict[str, str] = {} for value in values: if "=" not in value: raise SystemExit(f"--repo-version must use REPO=VERSION: {value}") repo, version = value.split("=", 1) repo = repo.strip() version = version.strip() if not repo or not version: raise SystemExit(f"--repo-version must use REPO=VERSION: {value}") result[repo] = version return result def default_plan_path(channel: str) -> Path: stamp = datetime.now(tz=UTC).strftime("%Y%m%d-%H%M%S") return META_ROOT / "runtime" / "release-plans" / f"{channel}-{stamp}.json" if __name__ == "__main__": raise SystemExit(main())