#!/usr/bin/env python3 """Create a Gitea issue when a CI workflow fails. Used by the release and publish workflows to alert on failures that would otherwise go unnoticed in the Actions tab. Uses the ``tea`` Gitea CLI for issue creation when available, falling back to ``GiteaClient`` (direct HTTP API) when tea is not installed. Usage: REPO_TOKEN= python3 scripts/notify_failure.py \ --repo \ --run-id \ --workflow \ --commit """ from __future__ import annotations import contextlib import os import shutil import click from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType] from gitea_runner_manager.api_clients import GiteaClient from gitea_runner_manager.config import GITEA_API_URL from gitea_runner_manager.exceptions import APIError from gitea_runner_manager.i18n import _ load_dotenv(override=True) def _create_issue_via_tea(repo: str, title: str, body: str) -> int | None: """Try creating issue via tea CLI. Returns issue index or None on failure.""" if shutil.which("tea") is None: return None from scripts.gitea_cli import TeaCLI, TeaCLIError tea = TeaCLI(repo=repo) try: # Check if "bug" label exists labels: list[str] = [] try: existing_labels = tea.list_labels(repo) if any(label.get("name") == "bug" for label in existing_labels): labels = ["bug"] except TeaCLIError: pass issue = tea.create_issue(repo, title=title, body=body, labels=labels if labels else None) if labels: with contextlib.suppress(TeaCLIError): tea.add_label(repo, issue["index"], labels) return int(issue.get("index", 0)) except TeaCLIError: return None def _create_issue_via_client(repo: str, title: str, body: str) -> int: """Create issue via GiteaClient (direct HTTP API). Returns issue ID.""" token = os.environ.get("REPO_TOKEN", "") owner, repo_name = repo.split("/") client = GiteaClient(GITEA_API_URL, token, owner, repo_name) # Look up label IDs by name (Gitea API expects integer IDs, not strings) label_ids: list[int] = [] for label in client.list_labels(): if label.get("name") == "bug": label_ids.append(int(label["id"])) break issue = client.create_issue(title=title, body=body, labels=label_ids if label_ids else None) return int(issue.get("id", 0)) @click.command() @click.option("--repo", required=True, help="Repository in owner/name format.") @click.option("--run-id", required=True, help="CI run ID.") @click.option("--workflow", required=True, help="Workflow name.") @click.option("--commit", required=True, help="Commit SHA.") def main(repo: str, run_id: str, workflow: str, commit: str) -> None: token = os.environ.get("REPO_TOKEN", "") if not token: raise click.ClickException(_("ERROR: REPO_TOKEN is not set.")) title = f"[CI] {workflow} workflow failed (run #{run_id})" body = ( f"The **{workflow}** workflow failed.\n\n" f"- **Run ID**: #{run_id}\n" f"- **Commit**: `{commit[:8]}`\n" f"- **Check the logs**: {GITEA_API_URL.replace('/api/v1', '')}/" f"{repo}/actions/runs/{run_id}\n\n" f"Please investigate and fix the issue." ) # Try tea CLI first, fall back to GiteaClient issue_id = _create_issue_via_tea(repo, title, body) if issue_id is None: try: issue_id = _create_issue_via_client(repo, title, body) except APIError as e: raise click.ClickException( _("Failed to create issue: HTTP {status} — {message}", status=e.status, message=e.message) ) from None click.echo( _( "Created issue #{issue_id}: {title}", issue_id=issue_id or "?", title=title, ) ) if __name__ == "__main__": # pragma: no cover main()