142 lines
4.5 KiB
Python
142 lines
4.5 KiB
Python
#!/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())
|