From 2e7ca8a5ff5b968d5789a51bf4288d50b27dc9c2 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 21 Jan 2026 19:14:27 -0800 Subject: [PATCH] Add mise task to list unresolved PR comments (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - New `pr-comments` mise task queries Forge API for unresolved review comments on a PR - Task takes a PR number as argument and displays all comments without a resolver - Updated CLAUDE.md to include using this task after user reviews PRs ## Deployment and Testing - [x] Tested task on PR #39 (shows no unresolved comments since all were resolved) - [x] Tested error handling with non-existent PR #9999 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/40 --- CLAUDE.md | 8 ++- mise-tasks/pr-comments | 118 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) create mode 100755 mise-tasks/pr-comments diff --git a/CLAUDE.md b/CLAUDE.md index bd80263..67687db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,11 @@ tea pr create --title "Description of change" --description "$(cat <<'EOF' EOF )" ``` -The user will review your work as you go, and will merge the pr as the last step in the process, even after deploying. +The user will review your work as you go, and will merge the PR as the last step in the process, even after deploying. After the user reviews the PR and leaves comments, check for unresolved comments with: +```fish +mise run pr-comments +``` +Address each unresolved comment before proceeding. The user will resolve comments on the Forge UI as they are addressed. 3. Always keep the zk cards up to date with any changes, and suggest new links to new cards whenever appropriate. Refer back to the zk docs often during the process of planning and making corrections to ensure accuracy, and if you make a mistake, figure out a way to guard against it using the zk. @@ -60,7 +64,7 @@ The user will review your work as you go, and will merge the pr as the last step ### Kubernetes Services (via ArgoCD) -Most services are migrating to Kubernetes. These are managed via ArgoCD using the app-of-apps pattern: +Most services run on `k8s.tail8d86e.ts.net`, via minikube on indri. They are managed via ArgoCD using the app-of-apps pattern: - **Application definitions**: `argocd/apps/.yaml` - **Manifests**: `argocd/manifests//` diff --git a/mise-tasks/pr-comments b/mise-tasks/pr-comments new file mode 100755 index 0000000..be8d25b --- /dev/null +++ b/mise-tasks/pr-comments @@ -0,0 +1,118 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.12" +# dependencies = ["httpx>=0.27.0", "rich>=13.0.0"] +# /// +#MISE description="List unresolved comments on a PR" +#USAGE arg "" help="Pull request number" +"""Fetch and display unresolved review comments on a pull request. + +This script queries the Forge (Gitea) API to find all review comments that +have not been resolved on a given PR. A comment is considered unresolved +if its 'resolver' field is null. + +Usage: mise run pr-comments +""" + +import sys + +import httpx +from rich.console import Console +from rich.text import Text + +FORGE_API_BASE = "https://forge.tail8d86e.ts.net/api/v1" +REPO_OWNER = "eblume" +REPO_NAME = "blumeops" + + +def get_reviews(client: httpx.Client, pr_number: int) -> list[dict]: + """Get all reviews for a pull request.""" + response = client.get( + f"{FORGE_API_BASE}/repos/{REPO_OWNER}/{REPO_NAME}/pulls/{pr_number}/reviews" + ) + response.raise_for_status() + return response.json() + + +def get_review_comments(client: httpx.Client, pr_number: int, review_id: int) -> list[dict]: + """Get all comments for a specific review.""" + response = client.get( + f"{FORGE_API_BASE}/repos/{REPO_OWNER}/{REPO_NAME}/pulls/{pr_number}/reviews/{review_id}/comments" + ) + response.raise_for_status() + return response.json() + + +def main() -> int: + console = Console() + + if len(sys.argv) < 2: + console.print("[red]Error:[/red] Please provide a PR number") + console.print("Usage: mise run pr-comments ") + return 1 + + try: + pr_number = int(sys.argv[1]) + except ValueError: + console.print(f"[red]Error:[/red] '{sys.argv[1]}' is not a valid PR number") + return 1 + + unresolved_comments: list[tuple[dict, dict]] = [] # (review, comment) pairs + + with httpx.Client() as client: + # Get all reviews + try: + reviews = get_reviews(client, pr_number) + except httpx.HTTPStatusError as e: + if e.response.status_code == 404: + console.print(f"[red]Error:[/red] PR #{pr_number} not found") + else: + console.print(f"[red]Error:[/red] API request failed: {e}") + return 1 + + # For each review, get comments and filter to unresolved + for review in reviews: + try: + comments = get_review_comments(client, pr_number, review["id"]) + except httpx.HTTPStatusError: + continue # Skip reviews we can't fetch comments for + + for comment in comments: + if comment.get("resolver") is None: + unresolved_comments.append((review, comment)) + + if not unresolved_comments: + console.print(f"[green]No unresolved comments on PR #{pr_number}[/green]") + return 0 + + # Display unresolved comments + console.print(f"[bold]Unresolved Comments on PR #{pr_number}[/bold] ({len(unresolved_comments)} comments)") + console.print("=" * 60) + console.print() + + for review, comment in unresolved_comments: + # Header with file path and reviewer + header = Text() + path = comment.get("path", "unknown file") + reviewer = review.get("user", {}).get("login", "unknown") + header.append(f"{path}", style="bold cyan") + header.append(f" (by {reviewer})") + console.print(header) + + # Comment body + body = comment.get("body", "").strip() + for line in body.split("\n"): + console.print(f" {line}") + + # Link to comment + html_url = comment.get("html_url", "") + if html_url: + console.print(f" [dim]{html_url}[/dim]") + + console.print() + + return 0 + + +if __name__ == "__main__": + sys.exit(main())