Created
January 23, 2026 12:14
-
-
Save cardil/a71ce93fabee91cd3436415ecd069ead to your computer and use it in GitHub Desktop.
Helper scripts to get GH review comments in LLM-friendly way
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
| #!/usr/bin/env python | |
| """Fetch unresolved review comments from a GitHub PR. | |
| Based on: https://stackoverflow.com/a/66072198/844449 | |
| Usage: | |
| python scripts/gh-comments.py [PR_NUMBER] | |
| python scripts/gh-comments.py 17 | |
| """ | |
| import json | |
| import subprocess | |
| import sys | |
| def get_pr_number() -> int: | |
| """Get PR number from argument or current branch.""" | |
| if len(sys.argv) > 1: | |
| return int(sys.argv[1]) | |
| # Try to get from current branch | |
| result = subprocess.run( | |
| ["gh", "pr", "view", "--json", "number", "--jq", ".number"], | |
| capture_output=True, | |
| text=True, | |
| ) | |
| if result.returncode == 0: | |
| return int(result.stdout.strip()) | |
| print("Usage: python scripts/gh-comments.py <PR_NUMBER>", file=sys.stderr) | |
| sys.exit(1) | |
| def get_repo_info() -> tuple[str, str]: | |
| """Get owner and repo from current directory.""" | |
| result = subprocess.run( | |
| [ | |
| "gh", | |
| "repo", | |
| "view", | |
| "--json", | |
| "owner,name", | |
| "--jq", | |
| "[.owner.login, .name] | @tsv", | |
| ], | |
| capture_output=True, | |
| text=True, | |
| ) | |
| if result.returncode != 0: | |
| print("Error: Could not determine repository", file=sys.stderr) | |
| sys.exit(1) | |
| parts = result.stdout.strip().split("\t") | |
| return parts[0], parts[1] | |
| def fetch_unresolved_comments(owner: str, repo: str, pr_number: int) -> list[dict]: | |
| """Fetch unresolved review threads from a PR using GraphQL.""" | |
| query = """ | |
| query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) { | |
| repository(owner: $owner, name: $repo) { | |
| pullRequest(number: $pr) { | |
| reviewThreads(first: 100, after: $cursor) { | |
| pageInfo { | |
| hasNextPage | |
| endCursor | |
| } | |
| nodes { | |
| id | |
| isResolved | |
| isOutdated | |
| path | |
| line | |
| comments(first: 20) { | |
| nodes { | |
| author { | |
| login | |
| } | |
| body | |
| createdAt | |
| url | |
| id | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| """ | |
| variables = { | |
| "owner": owner, | |
| "repo": repo, | |
| "pr": pr_number, | |
| "cursor": None, | |
| } | |
| all_threads = [] | |
| has_next_page = True | |
| while has_next_page: | |
| result = subprocess.run( | |
| [ | |
| "gh", | |
| "api", | |
| "graphql", | |
| "-f", | |
| f"query={query}", | |
| "-f", | |
| f"owner={owner}", | |
| "-f", | |
| f"repo={repo}", | |
| "-F", | |
| f"pr={pr_number}", | |
| "-f", | |
| f"cursor={variables['cursor'] or ''}", | |
| ], | |
| capture_output=True, | |
| text=True, | |
| ) | |
| if result.returncode != 0: | |
| print(f"Error: {result.stderr}", file=sys.stderr) | |
| sys.exit(1) | |
| data = json.loads(result.stdout) | |
| threads_data = data["data"]["repository"]["pullRequest"]["reviewThreads"] | |
| for thread in threads_data["nodes"]: | |
| if not thread["isResolved"]: | |
| all_threads.append(thread) | |
| has_next_page = threads_data["pageInfo"]["hasNextPage"] | |
| variables["cursor"] = threads_data["pageInfo"]["endCursor"] | |
| return all_threads | |
| def print_threads(threads: list[dict]) -> None: | |
| """Print unresolved threads in a readable format.""" | |
| if not threads: | |
| print("β No unresolved review comments!") | |
| return | |
| print(f"π Found {len(threads)} unresolved review thread(s):\n") | |
| for i, thread in enumerate(threads, 1): | |
| path = thread["path"] | |
| line = thread.get("line", "?") | |
| outdated = " (outdated)" if thread["isOutdated"] else "" | |
| print(f"ββ Thread {i}: {path}:{line}{outdated} ββ") | |
| for comment in thread["comments"]["nodes"]: | |
| author = comment["author"]["login"] if comment["author"] else "unknown" | |
| body = comment["body"] | |
| url = comment["url"] | |
| # Extract numeric comment ID from URL (discussion_rNNNN) | |
| comment_id = url.split("discussion_r")[-1] if "discussion_r" in url else "?" | |
| print(f"by @{author} (comment ID: {comment_id}):") | |
| print() | |
| # Print full body with indentation | |
| for line_text in body.split("\n"): | |
| print(f"{line_text}") | |
| print() | |
| print(f"π Link: {url}") | |
| print() | |
| print(f"== Thread end {i}: {path}:{line}{outdated} ==") | |
| def main() -> None: | |
| """Main entry point.""" | |
| pr_number = get_pr_number() | |
| owner, repo = get_repo_info() | |
| print(f"Fetching unresolved comments for {owner}/{repo}#{pr_number}...") | |
| threads = fetch_unresolved_comments(owner, repo, pr_number) | |
| print_threads(threads) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment