- Add scripts/validate_commit_msg.py with conventional commit enforcement - Add scripts/configure_repo.py for Gitea branch protection and labels - Add scripts/__init__.py for Python package importability - Create Gitea Actions workflows: ci, auto-merge, post-merge, publish - Update .pre-commit-config.yaml with commit-msg hook - Update pyproject.toml pythonpath and coverage for scripts - Add comprehensive unit tests for both scripts with 100% coverage
132 lines
4.3 KiB
Python
132 lines
4.3 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 os
|
|
import sys
|
|
|
|
import requests
|
|
|
|
|
|
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 main() -> None:
|
|
token = os.environ.get("GITEA_ADMIN_TOKEN", "")
|
|
if not token:
|
|
print("ERROR: GITEA_ADMIN_TOKEN is not set.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
cfg = GiteaRepoConfig(GITEA_API, token, OWNER, REPO)
|
|
|
|
print("Configuring branch protection for master...")
|
|
cfg.ensure_branch_protection("master", BRANCH_PROTECTION_CONFIG)
|
|
print(" - Direct pushes: BLOCKED (require PR)")
|
|
print(" - Required approvals: 1")
|
|
print(" - Dismiss stale approvals: yes")
|
|
print(" - Block outdated branches: yes")
|
|
print(" - Block rejected reviews: yes")
|
|
print(" - Required status checks: lint, unit-tests, molecule-tests")
|
|
|
|
print("")
|
|
print("Creating ready-to-merge label...")
|
|
result = cfg.ensure_label(**LABEL_CONFIG)
|
|
if result is None:
|
|
print(" Label 'ready-to-merge' already exists.")
|
|
else:
|
|
print(" Label 'ready-to-merge' created.")
|
|
|
|
print("")
|
|
print("Repository configuration complete.")
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
main()
|