Files
grm/scripts/auto_merge.py
T
Emil Simeonov 790b5c3769
CI / lint (push) Has been cancelled
CI / unit-tests (push) Has been cancelled
CI / molecule-tests (push) Has been cancelled
Post-merge Vikunja update / vikunja (push) Has been cancelled
GRM-20: refactor: rework all scripts to use click and i18n
- Replace argparse/print/sys.exit with click commands and ClickException
- Translate all user-facing messages via _()
- Add friendly Oops! / Nice! prompts
- Wrap HTTP errors in all scripts with user-friendly translated messages
- Update all unit tests to use CliRunner and expect ClickException
- Add 100% branch coverage for new HTTP error handling branches
- Add missing translation keys to i18n.py
- Fix pre-commit hook to use venv Python for validate_commit_msg.py
2026-06-19 20:11:20 +02:00

105 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Auto-merge PR by extracting task ID from branch and validating PR title.
Usage:
GITEA_TOKEN=<token> python3 scripts/auto_merge.py <branch> <pr_title> <repo> <pr_number>
"""
import os
import re
import click
import requests
from gitea_runner_manager.i18n import _
GITEA_API = "https://git.oblachno.oblachno.fyi/api/v1"
TASK_ID_RE = re.compile(r"GRM-\d+")
CONVENTIONAL_RE = re.compile(
r"^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert|BREAKING CHANGE)(\(.+\))?: .+"
)
def extract_task_id(branch: str) -> str:
"""Extract GRM-N task identifier from branch name."""
match = TASK_ID_RE.search(branch)
return match.group(0) if match else ""
def validate_pr_title(pr_title: str) -> None:
"""Raise ClickException if PR title does not follow conventional commits."""
if not CONVENTIONAL_RE.match(pr_title):
raise click.ClickException(
_(
"Oops! PR title must follow conventional commit format.\n"
" Expected: <type>: <description>\n"
" Got: {pr_title}",
pr_title=pr_title,
)
)
def merge_pr(token: str, repo: str, pr_number: str, merge_title: str) -> None:
"""Call Gitea API to squash-merge the PR."""
url = f"{GITEA_API}/repos/{repo}/pulls/{pr_number}/merge"
headers = {
"Authorization": f"token {token}",
"Content-Type": "application/json",
}
payload = {"Do": "squash", "MergeTitleField": merge_title}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
@click.command()
@click.argument("branch")
@click.argument("pr_title")
@click.argument("repo")
@click.argument("pr_number")
def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
token = os.environ.get("GITEA_TOKEN", "")
if not token:
raise click.ClickException(_("ERROR: GITEA_TOKEN is not set."))
task_id = extract_task_id(branch)
if not task_id:
raise click.ClickException(
_(
"Oops! No task ID (GRM-N) found in branch name '{branch}'.",
branch=branch,
)
)
validate_pr_title(pr_title)
merge_title = f"{task_id}: {pr_title}"
try:
merge_pr(token, repo, pr_number, merge_title)
except requests.HTTPError as e:
response = e.response
status = response.status_code if response else 0
try:
body = response.json() if response else {}
message = body.get("message", str(e))
except Exception:
message = str(e)
raise click.ClickException(
_(
"Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.",
status=status,
message=message,
)
) from None
click.echo(
_(
"Nice! PR #{pr_number} squash-merged with title: {merge_title}",
pr_number=pr_number,
merge_title=merge_title,
)
)
if __name__ == "__main__": # pragma: no cover
main()