141 lines
4.5 KiB
Python
141 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Post a review on a Gitea pull request.
|
|
|
|
Used by the GRM workflow to post structured PR reviews. The review body
|
|
is provided via --body and inline comments via a JSON file
|
|
(--comments-json) or stdin (--comments-stdin). This script is a thin
|
|
CLI wrapper around ``GiteaClient.create_review`` — the actual review
|
|
analysis is performed by the agent before invoking this tool.
|
|
|
|
Usage:
|
|
REPO_TOKEN=<token> python3 scripts/review_pr.py <pr_number> <repo> \
|
|
--event COMMENT \
|
|
--body "Review body text" \
|
|
--comments-json comments.json
|
|
|
|
The comments JSON file is a list of objects with keys:
|
|
- path: file path in the repo
|
|
- body: comment text
|
|
- new_position: line number in the new file (1-based)
|
|
- old_position: (optional) line number in the old file
|
|
|
|
Review focus areas (for the reviewer, not enforced by this script):
|
|
- Functional completeness
|
|
- Edge cases
|
|
- Technical excellence: architecture compliance, SRP, deduplication,
|
|
code smells, best practices, code quality, reusability, clean code,
|
|
readability, maintainability, extensibility
|
|
- Performance
|
|
- Security
|
|
- User experience
|
|
- Documentation completeness and relevance
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Any
|
|
|
|
import click
|
|
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
|
|
|
from gitea_runner_manager.api_clients import GiteaClient
|
|
from gitea_runner_manager.config import GITEA_API_URL
|
|
from gitea_runner_manager.exceptions import APIError
|
|
from gitea_runner_manager.i18n import _
|
|
|
|
load_dotenv(override=True)
|
|
|
|
VALID_EVENTS = ("APPROVE", "REQUEST_CHANGES", "COMMENT")
|
|
|
|
|
|
def parse_comments(comments_json: str | None, comments_stdin: bool) -> list[dict[str, Any]]:
|
|
"""Parse inline comments from a JSON file or stdin."""
|
|
if comments_json:
|
|
try:
|
|
with open(comments_json) as f:
|
|
data = json.load(f)
|
|
except json.JSONDecodeError as e:
|
|
raise click.ClickException(_("Invalid JSON in comments file: {error}", error=str(e))) from None
|
|
if not isinstance(data, list):
|
|
raise click.ClickException(_("Comments JSON must be a list of objects."))
|
|
return data
|
|
if comments_stdin:
|
|
raw = sys.stdin.read().strip()
|
|
if not raw:
|
|
return []
|
|
try:
|
|
data = json.loads(raw)
|
|
except json.JSONDecodeError as e:
|
|
raise click.ClickException(_("Invalid JSON on stdin: {error}", error=str(e))) from None
|
|
if not isinstance(data, list):
|
|
raise click.ClickException(_("Stdin comments JSON must be a list of objects."))
|
|
return data
|
|
return []
|
|
|
|
|
|
@click.command()
|
|
@click.argument("pr_number")
|
|
@click.argument("repo")
|
|
@click.option(
|
|
"--event",
|
|
default="COMMENT",
|
|
type=click.Choice(VALID_EVENTS),
|
|
help="Review event type: APPROVE, REQUEST_CHANGES, or COMMENT.",
|
|
)
|
|
@click.option("--body", default="", help="Top-level review body text.")
|
|
@click.option("--comments-json", default=None, help="Path to JSON file with inline comments.")
|
|
@click.option(
|
|
"--comments-stdin",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Read inline comments JSON from stdin.",
|
|
)
|
|
def main(
|
|
pr_number: str,
|
|
repo: str,
|
|
event: str,
|
|
body: str,
|
|
comments_json: str | None,
|
|
comments_stdin: bool,
|
|
) -> None:
|
|
token = os.environ.get("REPO_TOKEN", "")
|
|
if not token:
|
|
raise click.ClickException(_("ERROR: REPO_TOKEN is not set."))
|
|
|
|
owner, repo_name = repo.split("/")
|
|
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
|
|
|
|
comments = parse_comments(comments_json, comments_stdin)
|
|
|
|
if event != "APPROVE" and not body and not comments:
|
|
raise click.ClickException(_("Review body or inline comments are required for event '{event}'.", event=event))
|
|
|
|
try:
|
|
review = client.create_review(pr_number, event=event, body=body, comments=comments)
|
|
except APIError as e:
|
|
raise click.ClickException(
|
|
_(
|
|
"Failed to post review: HTTP {status} — {message}",
|
|
status=e.status,
|
|
message=e.message,
|
|
)
|
|
) from None
|
|
|
|
review_id = review.get("id", "?")
|
|
click.echo(
|
|
_(
|
|
"Review #{review_id} posted on PR #{pr_number} with event '{event}' ({num_comments} inline comments).",
|
|
review_id=review_id,
|
|
pr_number=pr_number,
|
|
event=event,
|
|
num_comments=len(comments),
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
main()
|