#!/usr/bin/env python3 """Configure GRM repository: branch protection + labels via Gitea REST API. Usage: GITEA_ADMIN_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()