From 01e373065f40c45e1daadbd32595edfcaa4779e6 Mon Sep 17 00:00:00 2001 From: Evan Hicks Date: Wed, 18 Feb 2026 10:49:38 -0500 Subject: [PATCH 1/3] feat: Add a skill that shows PRs that need to be reviewed The skill will run a script that outputs all the PRs that the user should review for their team. It can then mark those notifications as read. --- .../skills/gh-review-requests/SKILL.md | 77 ++++++++++++ .../scripts/fetch_review_requests.py | 112 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 plugins/sentry-skills/skills/gh-review-requests/SKILL.md create mode 100644 plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py diff --git a/plugins/sentry-skills/skills/gh-review-requests/SKILL.md b/plugins/sentry-skills/skills/gh-review-requests/SKILL.md new file mode 100644 index 0000000..1e6f755 --- /dev/null +++ b/plugins/sentry-skills/skills/gh-review-requests/SKILL.md @@ -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 +``` + +To filter by multiple teams, pass a comma-separated list: + +```bash +uv run ${CLAUDE_SKILL_ROOT}/scripts/fetch_review_requests.py --org getsentry --teams +``` + +### 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: "` — the team is a requested reviewer +- `"opened by: "` — 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 diff --git a/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py b/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py new file mode 100644 index 0000000..8e170e4 --- /dev/null +++ b/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py @@ -0,0 +1,112 @@ +# /// 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: + 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["name"] for t in reviewers_data.get("teams", [])] + matching_teams = [ + t for t in requested_team_names + if any(slug.lower() in t.lower() or t.lower() in slug.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() From 502c0a8cc2cf6f65e0296bab0033222245da8688 Mon Sep 17 00:00:00 2001 From: Evan Hicks Date: Thu, 19 Feb 2026 14:15:48 -0500 Subject: [PATCH 2/3] exact match --- .../skills/gh-review-requests/scripts/fetch_review_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py b/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py index 8e170e4..326a829 100644 --- a/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py +++ b/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py @@ -80,7 +80,7 @@ def main(): requested_team_names = [t["name"] for t in reviewers_data.get("teams", [])] matching_teams = [ t for t in requested_team_names - if any(slug.lower() in t.lower() or t.lower() in slug.lower() for slug in team_slugs) + if any(slug.lower() == t.lower() for slug in team_slugs) ] by_team_member = author in members From 0e2a68df588d83d8da7159578b4bfc631822b7b8 Mon Sep 17 00:00:00 2001 From: Evan Hicks Date: Fri, 20 Feb 2026 10:50:08 -0500 Subject: [PATCH 3/3] fixes --- .../gh-review-requests/scripts/fetch_review_requests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py b/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py index 326a829..0eb0255 100644 --- a/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py +++ b/plugins/sentry-skills/skills/gh-review-requests/scripts/fetch_review_requests.py @@ -27,7 +27,8 @@ def gh(path: str, paginate: bool = False) -> list | dict: if paginate: cmd.append("--paginate") result = subprocess.run(cmd, capture_output=True, text=True) - if result.returncode != 0: + 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) @@ -77,7 +78,7 @@ def main(): author = pr_data["user"]["login"] reviewers_data = gh(f"repos/{repo}/pulls/{pr_num}/requested_reviewers") - requested_team_names = [t["name"] for t in reviewers_data.get("teams", [])] + 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)