#!/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= python3 scripts/review_pr.py \ --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()