-
Notifications
You must be signed in to change notification settings - Fork 38
claude: add pr-review command #217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bbqiu
wants to merge
3
commits into
main
Choose a base branch
from
bbqiu/add-pr-review-command
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| --- | ||
| allowed-tools: Read, Skill, Bash, Grep, Glob | ||
| argument-hint: [extra_context] | ||
| description: Review a GitHub pull request and display all issues found in Claude Code | ||
| --- | ||
|
|
||
| # Review Pull Request | ||
|
|
||
| Automatically review a GitHub pull request and display all found issues in Claude Code with detailed analysis and suggestions. | ||
|
|
||
| ## Usage | ||
|
|
||
| ``` | ||
| /pr-review [extra_context] | ||
| ``` | ||
|
|
||
| ## Arguments | ||
|
|
||
| - `extra_context` (optional): Additional instructions or filtering context (e.g., focus on specific issues or areas) | ||
|
|
||
| ## Examples | ||
|
|
||
| ``` | ||
| /pr-review # Review all changes | ||
| /pr-review Please focus on security issues # Focus on security | ||
| /pr-review Only review Python files # Filter specific file types | ||
| /pr-review Check for performance issues # Focus on specific concern | ||
| ``` | ||
|
|
||
| ## Instructions | ||
|
|
||
| ### 1. Auto-detect PR context | ||
|
|
||
| - First check for environment variables: | ||
| - If `PR_NUMBER` and `GITHUB_REPOSITORY` are set, parse `GITHUB_REPOSITORY` as `owner/repo` and use `PR_NUMBER` | ||
| - Then use `gh pr view <PR_NUMBER> --repo <owner/repo> --json 'title,body'` to retrieve the PR title and description | ||
| - Otherwise: | ||
| - Use `gh pr view --json 'title,body,url,number'` to get PR info for the current branch | ||
| - Parse the output to extract owner, repo, PR number, title, and description | ||
| - If neither method works, inform the user that no PR was found and exit | ||
|
|
||
| ### 2. Fetch PR Diff and Existing Comments | ||
|
|
||
| - Use `gh pr diff <PR_NUMBER> --repo <owner/repo>` to fetch the PR diff | ||
| - Use the `fetch_unresolved_comments` skill to get existing unresolved review comments: | ||
| ``` | ||
| /skill fetch_unresolved_comments <owner> <repo> <PR_NUMBER> | ||
| ``` | ||
| - Parse the returned comment data to identify lines that already have unresolved issues to avoid duplicate reports | ||
|
|
||
| ### 3. Review Changed Lines | ||
|
|
||
| **Apply additional filtering** from user instructions if provided (e.g., focus on specific issues or areas) | ||
|
|
||
| Carefully examine **only the changed lines** (added or modified) in the diff for: | ||
|
|
||
| - Potential bugs and code quality issues | ||
| - Common mistakes | ||
|
|
||
| **Important**: Ignore unchanged/context lines and pre-existing code. | ||
|
|
||
| **Collect all issues** in a structured format before presenting them: | ||
|
|
||
| ``` | ||
| issues = [ | ||
| { | ||
| "file": "path/to/file.py", | ||
| "line": 42, | ||
| "end_line": 45, // (optional, for multi-line issues) | ||
| "severity": "error|warning|info", | ||
| "category": "bug|style|performance|security", | ||
| "description": "Detailed issue description", | ||
| "suggestion": "Recommended fix", | ||
| "is_duplicate": false // true if similar issue already commented on this line | ||
| } | ||
| ] | ||
| ``` | ||
|
|
||
| **Important**: Before adding an issue to the list, check the existing unresolved comments from step 2. If there's already an unresolved comment on the same line(s) or covering a similar concern, mark the issue as duplicate to avoid redundant reports. | ||
|
|
||
| ### 4. Present Complete Analysis | ||
|
|
||
| After reviewing all changed files, display a comprehensive summary in Claude Code: | ||
|
|
||
| - **Group by file and severity** for clear organization | ||
| - **Display statistics**: Total issues, breakdown by severity and category | ||
| - **Use navigation-friendly format**: Include `file:line` references for easy IDE navigation | ||
| - **Show detailed descriptions** and suggested fixes for each issue | ||
|
|
||
| **Output format:** | ||
| ``` | ||
| ## PR Review Results | ||
|
|
||
| **Total Issues Found: X (Y new, Z already commented)** | ||
| - 🔴 Errors: X | ||
| - 🟡 Warnings: X | ||
| - 🔵 Info: X | ||
|
|
||
| ### path/to/file1.py:42-45 | ||
| 🔴 **[Bug]:** Description here | ||
| → Suggestion: Fix recommendation | ||
|
|
||
| ### path/to/file2.js:67 | ||
| 🟡 **[Style]:** Description here | ||
| → Suggestion: Fix recommendation | ||
|
|
||
| ### path/to/file3.py:23 | ||
| 🔴 **[Security]:** Description here | ||
| → Suggestion: Fix recommendation | ||
| ⚠️ *Similar issue already commented on this PR* | ||
| ``` | ||
|
|
||
| **Format Notes:** | ||
| - Use `file:line` or `file:line-endline` format for direct IDE navigation | ||
| - Mark duplicate/already-commented issues with ⚠️ warning | ||
| - Group issues by file, with each issue as a separate section for easy clicking | ||
|
|
||
| If **no issues found**, display: "✅ **No issues found** - All changes look good!" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| --- | ||
| name: fetch-unresolved-comments | ||
| description: Fetch unresolved PR review comments using GitHub GraphQL API, filtering out resolved and outdated feedback. | ||
| --- | ||
|
|
||
| # Fetch Unresolved PR Review Comments | ||
|
|
||
| Uses GitHub's GraphQL API to fetch only unresolved review thread comments from a pull request. | ||
|
|
||
| ## When to Use | ||
|
|
||
| - You need to get only unresolved review comments from a PR | ||
| - You want to filter out already-resolved and outdated feedback | ||
|
|
||
| ## Instructions | ||
|
|
||
| 1. **Parse PR information**: | ||
|
|
||
| - First check for environment variables: | ||
| - If `PR_NUMBER` and `GITHUB_REPOSITORY` are set, read them and parse `GITHUB_REPOSITORY` as `owner/repo` and use `PR_NUMBER` directly | ||
| - Otherwise: | ||
| - Use `gh pr view --json url -q '.url'` to get the current branch's PR URL and parse to extract owner, repo, and PR number | ||
|
|
||
| 2. **Run the Python script**: | ||
|
|
||
| ```bash | ||
| GITHUB_TOKEN=$(gh auth token) \ | ||
| uv run python .claude/skills/fetch-unresolved-comments/fetch_unresolved_comments.py <owner> <repo> <pr_number> | ||
| ``` | ||
|
|
||
| 3. **Script options**: | ||
|
|
||
| - `--token <token>`: Provide token explicitly (default: GITHUB_TOKEN or GH_TOKEN env var) | ||
|
|
||
| 4. **Parse the JSON output**: | ||
| The script always outputs JSON with: | ||
| - `total`: Total number of unresolved comments across all threads | ||
| - `by_file`: Review threads grouped by file path (each thread contains multiple comments in a conversation) | ||
|
|
||
| ## Example JSON Output | ||
|
|
||
| ```json | ||
| { | ||
| "total": 3, | ||
| "by_file": { | ||
| ".github/workflows/resolve.yml": [ | ||
| { | ||
| "thread_id": "PRRT_kwDOAL...", | ||
| "isOutdated": false, | ||
| "line": 40, | ||
| "startLine": null, | ||
| "diffHunk": "@@ -0,0 +1,245 @@\n+name: resolve...", | ||
| "comments": [ | ||
| { | ||
| "id": 2437935275, | ||
| "body": "We can remove this once we get the key.", | ||
| "author": "harupy", | ||
| "createdAt": "2025-10-17T00:53:20Z" | ||
| }, | ||
| { | ||
| "id": 2437935276, | ||
| "body": "Good catch, I'll update it.", | ||
| "author": "contributor", | ||
| "createdAt": "2025-10-17T01:10:15Z" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| ".gitignore": [ | ||
| { | ||
| "thread_id": "PRRT_kwDOAL...", | ||
| "isOutdated": false, | ||
| "line": 133, | ||
| "startLine": null, | ||
| "diffHunk": "@@ -130,0 +133,2 @@\n+.claude/*", | ||
| "comments": [ | ||
| { | ||
| "id": 2437935280, | ||
| "body": "Should we add this to .gitignore?", | ||
| "author": "reviewer", | ||
| "createdAt": "2025-10-17T01:15:42Z" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| ``` |
169 changes: 169 additions & 0 deletions
169
.claude/skills/fetch-unresolved-comments/fetch_unresolved_comments.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| """Fetch unresolved PR review comments using GitHub GraphQL API. | ||
|
|
||
| Example usage: | ||
| GITHUB_TOKEN=$(gh auth token) uv run --no-project .claude/skills/fetch-unresolved-comments/fetch_unresolved_comments.py mlflow mlflow 18327 | ||
| """ # noqa: E501 | ||
| # ruff: noqa: T201 | ||
|
|
||
| import argparse | ||
| import json | ||
| import os | ||
| import sys | ||
| from typing import Any | ||
| from urllib.request import Request, urlopen | ||
|
|
||
|
|
||
| def fetch_unresolved_comments(owner: str, repo: str, pr_number: int, token: str) -> dict[str, Any]: | ||
| """Fetch unresolved review threads from a PR using GraphQL.""" | ||
|
|
||
| query = """ | ||
| query($owner: String!, $repo: String!, $prNumber: Int!) { | ||
| repository(owner: $owner, name: $repo) { | ||
| pullRequest(number: $prNumber) { | ||
| reviewThreads(first: 100) { | ||
| nodes { | ||
| id | ||
| isResolved | ||
| isOutdated | ||
| comments(first: 100) { | ||
| nodes { | ||
| id | ||
| databaseId | ||
| body | ||
| path | ||
| line | ||
| startLine | ||
| diffHunk | ||
| author { | ||
| login | ||
| } | ||
| createdAt | ||
| updatedAt | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| """ | ||
|
|
||
| variables = { | ||
| "owner": owner, | ||
| "repo": repo, | ||
| "prNumber": pr_number, | ||
| } | ||
|
|
||
| headers = { | ||
| "Authorization": f"Bearer {token}", | ||
| "Content-Type": "application/json", | ||
| } | ||
|
|
||
| data = json.dumps({"query": query, "variables": variables}).encode("utf-8") | ||
| request = Request("https://api.github.com/graphql", data=data, headers=headers) | ||
|
|
||
| try: | ||
| with urlopen(request) as response: | ||
| return json.loads(response.read().decode("utf-8")) | ||
| except Exception as e: | ||
| print(f"Error fetching data: {e}", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| def format_comments(data: dict[str, Any]) -> dict[str, Any]: | ||
| """Format unresolved comments for easier consumption.""" | ||
|
|
||
| try: | ||
| threads = data["data"]["repository"]["pullRequest"]["reviewThreads"]["nodes"] | ||
| except (KeyError, TypeError): | ||
| print("Error: Invalid response structure", file=sys.stderr) | ||
| print(json.dumps(data, indent=2), file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| by_file = {} | ||
| total_comments = 0 | ||
|
|
||
| for thread in threads: | ||
| if not thread["isResolved"]: | ||
| comments = [] | ||
| path = None | ||
| line = None | ||
| start_line = None | ||
| diff_hunk = None | ||
|
|
||
| for comment in thread["comments"]["nodes"]: | ||
| if path is None: | ||
| path = comment["path"] | ||
| line = comment["line"] | ||
| start_line = comment.get("startLine") | ||
| diff_hunk = comment.get("diffHunk") | ||
|
|
||
| comments.append( | ||
| { | ||
| "id": comment["databaseId"], | ||
| "body": comment["body"], | ||
| "author": comment["author"]["login"] if comment["author"] else "unknown", | ||
| "createdAt": comment["createdAt"], | ||
| } | ||
| ) | ||
| total_comments += 1 | ||
|
|
||
| if path: | ||
| if path not in by_file: | ||
| by_file[path] = [] | ||
|
|
||
| by_file[path].append( | ||
| { | ||
| "thread_id": thread["id"], | ||
| "isOutdated": thread["isOutdated"], | ||
| "line": line, | ||
| "startLine": start_line, | ||
| "diffHunk": diff_hunk, | ||
| "comments": comments, | ||
| } | ||
| ) | ||
|
|
||
| return { | ||
| "total": total_comments, | ||
| "by_file": by_file, | ||
| } | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser( | ||
| description="Fetch unresolved PR review comments using GitHub GraphQL API" | ||
| ) | ||
| parser.add_argument("owner", help="Repository owner") | ||
| parser.add_argument("repo", help="Repository name") | ||
| parser.add_argument("pr_number", type=int, help="Pull request number") | ||
| parser.add_argument( | ||
| "--token", | ||
| default=os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN"), | ||
| help="GitHub token (default: GITHUB_TOKEN or GH_TOKEN env var)", | ||
| ) | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| if not args.token: | ||
| print( | ||
| "Error: GitHub token required (use --token or set GITHUB_TOKEN/GH_TOKEN)", | ||
| file=sys.stderr, | ||
| ) | ||
| sys.exit(1) | ||
|
|
||
| data = fetch_unresolved_comments(args.owner, args.repo, args.pr_number, args.token) | ||
| formatted = format_comments(data) | ||
| formatted["by_file"] = { | ||
| path: [thread for thread in threads if not thread["isOutdated"]] | ||
| for path, threads in formatted["by_file"].items() | ||
| } | ||
| formatted["by_file"] = {k: v for k, v in formatted["by_file"].items() if v} | ||
| formatted["total"] = sum( | ||
| len(thread["comments"]) for threads in formatted["by_file"].values() for thread in threads | ||
| ) | ||
|
|
||
| print(json.dumps(formatted, indent=2)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just in case AI get misled by mlflow