Files
grm/scripts/post_merge.py
T
emil 3364355c73
Post-merge Vikunja update / vikunja (push) Failing after 8s
CI / quality (push) Successful in 1m5s
CI / molecule-tests (1) (push) Successful in 6m44s
CI / molecule-tests (2) (push) Successful in 7m7s
CI / molecule-tests (0) (push) Successful in 9m35s
GRM-28: fix: Vikunja task resolution pagination in post_merge.py
2026-06-20 17:48:08 +00:00

109 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Update Vikunja task after a merge to master.
Usage:
VIKUNJA_TOKEN=<token> python3 scripts/post_merge.py <commit_msg> [--commit-sha <sha>]
"""
import os
import re
import click
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
from gitea_runner_manager.api_clients import VikunjaClient
from gitea_runner_manager.config import DEFAULT_PER_PAGE, TASK_ID_RE, VIKUNJA_API_URL, VIKUNJA_PROJECT_ID
from gitea_runner_manager.exceptions import APIError
from gitea_runner_manager.i18n import _
load_dotenv(override=True)
def extract_task_id(commit_msg: str) -> str:
"""Extract GRM-N task identifier from the first line of commit message."""
first_line = commit_msg.split("\n")[0]
match = TASK_ID_RE.search(first_line)
return match.group(0) if match else ""
def extract_conventional_msg(commit_msg: str) -> str:
"""Strip the GRM-N prefix from the commit subject."""
first_line = commit_msg.split("\n")[0]
return re.sub(r"^GRM-\d+:\s*", "", first_line)
def resolve_task_id(client: VikunjaClient, task_id: str) -> int:
"""Resolve GRM-N identifier to Vikunja numeric task ID.
Paginates through the project's tasks to handle projects with more
than 50 tasks.
"""
page = 1
while True:
tasks = client.list_project_tasks(VIKUNJA_PROJECT_ID, page=page, per_page=DEFAULT_PER_PAGE)
if not tasks:
break
matches = [t for t in tasks if t.get("identifier") == task_id]
if matches:
return int(matches[0]["id"])
if len(tasks) < DEFAULT_PER_PAGE:
break
page += 1
raise click.ClickException(
_(
"Could not find Vikunja task for {task_id} in project {project_id}.",
task_id=task_id,
project_id=VIKUNJA_PROJECT_ID,
)
)
def build_comment(task_id: str, conv_msg: str, commit_sha: str) -> str:
"""Build HTML comment body for Vikunja."""
return f"<p><strong>{task_id}</strong>: {conv_msg}</p><p>Commit: <code>{commit_sha}</code></p>"
@click.command()
@click.argument("commit_msg")
@click.option("--commit-sha", default="", help="Commit SHA")
def main(commit_msg: str, commit_sha: str) -> None:
token = os.environ.get("VIKUNJA_TOKEN", "")
if not token:
raise click.ClickException(_("ERROR: VIKUNJA_TOKEN is not set."))
task_id = extract_task_id(commit_msg)
if not task_id:
click.echo(_("No task ID in commit message, skipping Vikunja update. All good — nothing to do here!"))
return
client = VikunjaClient(VIKUNJA_API_URL, token)
vikunja_task_id = 0
try:
vikunja_task_id = resolve_task_id(client, task_id)
conv_msg = extract_conventional_msg(commit_msg)
sha = commit_sha or "unknown"
html = build_comment(task_id, conv_msg, sha)
client.post_comment(vikunja_task_id, html)
client.update_task(vikunja_task_id, done=True)
except APIError as e:
raise click.ClickException(
_(
"Vikunja API error: HTTP {status}{message}",
status=e.status,
message=e.message,
)
) from None
click.echo(
_(
"Nice! Vikunja task {task_id} (ID {vikunja_id}) updated and marked done.",
task_id=task_id,
vikunja_id=vikunja_task_id,
)
)
if __name__ == "__main__": # pragma: no cover
main()