Public Access
128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Auto-fix PR title to follow the ``{PREFIX}-N: <title>`` convention.
|
|
|
|
Reads the task ID from the branch name, fetches the Vikunja task title,
|
|
and updates the PR title via the Gitea API.
|
|
|
|
Exit codes:
|
|
0 = PR title updated (or already correct)
|
|
1 = Error (missing token, PR not found, etc.)
|
|
|
|
Usage::
|
|
|
|
python3 -m devx.ci.fix_pr_title --repo owner/repo --pr-number 123
|
|
python3 -m devx.ci.fix_pr_title --repo owner/repo --branch DEVX-256-fix-foo --pr-number 123
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import click
|
|
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
|
|
|
from devx.api_clients import GiteaClient
|
|
from devx.ci.auto_merge import extract_task_id
|
|
from devx.ci.check_auto_merge_ready import get_vikunja_title_optional
|
|
from devx.config import (
|
|
GITEA_API_URL,
|
|
TASK_PREFIX,
|
|
)
|
|
from devx.exceptions import APIError
|
|
from devx.i18n import _
|
|
from devx.tokens import get_ci_token
|
|
|
|
load_dotenv()
|
|
|
|
|
|
@click.command()
|
|
@click.option("--repo", required=True, help=_("Repository in owner/name format"))
|
|
@click.option("--pr-number", type=int, required=True, help=_("PR number to fix"))
|
|
@click.option("--branch", default=None, help=_("Branch name (auto-fetched from PR if not given)"))
|
|
@click.option("--dry-run", is_flag=True, help=_("Show what would change without updating"))
|
|
def cli(repo: str, pr_number: int, branch: str | None, dry_run: bool) -> None:
|
|
"""Fix PR title to follow the ``{PREFIX}-N: <title>`` convention."""
|
|
if "/" not in repo:
|
|
raise click.ClickException(_("Repo must be in 'owner/name' format, got: {repo}", repo=repo))
|
|
owner, repo_name = repo.split("/", 1)
|
|
|
|
# 1. Get CI token
|
|
try:
|
|
token = get_ci_token()
|
|
except click.ClickException as exc:
|
|
raise click.ClickException(_("CI_GITEA_API_TOKEN not set: {error}", error=str(exc))) from exc
|
|
|
|
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
|
|
|
|
# 2. Fetch PR
|
|
try:
|
|
pr = client.get_pr(pr_number)
|
|
except APIError as exc:
|
|
raise click.ClickException(_("Failed to fetch PR #{pr}: {error}", pr=pr_number, error=str(exc))) from exc
|
|
|
|
current_title = str(pr.get("title", ""))
|
|
if not branch:
|
|
branch = str(pr.get("head", {}).get("ref", ""))
|
|
if not branch:
|
|
raise click.ClickException(_("Could not determine branch name from PR #{pr}", pr=pr_number))
|
|
|
|
click.echo(f"[fix-pr-title] Branch: {branch}")
|
|
click.echo(f"[fix-pr-title] Current PR title: {current_title}")
|
|
|
|
# 3. Extract task ID from branch
|
|
task_id = extract_task_id(branch)
|
|
if not task_id:
|
|
raise click.ClickException(
|
|
_(
|
|
"No task ID found in branch '{branch}'. Expected format: {prefix}-N-description.",
|
|
branch=branch,
|
|
prefix=TASK_PREFIX,
|
|
)
|
|
)
|
|
|
|
click.echo(f"[fix-pr-title] Task ID: {task_id}")
|
|
|
|
# 4. Get Vikunja task title
|
|
vikunja_title = get_vikunja_title_optional(task_id)
|
|
if vikunja_title is None:
|
|
# Fallback: strip common prefixes from current title
|
|
# (e.g. "fix: ...", "feat: ...", "refactor: ...")
|
|
import re
|
|
|
|
stripped = re.sub(
|
|
r"^(fix|feat|refactor|chore|docs|test|ci|build|perf|style|revert)(\(.+?\))?!?:\s*", "", current_title
|
|
)
|
|
# Also strip any leading task ID prefix
|
|
stripped = re.sub(rf"^{TASK_PREFIX}-\d+:\s*", "", stripped)
|
|
vikunja_title = stripped if stripped else current_title
|
|
click.echo(f"[fix-pr-title] WARNING: Vikunja task not found — using stripped title: {vikunja_title}")
|
|
else:
|
|
click.echo(f"[fix-pr-title] Vikunja title: {vikunja_title}")
|
|
|
|
# 5. Build new title
|
|
# Defensive: strip task ID prefix from Vikunja title if present
|
|
if vikunja_title.startswith(f"{task_id}:"):
|
|
vikunja_title = vikunja_title[len(f"{task_id}:") :].strip()
|
|
|
|
new_title = f"{task_id}: {vikunja_title}"
|
|
|
|
if current_title == new_title:
|
|
click.echo(f"[fix-pr-title] PR title already correct: {new_title}")
|
|
return
|
|
|
|
click.echo(f"[fix-pr-title] New PR title: {new_title}")
|
|
|
|
if dry_run:
|
|
click.echo("[fix-pr-title] Dry run — not updating PR.")
|
|
return
|
|
|
|
# 6. Update PR title
|
|
try:
|
|
client.update_pr(pr_number, {"title": new_title})
|
|
except APIError as exc:
|
|
raise click.ClickException(_("Failed to update PR #{pr}: {error}", pr=pr_number, error=str(exc))) from exc
|
|
|
|
click.echo(f"[fix-pr-title] PR #{pr_number} title updated to: {new_title}")
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
cli() # pragma: no cover
|