"""Minimal Gitea API client for workflow operations. Uses urllib from the standard library to avoid adding requests as a runtime dependency. Only covers the Actions workflow dispatch endpoint. """ from __future__ import annotations import json import urllib.error import urllib.request # noqa: PTH123 # nosec B404 from contextlib import suppress from typing import Any class GiteaAPIError(Exception): """Raised when a Gitea API call fails.""" def __init__(self, status: int, message: str) -> None: super().__init__(f"Gitea API error {status}: {message}") self.status = status self.message = message class GiteaWorkflowClient: """Thin client for Gitea Actions workflow API endpoints.""" def __init__(self, base_url: str, token: str) -> None: self._base_url = base_url.rstrip("/") self._token = token def _request(self, method: str, path: str, body: dict[str, Any] | None = None) -> dict[str, Any] | None: url = f"{self._base_url}/api/v1{path}" data = json.dumps(body).encode("utf-8") if body else None req = urllib.request.Request( # nosec B310 url, data=data, method=method, ) req.add_header("Authorization", f"token {self._token}") req.add_header("Content-Type", "application/json") req.add_header("Accept", "application/json") try: with urllib.request.urlopen(req) as resp: # noqa: PTH123 # nosec B310 if resp.status == 204: return None raw = resp.read() return json.loads(raw) if raw else None except urllib.error.HTTPError as e: detail = e.read().decode("utf-8", errors="replace") with suppress(json.JSONDecodeError, ValueError): detail = json.loads(detail).get("message", detail) raise GiteaAPIError(e.code, detail) from e def list_workflows(self, owner: str, repo: str) -> list[dict[str, Any]]: """List all workflows in a repository.""" result = self._request("GET", f"/repos/{owner}/{repo}/actions/workflows") if result is None: return [] return result.get("workflows", []) def dispatch_workflow( self, owner: str, repo: str, workflow_id: str, ref: str = "master", inputs: dict[str, str] | None = None, ) -> dict[str, Any] | None: """Trigger a workflow dispatch event. Args: owner: Repository owner. repo: Repository name. workflow_id: Workflow file name (e.g. "ci.yml") or numeric ID. ref: Git ref (branch/tag) to run on. Defaults to "master". inputs: Optional workflow inputs. Returns: Run details dict if return_run_details is requested, else None. """ path = f"/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches?return_run_details=true" body: dict[str, Any] = {"ref": ref} if inputs: body["inputs"] = inputs return self._request("POST", path, body)