#!/usr/bin/env python3 """Update Vikunja task after a merge to master. Usage: VIKUNJA_TOKEN= python3 scripts/post_merge.py [--commit-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"

{task_id}: {conv_msg}

Commit: {commit_sha}

" @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()