GRM-113: feat: upgrade all dependencies, add trigger-workflow command
Post-merge / detect-type (push) Successful in 55s
Post-merge / release (push) Successful in 1m19s
Post-merge / validate-commit-msg (push) Successful in 1m19s
Post-merge / badges (push) Successful in 1m33s
Post-merge / vikunja (push) Successful in 1m13s
Post-merge / configure-repo (push) Successful in 1m13s
Post-merge / sync-wiki (push) Successful in 1m54s
Post-merge / publish (push) Successful in 55s

This commit was merged in pull request #180.
This commit is contained in:
2026-06-28 13:13:06 +00:00
parent c1f68f115a
commit d9ce4e240f
11 changed files with 488 additions and 9 deletions
+80
View File
@@ -14,6 +14,7 @@ from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnk
from . import __version__
from .exceptions import GRMError
from .gitea_client import GiteaAPIError, GiteaWorkflowClient
from .i18n import _
from .runner_manager import RunnerManager
@@ -468,3 +469,82 @@ def list_runners(ask_become_pass: bool, no_status: bool) -> None:
click.echo("-" * 90)
for r in runners:
click.echo(f"{r['name']:<18} {r['host']:<16} {r['user']:<10} {r['labels']:<30} {r['status']}")
@cli.command(name="trigger-workflow", help=_("Trigger a Gitea Actions workflow via the API."))
@click.argument("workflow_id", required=False)
@click.option(
"--repo",
default=lambda: os.getenv("GRM_REPO", "oblachno-oss/grm"),
help=_("Repository in owner/repo format (env: GRM_REPO, default: oblachno-oss/grm)"),
)
@click.option(
"--ref",
default="master",
help=_("Git ref to run the workflow on (default: master)"),
)
@click.option(
"--url",
default=lambda: os.getenv("GITEA_URL", ""),
help=_("Gitea URL (env: GITEA_URL)"),
)
@click.option(
"--token",
default=lambda: os.getenv("CI_GITEA_TOKEN"),
help=_("Gitea API token (env: CI_GITEA_TOKEN)"),
)
@click.option(
"--list",
"list_only",
is_flag=True,
default=False,
help=_("List available workflows instead of triggering one"),
)
def trigger_workflow(
workflow_id: str,
repo: str,
ref: str,
url: str,
token: str | None,
list_only: bool,
) -> None:
"""Trigger a Gitea Actions workflow dispatch event."""
if not url:
raise click.ClickException(_("GITEA_URL is required (set --url or GITEA_URL env var)"))
if not token:
raise click.ClickException(_("CI_GITEA_TOKEN is required (set --token or CI_GITEA_TOKEN env var)"))
if not list_only and not workflow_id:
raise click.ClickException(_("WORKFLOW_ID is required unless --list is used"))
client = GiteaWorkflowClient(url, token)
owner, repo_name = repo.split("/", 1)
if list_only:
workflows = client.list_workflows(owner, repo_name)
if not workflows:
click.echo(_("No workflows found in {repo}", repo=repo))
return
click.echo(f"{'ID':<30} {'NAME':<20} {'PATH':<25} {'STATE'}")
click.echo("-" * 85)
for wf in workflows:
wf_id = str(wf.get("id", ""))
wf_name = wf.get("name", "")
wf_path = wf.get("path", "")
wf_state = wf.get("state", "")
click.echo(f"{wf_id:<30} {wf_name:<20} {wf_path:<25} {wf_state}")
return
click.echo(_("Triggering workflow {wf} on {repo}@{ref}...", wf=workflow_id, repo=repo, ref=ref))
try:
result = client.dispatch_workflow(owner, repo_name, workflow_id, ref)
except GiteaAPIError as e:
raise click.ClickException(str(e)) from e
if result and result.get("id"):
run_id: Any = result["id"]
click.echo(_("Workflow triggered successfully. Run ID: {run_id}", run_id=run_id))
if result.get("html_url"):
html_url: Any = result["html_url"]
click.echo(f" {html_url}")
else:
click.echo(_("Workflow triggered successfully."))
+86
View File
@@ -0,0 +1,86 @@
"""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)
@@ -630,5 +630,101 @@
"pl": "nieznany",
"ru": "неизвестно",
"zh": "未知"
},
"CI_GITEA_TOKEN is required (set --token or CI_GITEA_TOKEN env var)": {
"bg": "CI_GITEA_TOKEN е задължителен (задайте --token или CI_GITEA_TOKEN env var)",
"de": "CI_GITEA_TOKEN ist erforderlich (setzen Sie --token oder CI_GITEA_TOKEN env var)",
"en": "CI_GITEA_TOKEN is required (set --token or CI_GITEA_TOKEN env var)",
"pl": "CI_GITEA_TOKEN jest wymagany (ustaw --token lub CI_GITEA_TOKEN env var)",
"ru": "CI_GITEA_TOKEN обязателен (установите --token или CI_GITEA_TOKEN env var)",
"zh": "需要 CI_GITEA_TOKEN(设置 --token 或 CI_GITEA_TOKEN 环境变量)"
},
"GITEA_URL is required (set --url or GITEA_URL env var)": {
"bg": "GITEA_URL е задължителен (задайте --url или GITEA_URL env var)",
"de": "GITEA_URL ist erforderlich (setzen Sie --url oder GITEA_URL env var)",
"en": "GITEA_URL is required (set --url or GITEA_URL env var)",
"pl": "GITEA_URL jest wymagany (ustaw --url lub GITEA_URL env var)",
"ru": "GITEA_URL обязателен (установите --url или GITEA_URL env var)",
"zh": "需要 GITEA_URL(设置 --url 或 GITEA_URL 环境变量)"
},
"Git ref to run the workflow on (default: master)": {
"bg": "Git ref за изпълнение на работния процес (по подразбиране: master)",
"de": "Git-Ref für die Workflow-Ausführung (Standard: master)",
"en": "Git ref to run the workflow on (default: master)",
"pl": "Git ref do uruchomienia workflow (domyślnie: master)",
"ru": "Git ref для запуска workflow (по умолчанию: master)",
"zh": "运行工作流的 Git ref(默认:master"
},
"Gitea API token (env: CI_GITEA_TOKEN)": {
"bg": "Gitea API токен (env: CI_GITEA_TOKEN)",
"de": "Gitea API-Token (env: CI_GITEA_TOKEN)",
"en": "Gitea API token (env: CI_GITEA_TOKEN)",
"pl": "Token API Gitea (env: CI_GITEA_TOKEN)",
"ru": "Токен API Gitea (env: CI_GITEA_TOKEN)",
"zh": "Gitea API 令牌(环境变量:CI_GITEA_TOKEN"
},
"List available workflows instead of triggering one": {
"bg": "Списък на наличните работни процеси вместо изпълнение",
"de": "Verfügbare Workflows auflisten statt auszuführen",
"en": "List available workflows instead of triggering one",
"pl": "Wyświetl dostępne workflow zamiast uruchamiać",
"ru": "Список доступных workflow вместо запуска",
"zh": "列出可用工作流而不是触发"
},
"No workflows found in {repo}": {
"bg": "Няма намерени работни процеси в {repo}",
"de": "Keine Workflows in {repo} gefunden",
"en": "No workflows found in {repo}",
"pl": "Nie znaleziono workflow w {repo}",
"ru": "В {repo} не найдено workflow",
"zh": "在 {repo} 中未找到工作流"
},
"Repository in owner/repo format (env: GRM_REPO, default: oblachno-oss/grm)": {
"bg": "Хранилище във формат owner/repo (env: GRM_REPO, по подразбиране: oblachno-oss/grm)",
"de": "Repository im owner/repo-Format (env: GRM_REPO, Standard: oblachno-oss/grm)",
"en": "Repository in owner/repo format (env: GRM_REPO, default: oblachno-oss/grm)",
"pl": "Repozytorium w formacie owner/repo (env: GRM_REPO, domyślnie: oblachno-oss/grm)",
"ru": "Репозиторий в формате owner/repo (env: GRM_REPO, по умолчанию: oblachno-oss/grm)",
"zh": "仓库格式为 owner/repo(环境变量:GRM_REPO,默认:oblachno-oss/grm"
},
"Trigger a Gitea Actions workflow via the API.": {
"bg": "Стартиране на Gitea Actions работен процес чрез API.",
"de": "Einen Gitea Actions-Workflow über die API auslösen.",
"en": "Trigger a Gitea Actions workflow via the API.",
"pl": "Uruchom workflow Gitea Actions przez API.",
"ru": "Запустить workflow Gitea Actions через API.",
"zh": "通过 API 触发 Gitea Actions 工作流。"
},
"Triggering workflow {wf} on {repo}@{ref}...": {
"bg": "Стартиране на работен процес {wf} в {repo}@{ref}...",
"de": "Workflow {wf} auf {repo}@{ref} wird ausgelöst...",
"en": "Triggering workflow {wf} on {repo}@{ref}...",
"pl": "Uruchamianie workflow {wf} na {repo}@{ref}...",
"ru": "Запуск workflow {wf} на {repo}@{ref}...",
"zh": "正在触发工作流 {wf} 于 {repo}@{ref}..."
},
"WORKFLOW_ID is required unless --list is used": {
"bg": "WORKFLOW_ID е задължителен, освен ако не се използва --list",
"de": "WORKFLOW_ID ist erforderlich, es sei denn --list wird verwendet",
"en": "WORKFLOW_ID is required unless --list is used",
"pl": "WORKFLOW_ID jest wymagany, chyba że użyto --list",
"ru": "WORKFLOW_ID обязателен, если не используется --list",
"zh": "除非使用 --list,否则需要 WORKFLOW_ID"
},
"Workflow triggered successfully.": {
"bg": "Работният процес е стартиран успешно.",
"de": "Workflow erfolgreich ausgelöst.",
"en": "Workflow triggered successfully.",
"pl": "Workflow uruchomiony pomyślnie.",
"ru": "Workflow успешно запущен.",
"zh": "工作流触发成功。"
},
"Workflow triggered successfully. Run ID: {run_id}": {
"bg": "Работният процес е стартиран успешно. ID на изпълнение: {run_id}",
"de": "Workflow erfolgreich ausgelöst. Run-ID: {run_id}",
"en": "Workflow triggered successfully. Run ID: {run_id}",
"pl": "Workflow uruchomiony pomyślnie. ID uruchomienia: {run_id}",
"ru": "Workflow успешно запущен. ID запуска: {run_id}",
"zh": "工作流触发成功。运行 ID{run_id}"
}
}