Files
govoplan/tools/gitea/gitea-configure-package-releases.py
zemion f7590a7b8b
Dependency Audit / dependency-audit (push) Failing after 1m37s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m21s
Add registry-backed module package releases
2026-08-04 04:14:28 +02:00

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())