Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions plugins/sentry-skills/skills/gh-review-requests/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: gh-review-requests
description: Fetch unread GitHub notifications for open PRs where review is requested from a specified team or opened by a team member. Use when asked to "find PRs I need to review", "show my review requests", "what needs my review", "fetch GitHub review requests", or "check team review queue".
allowed-tools: Bash
---

# GitHub Review Requests

Fetch unread `review_requested` notifications for open (unmerged) PRs, filtered by a GitHub team.

**Requires**: GitHub CLI (`gh`) authenticated.

## Step 1: Identify the Team

If the user has not specified a team, ask:

> Which GitHub team should I filter by? (e.g. `streaming-platform`)

Accept either a team slug (`streaming-platform`) or a display name ("Streaming Platform") — convert to lowercase-hyphenated slug before passing to the script.

## Step 2: Run the Script

```bash
uv run ${CLAUDE_SKILL_ROOT}/scripts/fetch_review_requests.py --org getsentry --teams <team-slug>
```

To filter by multiple teams, pass a comma-separated list:

```bash
uv run ${CLAUDE_SKILL_ROOT}/scripts/fetch_review_requests.py --org getsentry --teams <team slugs>
```

### Script output

```json
{
"total": 3,
"prs": [
{
"notification_id": "12345",
"title": "feat(kafka): add workflow to restart a broker",
"url": "https://github.com/getsentry/ops/pull/19144",
"repo": "getsentry/ops",
"pr_number": 19144,
"author": "bmckerry",
"reasons": ["opened by: bmckerry"]
}
]
}
```

`reasons` will contain one or both of:
- `"review requested from: <Team Name>"` — the team is a requested reviewer
- `"opened by: <login>"` — the PR author is a team member

## Step 3: Present Results

Display results as a markdown table with full URLs:

| # | Title | URL | Reason |
|---|-------|-----|--------|
| 1 | feat(kafka): add workflow to restart a broker | https://github.com/getsentry/ops/pull/19144 | opened by: evanh |

If `total` is 0, say: "No unread review requests found for that team."

## Fallback

If the script fails, run manually:

```bash
gh api notifications --paginate
```

Then for each `review_requested` notification, check:
- `gh api repos/{repo}/pulls/{number}` — skip if `state == "closed"` or `merged_at` is set
- `gh api repos/{repo}/pulls/{number}/requested_reviewers` — check `teams[].name`
- `gh api orgs/{org}/teams/{slug}/members` — check if author is a member
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
"""
Fetch unread GitHub review-requested notifications for open (unmerged) PRs,
filtered by team membership and/or team review requests.

Usage:
uv run fetch_review_requests.py --org ORG --teams TEAM1,TEAM2

Arguments:
--org GitHub organization slug (default: getsentry)
--teams Comma-separated team slugs to filter by (e.g. streaming-platform)

Output: JSON to stdout
"""

import argparse
import json
import subprocess
import sys


def gh(path: str, paginate: bool = False) -> list | dict:
cmd = ["gh", "api", path]
if paginate:
cmd.append("--paginate")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0 or not result.stdout:
print(f"Error running gh {' '.join(cmd)}: {result.stderr}", file=sys.stderr)
return [] if paginate else {}
return json.loads(result.stdout)


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--org", default="getsentry")
parser.add_argument("--teams", required=True, help="Comma-separated team slugs")
args = parser.parse_args()

team_slugs = [t.strip() for t in args.teams.split(",")]

# Resolve team members for all specified teams
members: set[str] = set()
team_display_names: dict[str, str] = {}
for slug in team_slugs:
data = gh(f"orgs/{args.org}/teams/{slug}/members", paginate=True)
for m in data:
members.add(m["login"])
# Get display name
team_data = gh(f"orgs/{args.org}/teams/{slug}")
team_display_names[slug] = team_data.get("name", slug)

# Fetch unread notifications (GitHub API default: unread only)
all_notifs = gh("notifications", paginate=True)
review_notifs = [
n for n in all_notifs
if n["reason"] == "review_requested" and n["unread"]
]

prs = []
for n in review_notifs:
url = n["subject"]["url"]
repo_path = url.replace("https://api.github.com/repos/", "")
repo = repo_path.rsplit("/pulls/", 1)[0]
pr_num = repo_path.rsplit("/", 1)[-1]
html_url = f"https://github.com/{repo}/pull/{pr_num}"

pr_data = gh(f"repos/{repo}/pulls/{pr_num}")
if not pr_data:
continue

# Skip merged or closed PRs
if pr_data.get("merged_at") or pr_data.get("state") == "closed":
continue

author = pr_data["user"]["login"]

reviewers_data = gh(f"repos/{repo}/pulls/{pr_num}/requested_reviewers")
requested_team_names = [t["slug"] for t in reviewers_data.get("teams", [])]
matching_teams = [
t for t in requested_team_names
if any(slug.lower() == t.lower() for slug in team_slugs)
]

by_team_member = author in members
review_from_team = len(matching_teams) > 0

if not (by_team_member or review_from_team):
continue

reasons = []
if review_from_team:
reasons.append(f"review requested from: {', '.join(matching_teams)}")
if by_team_member:
reasons.append(f"opened by: {author}")

prs.append({
"notification_id": n["id"],
"title": n["subject"]["title"],
"url": html_url,
"repo": repo,
"pr_number": int(pr_num),
"author": author,
"reasons": reasons,
})

print(json.dumps({"total": len(prs), "prs": prs}, indent=2))


if __name__ == "__main__":
main()