Files
grm/scripts/configure_repo.py
T
Emil Simeonov 790b5c3769 GRM-20: refactor: rework all scripts to use click and i18n
- Replace argparse/print/sys.exit with click commands and ClickException
- Translate all user-facing messages via _()
- Add friendly Oops! / Nice! prompts
- Wrap HTTP errors in all scripts with user-friendly translated messages
- Update all unit tests to use CliRunner and expect ClickException
- Add 100% branch coverage for new HTTP error handling branches
- Add missing translation keys to i18n.py
- Fix pre-commit hook to use venv Python for validate_commit_msg.py
2026-06-19 20:11:20 +02:00

165 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Configure GRM repository: branch protection + labels via Gitea REST API.
Usage:
GITEA_ADMIN_TOKEN=<token> python3 scripts/configure_repo.py
"""
import http
import os
import click
import requests
from gitea_runner_manager.i18n import _
GITEA_API = "https://git.oblachno.oblachno.fyi/api/v1"
OWNER = "oblachno-oss"
REPO = "grm"
BRANCH_PROTECTION_CONFIG = {
"branch_name": "master",
"enable_push": False,
"enable_status_check": True,
"status_check_contexts": ["lint", "unit-tests", "molecule-tests"],
"required_approvals": 1,
"dismiss_stale_approvals": True,
"block_on_outdated_branch": True,
"block_on_rejected_reviews": True,
"block_on_official_review_requests": True,
}
LABEL_CONFIG = {
"name": "ready-to-merge",
"color": "2ecc71",
"description": "Auto-merge PR when all CI checks pass",
}
class GiteaRepoConfig:
"""Configure a Gitea repository: branch protection and labels."""
def __init__(self, base_url: str, token: str, owner: str, repo: str) -> None:
self._base_url = base_url.rstrip("/")
self._owner = owner
self._repo = repo
self._session = requests.Session()
self._session.headers.update(
{
"Authorization": f"token {token}",
"Content-Type": "application/json",
}
)
def _url(self, path: str) -> str:
return f"{self._base_url}/repos/{self._owner}/{self._repo}{path}"
def list_branch_protections(self) -> list[dict]:
r = self._session.get(self._url("/branch_protections"))
r.raise_for_status()
return r.json()
def create_branch_protection(self, config: dict) -> dict:
r = self._session.post(self._url("/branch_protections"), json=config)
r.raise_for_status()
return r.json()
def update_branch_protection(self, protection_id: int, config: dict) -> dict:
r = self._session.patch(self._url(f"/branch_protections/{protection_id}"), json=config)
r.raise_for_status()
return r.json()
def ensure_branch_protection(self, branch: str, config: dict) -> dict:
"""Idempotent: create or update branch protection for the given branch."""
existing = self.list_branch_protections()
for p in existing:
if p.get("branch_name") == branch:
protection_id = p["id"]
update_config = {k: v for k, v in config.items() if k != "branch_name"}
return self.update_branch_protection(protection_id, update_config)
return self.create_branch_protection(config)
def list_labels(self) -> list[dict]:
r = self._session.get(self._url("/labels"))
r.raise_for_status()
return r.json()
def create_label(self, name: str, color: str, description: str = "") -> dict:
r = self._session.post(
self._url("/labels"),
json={"name": name, "color": color, "description": description},
)
r.raise_for_status()
return r.json()
def ensure_label(self, name: str, color: str, description: str = "") -> dict | None:
"""Idempotent: create label if it doesn't already exist."""
labels = self.list_labels()
for label in labels:
if label["name"] == name:
return None # already exists
return self.create_label(name, color, description)
def _handle_http_error(e: requests.HTTPError) -> None:
"""Raise a user-friendly Click exception for HTTP errors."""
response = e.response
status = response.status_code if response else 0
if status == http.HTTPStatus.FORBIDDEN:
raise click.ClickException(
_(
"HTTP {status} Forbidden — your token lacks admin rights.\n"
"Make sure the token belongs to a repo owner or organisation admin.\n"
"Alternatively, configure branch protection manually in Settings → Branches.",
status=status,
)
)
try:
body = response.json() if response else {}
message = body.get("message", str(e))
except Exception:
message = str(e)
raise click.ClickException(_("HTTP error: {status} — {message}", status=status, message=message))
def main() -> None:
token = os.environ.get("GITEA_ADMIN_TOKEN", "")
if not token:
raise click.ClickException(_("ERROR: GITEA_ADMIN_TOKEN is not set."))
cfg = GiteaRepoConfig(GITEA_API, token, OWNER, REPO)
try:
click.echo(_("Configuring branch protection for {branch}...", branch="master"))
cfg.ensure_branch_protection("master", BRANCH_PROTECTION_CONFIG)
click.echo(_(" - Direct pushes: BLOCKED (require PR)"))
click.echo(
_(
" - Required approvals: {count}",
count=BRANCH_PROTECTION_CONFIG["required_approvals"],
)
)
click.echo(_(" - Dismiss stale approvals: yes"))
click.echo(_(" - Block outdated branches: yes"))
click.echo(_(" - Block rejected reviews: yes"))
checks = ", ".join(BRANCH_PROTECTION_CONFIG["status_check_contexts"])
click.echo(_(" - Required status checks: {checks}", checks=checks))
click.echo("")
label_name = LABEL_CONFIG["name"]
click.echo(_("Creating {label} label...", label=label_name))
result = cfg.ensure_label(**LABEL_CONFIG)
if result is None:
click.echo(_(" Label '{label}' already exists.", label=label_name))
else:
click.echo(_(" Label '{label}' created.", label=label_name))
click.echo("")
click.echo(_("Repository configuration complete."))
except requests.HTTPError as e:
_handle_http_error(e)
if __name__ == "__main__": # pragma: no cover
main()