#!/usr/bin/env python3 """Configure and verify protected GovOPlaN package-release boundaries.""" from __future__ import annotations import argparse import json from pathlib import Path import sys from gitea_common import ( GiteaClient, GiteaError, RepoTarget, load_dotenv, org_path, quote_path, repo_path, require_token, ) META_ROOT = Path(__file__).resolve().parents[2] REQUIRED_SECRETS = {"GOVOPLAN_PACKAGE_USERNAME", "GOVOPLAN_PACKAGE_TOKEN"} def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--url", default="https://git.add-ideas.de") parser.add_argument("--owner", default="GovOPlaN") parser.add_argument("--team", default="Owners") parser.add_argument("--pattern", default="v*") parser.add_argument("--env-file", type=Path) parser.add_argument("--apply", action="store_true") return parser def package_repositories() -> tuple[str, ...]: inventory = json.loads((META_ROOT / "repositories.json").read_text(encoding="utf-8")) values = ["govoplan"] for item in inventory["repositories"]: name = str(item["name"]) repository = META_ROOT.parent / str(item["path"]) if name.startswith("govoplan-") and (repository / "pyproject.toml").is_file(): values.append(name) return tuple(sorted(set(values))) def configure( client: GiteaClient, *, owner: str, team: str, pattern: str, apply: bool, ) -> tuple[str, ...]: missing: list[str] = [] expected = { "name_pattern": pattern, "whitelist_teams": [team], "whitelist_usernames": [], } for repository in package_repositories(): path = repo_path(owner, repository, "/tag_protections") protections = client.request_json("GET", path) matching = [ item for item in protections if isinstance(item, dict) and item.get("name_pattern") == pattern ] if len(matching) == 1 and _matches(matching[0], expected): print(f"protected {repository}:{pattern}") continue missing.append(repository) if not apply: print(f"would protect {repository}:{pattern}") continue if len(matching) == 1: protection_id = matching[0].get("id") client.request_json( "PATCH", f"{path}/{quote_path(str(protection_id))}", body=expected, ) print(f"updated {repository}:{pattern}") elif not matching: client.request_json("POST", path, body=expected) print(f"created {repository}:{pattern}") else: raise GiteaError(f"{repository} has duplicate {pattern!r} tag protections") return tuple(missing) def _matches(value: dict[str, object], expected: dict[str, object]) -> bool: return ( value.get("name_pattern") == expected["name_pattern"] and sorted(value.get("whitelist_teams") or []) == expected["whitelist_teams"] and sorted(value.get("whitelist_usernames") or []) == expected["whitelist_usernames"] ) def main() -> int: args = build_parser().parse_args() try: load_dotenv(args.env_file) token = require_token() target = RepoTarget(base_url=args.url, owner=args.owner, repo="govoplan") with GiteaClient(target, token) as client: mismatches = configure( client, owner=args.owner, team=args.team, pattern=args.pattern, apply=args.apply, ) secrets = client.request_json( "GET", org_path(args.owner, "/actions/secrets"), query={"limit": 50} ) names = { str(item.get("name") or "") for item in secrets if isinstance(item, dict) } missing_secrets = sorted(REQUIRED_SECRETS - names) if missing_secrets: print( "Missing organization Actions secrets: " + ", ".join(missing_secrets), file=sys.stderr, ) unresolved = (bool(mismatches) and not args.apply) or bool(missing_secrets) if unresolved: return 1 print("Package release protection and credential names are configured.") return 0 except (GiteaError, OSError, ValueError, json.JSONDecodeError) as exc: print(f"error: {exc}", file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())