Public Access
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / vikunja (push) Successful in 18s
Post-merge / release (push) Successful in 36s
Post-merge / publish (push) Successful in 20s
Post-merge / badges (push) Successful in 35s
111 lines
3.6 KiB
Python
111 lines
3.6 KiB
Python
#!/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 — tea must be installed and configured.
|
|
|
|
Usage:
|
|
CI_GITEA_API_TOKEN=<token> python3 -m devx.ci.notify_failure \
|
|
--repo <owner/repo> \
|
|
--run-id <run_id> \
|
|
--workflow <workflow_name> \
|
|
--commit <commit_sha> \
|
|
--auto-login
|
|
|
|
With ``--auto-login``, the script configures the tea CLI login profile
|
|
from the CI API token and ``DEVX_GITEA_API_URL`` before creating the issue,
|
|
eliminating the need for a separate ``tea login add`` step in the workflow.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import click
|
|
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
|
|
|
from devx.config import GITEA_API_URL
|
|
from devx.gitea_cli import TeaCLI, TeaCLIError, configure_tea_login
|
|
from devx.i18n import _
|
|
from devx.tokens import get_ci_token
|
|
|
|
load_dotenv()
|
|
|
|
logger = logging.getLogger("devx")
|
|
|
|
|
|
def _create_issue_via_tea(repo: str, title: str, body: str) -> int:
|
|
"""Create issue via tea CLI. Returns issue index.
|
|
|
|
Raises TeaCLIError if tea is not installed or the command fails.
|
|
Label operations are best-effort — failures are logged but don't
|
|
prevent issue creation.
|
|
"""
|
|
tea = TeaCLI(repo=repo)
|
|
|
|
# Check if "bug" label exists (best-effort)
|
|
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 as e:
|
|
logger.warning("Could not fetch labels (best-effort): %s", e)
|
|
|
|
issue = tea.create_issue(repo, title=title, body=body, labels=labels if labels else None)
|
|
if labels:
|
|
try:
|
|
tea.add_label(repo, issue["index"], labels)
|
|
except TeaCLIError as e:
|
|
logger.warning("Could not add label to issue #%s (best-effort): %s", issue.get("index"), e)
|
|
return int(issue.get("index", 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.")
|
|
@click.option(
|
|
"--auto-login",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Configure tea CLI login from CI_GITEA_TOKEN before creating the issue.",
|
|
)
|
|
def main(repo: str, run_id: str, workflow: str, commit: str, auto_login: bool) -> None:
|
|
try:
|
|
get_ci_token()
|
|
except click.ClickException:
|
|
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) from None
|
|
|
|
if auto_login:
|
|
configure_tea_login()
|
|
|
|
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:
|
|
issue_id = _create_issue_via_tea(repo, title, body)
|
|
except TeaCLIError as e:
|
|
raise click.ClickException(_("Failed to create issue via tea: {error}", error=str(e))) from None
|
|
|
|
click.echo(
|
|
_(
|
|
"Created issue #{issue_id}: {title}",
|
|
issue_id=issue_id or "?",
|
|
title=title,
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
main()
|