#!/usr/bin/env python3 """Update Vikunja task after a merge to master. Usage: VIKUNJA_TOKEN= python3 scripts/post_merge.py """ import argparse import json import os import re import sys import requests VIKUNJA_API = "https://work.oblachno.oblachno.fyi/api/v1" TASK_ID_RE = re.compile(r"GRM-\d+") PROJECT_ID = 6 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(token: str, task_id: str) -> int: """Resolve GRM-N identifier to Vikunja numeric task ID.""" url = f"{VIKUNJA_API}/tasks/all" headers = {"Authorization": f"Bearer {token}"} params = {"per_page": 50} response = requests.get(url, headers=headers, params=params, timeout=30) response.raise_for_status() tasks = response.json() matches = [ t for t in tasks if t.get("project_id") == PROJECT_ID and t.get("identifier") == task_id ] if not matches: print(f"ERROR: Could not find Vikunja task for {task_id} in project {PROJECT_ID}") sys.exit(1) return int(matches[0]["id"]) def post_comment(token: str, task_id: int, html: str) -> None: """Post an HTML comment to a Vikunja task.""" url = f"{VIKUNJA_API}/tasks/{task_id}/comments" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } payload = {"comment": html} response = requests.post(url, headers=headers, json=payload, timeout=30) response.raise_for_status() def mark_task_done(token: str, task_id: int) -> None: """Mark a Vikunja task as done.""" url = f"{VIKUNJA_API}/tasks/{task_id}" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } payload = {"done": True} response = requests.put(url, headers=headers, json=payload, timeout=30) response.raise_for_status() 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}

" f"

Commit: {commit_sha}

" ) def main(args: list[str] | None = None) -> None: # pragma: no cover argv = args if args is not None else sys.argv parser = argparse.ArgumentParser(description="Update Vikunja task after merge") parser.add_argument("commit_msg", help="Full merge commit message") parser.add_argument("--commit-sha", default="", help="Commit SHA") parsed = parser.parse_args(argv[1:]) token = os.environ.get("VIKUNJA_TOKEN", "") if not token: print("ERROR: VIKUNJA_TOKEN is not set.", file=sys.stderr) sys.exit(1) task_id = extract_task_id(parsed.commit_msg) if not task_id: print("No task ID in commit message, skipping Vikunja update.") return vikunja_task_id = resolve_task_id(token, task_id) conv_msg = extract_conventional_msg(parsed.commit_msg) commit_sha = parsed.commit_sha or "unknown" html = build_comment(task_id, conv_msg, commit_sha) post_comment(token, vikunja_task_id, html) mark_task_done(token, vikunja_task_id) print(f"Vikunja task {task_id} (ID {vikunja_task_id}) updated and marked done.") if __name__ == "__main__": # pragma: no cover main()