Iterate Pr
SentryOfficialPublished by Sentry
- Skill
- Workflow
- Software development
- Apache-2.0
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The iterate-pr skill provides a structured workflow for managing pull requests by automatically monitoring CI status and review feedback. It guides an agent to identify actionable failures, apply fixes, and verify changes locally before pushing updates. The process focuses on resolving high and medium priority issues while intelligently handling low-priority feedback and ignoring non-actionable gates like human approvals or draft status.
What it does
- Fetches and categorizes PR feedback into high, medium, and low priority buckets.
- Monitors CI check status to distinguish between actionable failures and pending gates.
- Provides a systematic loop for fixing root causes of CI failures and pushing updates.
- Handles low-priority feedback by prompting the user for guidance.
- Uses specialized scripts to interact with PR metadata, checks, and review threads.
- Includes fallback procedures using standard CLI tools if automated scripts encounter errors.
When to use it
- When a PR has failed CI checks that require code adjustments.
- When there is high or medium priority review feedback that needs to be addressed.
- To automate the repetitive cycle of fixing, testing, and pushing code to a PR.
- When you need to ensure a PR is clean and ready for human review without manual intervention.
Advantages
- Reduces manual overhead in the PR feedback and CI repair loop.
- Ensures consistent handling of feedback priorities.
- Focuses on root cause resolution rather than symptomatic fixes.
- Integrates seamlessly with existing CI/CD workflows and GitHub CLI.
Usage examples
- “Iterate on PR #123 until all CI checks pass.”
- The agent will fetch the PR status, identify failed checks, apply necessary fixes, and continue looping until all actionable CI failures are resolved.
- “Address the high and medium feedback on my current PR.”
- The agent will retrieve the review comments, categorize them, and systematically apply fixes for all high and medium priority items.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/getsentry/iterate-pr/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip iterate-pr.zip -d .agents/skills
Documentation by Sentry
Iterate on PR Until CI Passes
Goal: fix actionable CI failures and high/medium review feedback. Stop and report human approval, draft-readiness, and merge-readiness gates.
Requires:
authenticated
ghuvtarget repository root as cwd
skill-root-relative script paths, for example
scripts/fetch_pr_checks.py
Bundled Scripts
| Script | Run | Output |
|---|---|---|
scripts/fetch_pr_checks.py | uv run scripts/fetch_pr_checks.py [--pr NUMBER] | JSON: pr, summary, checks, failure snippets |
scripts/fetch_pr_feedback.py | uv run scripts/fetch_pr_feedback.py [--pr NUMBER] | JSON buckets: high, medium, low, bot, resolved |
scripts/monitor_pr_checks.py | uv run scripts/monitor_pr_checks.py [--pr NUMBER] | terminal marker plus tab-separated checks |
scripts/reply_to_thread.py | uv run scripts/reply_to_thread.py THREAD_ID BODY [...] | JSON reply results |
Check summary fields include failed, pending, actionable_pending, and human_gate_pending.
Monitor markers:
ALL_CHECKS_PASSEDCHECKS_DONE_WITH_FAILURESNO_CHECKS_REGISTEREDDRAFT_PR_WITH_NO_CHECKSCHECKS_BLOCKED_BY_REVIEW_GATE
Workflow
1. Identify PR
Run:
gh pr view --json number,url,headRefName,isDraft,reviewDecisionStop when:
no PR exists
draft PR has no checks after monitor grace period: report
DRAFT_PR_WITH_NO_CHECKS
Draft rule: inspect existing checks/feedback only. Do not mark ready for review unless asked.
2. Handle Feedback
Run uv run scripts/fetch_pr_feedback.py [--pr NUMBER].
| Bucket | Action |
|---|---|
high | fix |
medium | fix |
low | ask user which to address |
bot | skip informational comments |
resolved | skip |
Feedback fix checklist:
verify root cause
search related code
fix all instances
for
review_bot: true: fix real issues, explain false positives
Low-priority prompt format:
Found 3 low-priority suggestions:
1. [l] "Consider renaming this variable" - @reviewer in api.py:42
2. [nit] "Could use a list comprehension" - @reviewer in utils.py:18
3. [style] "Add a docstring" - @reviewer in models.py:55
Which should I address? ("1,3", "all", or "none")3. Check CI Status
Run uv run scripts/fetch_pr_checks.py [--pr NUMBER].
| State | Action |
|---|---|
failed > 0 and actionable_pending == 0 | fix failures |
actionable_pending > 0 | wait; poll feedback while waiting |
pending > 0 and actionable_pending == 0 | report CHECKS_BLOCKED_BY_REVIEW_GATE |
| no checks after grace period | report NO_CHECKS_REGISTERED or DRAFT_PR_WITH_NO_CHECKS |
| all actionable checks passed | run post-CI feedback check |
Wait for actionable review bots: sentry, warden, cursor, bugbot, seer, codeql.
Do not wait for approval, isDraft, REVIEW_REQUIRED, Codecov, or informational bots.
4. Fix CI Failures
For each failure:
read full log:
gh run view <run-id> --log-failedtrace from assertion/exception/lint rule to source
state the cause before editing: "fails because X, affected by Y"
search related call sites/patterns
fix root cause, not symptom
add focused test coverage when needed
5. Verify Locally, Then Commit and Push
Before commit:
test fix: rerun specific test
lint/type fix: rerun affected checker
code fix: rerun covering tests
local failure: fix before pushing
git add <files>
git commit -m "fix: <descriptive message>"
git push6. Monitor CI and Address Feedback
Loop:
run
uv run scripts/fetch_pr_checks.pyhandle table in step 3
while
actionable_pending > 0, runuv run scripts/fetch_pr_feedback.pyfix new high/medium feedback immediately
if changed, verify, commit, push, restart loop
otherwise sleep 30 seconds and repeat
after checks pass, wait 10 seconds, fetch feedback once more
if new high/medium feedback exists, return to step 4
Claude Code optional: run uv run scripts/monitor_pr_checks.py through MonitorTool with persistent: false; set timeout to normal repo CI duration. Restart the monitor after every push.
Exit Conditions
| Exit | Conditions |
|---|---|
| Success | actionable CI passed; post-CI feedback clean; low-priority choice handled |
| Ask user | same failure after 2 attempts; feedback unclear; infrastructure issue |
| Stop | no PR; branch needs rebase; no checks; draft no-checks; only human gates remain |
Fallback
If scripts fail, use gh CLI directly:
gh pr view --json number,url,headRefName,isDraft,reviewDecisiongh pr checks --json name,state,bucket,description,linkgh run view <run-id> --log-failedgh api repos/{owner}/{repo}/pulls/{number}/comments
Files
- ATTRIBUTION.mdAdded by AutoGineer844 B
- LICENSELicence file added by AutoGineer10.5 KiB
- SKILL.md4.9 KiB
- SPEC.md5.2 KiB
- agents/openai.yaml292 B
- scripts/fetch_pr_checks.pyScript9.7 KiB
- scripts/fetch_pr_feedback.pyScript14.4 KiB
- scripts/monitor_pr_checks.pyScript6.5 KiB
- scripts/reply_to_thread.pyScript4.5 KiB
scripts/fetch_pr_checks.py
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# ///
"""
Fetch PR CI checks and extract relevant failure snippets.
Usage:
uv run fetch_pr_checks.py [--pr PR_NUMBER]
If --pr is not specified, uses the PR for the current branch.
Output: JSON to stdout with structured check data.
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from typing import Any
HUMAN_GATE_PATTERNS = [
r"(?i)review\s+required",
r"(?i)required\s+review",
r"(?i)requires\s+review",
r"(?i)required\s+approving\s+review",
r"(?i)approval\s+required",
r"(?i)waiting\s+for\s+approval",
r"(?i)manual\s+approval",
r"(?i)draft\s+(pull\s+request|pr)",
]
def run_gh(args: list[str]) -> dict[str, Any] | list[Any] | None:
"""Run a gh CLI command and return parsed JSON output."""
try:
result = subprocess.run(
["gh"] + args,
capture_output=True,
text=True,
check=True,
)
return json.loads(result.stdout) if result.stdout.strip() else None
except subprocess.CalledProcessError as e:
print(f"Error running gh {' '.join(args)}: {e.stderr}", file=sys.stderr)
return None
except json.JSONDecodeError:
return None
def get_pr_info(pr_number: int | None = None) -> dict[str, Any] | None:
"""Get PR info, optionally by number or for current branch."""
args = [
"pr",
"view",
"--json",
"number,url,headRefName,baseRefName,isDraft,reviewDecision",
]
if pr_number:
args.insert(2, str(pr_number))
return run_gh(args)
def get_checks(pr_number: int | None = None) -> list[dict[str, Any]]:
"""Get all checks for a PR."""
args = ["gh", "pr", "checks"]
if pr_number:
args.append(str(pr_number))
args.extend(["--json", "name,bucket,link,workflow,state,description,event"])
try:
result = subprocess.run(
args,
capture_output=True,
text=True,
)
if not result.stdout.strip():
return []
try:
checks = json.loads(result.stdout)
return checks if isinstance(checks, list) else []
except json.JSONDecodeError:
pass
checks = []
for line in result.stdout.strip().split("\n"):
if not line.strip():
continue
parts = line.split("\t")
if len(parts) >= 2:
checks.append({
"name": parts[0].strip(),
"bucket": parts[1].strip(),
"link": parts[3].strip() if len(parts) > 3 else "",
"workflow": "",
})
return checks
except Exception:
return []
def is_human_gate_check(check: dict[str, Any]) -> bool:
"""Return true when a pending entry is a human review/approval gate."""
haystack = " ".join(
str(check.get(field, ""))
for field in ("name", "state", "description", "workflow")
)
return any(re.search(pattern, haystack) for pattern in HUMAN_GATE_PATTERNS)
def get_failed_runs(branch: str) -> list[dict[str, Any]]:
"""Get recent failed workflow runs for a branch."""
result = run_gh([
"run", "list",
"--branch", branch,
"--limit", "10",
"--json", "databaseId,name,status,conclusion,headSha"
])
if not isinstance(result, list):
return []
# Return runs that failed or are in progress
return [r for r in result if r.get("conclusion") == "failure"]
def extract_failure_snippet(log_text: str, max_lines: int = 50) -> str:
"""Extract relevant failure snippet from log text.
Looks for common failure markers and extracts surrounding context.
"""
lines = log_text.split("\n")
# Patterns that indicate failure points (case-insensitive via re.IGNORECASE)
failure_patterns = [
r"error[:\s]",
r"failed[:\s]",
r"failure[:\s]",
r"traceback",
r"exception",
r"assert(ion)?.*failed",
r"FAILED",
r"panic:",
r"fatal:",
r"npm ERR!",
r"yarn error",
r"ModuleNotFoundError",
r"ImportError",
r"SyntaxError",
r"TypeError",
r"ValueError",
r"KeyError",
r"AttributeError",
r"NameError",
r"IndentationError",
r"===.*FAILURES.*===",
r"___.*___", # pytest failure separators
]
combined_pattern = "|".join(failure_patterns)
# Find lines matching failure patterns
failure_indices = []
for i, line in enumerate(lines):
if re.search(combined_pattern, line, re.IGNORECASE):
failure_indices.append(i)
if not failure_indices:
# No clear failure point, return last N lines
return "\n".join(lines[-max_lines:])
# Extract context around first failure point
# Include some context before and after
first_failure = failure_indices[0]
start = max(0, first_failure - 5)
end = min(len(lines), first_failure + max_lines - 5)
snippet_lines = lines[start:end]
# If there are more failures after our snippet, note it
remaining_failures = [i for i in failure_indices if i >= end]
if remaining_failures:
snippet_lines.append(f"\n... ({len(remaining_failures)} more error(s) follow)")
return "\n".join(snippet_lines)
def get_run_logs(run_id: int) -> str | None:
"""Get failed logs for a workflow run."""
try:
result = subprocess.run(
["gh", "run", "view", str(run_id), "--log-failed"],
capture_output=True,
text=True,
timeout=60,
)
return result.stdout if result.stdout else result.stderr
except subprocess.TimeoutExpired:
return None
except subprocess.CalledProcessError:
return None
def main():
parser = argparse.ArgumentParser(description="Fetch PR CI checks with failure snippets")
parser.add_argument("--pr", type=int, help="PR number (defaults to current branch PR)")
args = parser.parse_args()
# Get PR info
pr_info = get_pr_info(args.pr)
if not pr_info:
print(json.dumps({"error": "No PR found for current branch"}))
sys.exit(1)
pr_number = pr_info["number"]
branch = pr_info["headRefName"]
# Get checks
checks = get_checks(pr_number)
# Process checks and add failure snippets
processed_checks = []
failed_runs = None # Lazy load
for check in checks:
status = check.get("bucket", check.get("state", "unknown"))
human_gate = status == "pending" and is_human_gate_check(check)
processed = {
"name": check.get("name", "unknown"),
"status": status,
"link": check.get("link", ""),
"workflow": check.get("workflow", ""),
}
if check.get("state"):
processed["state"] = check["state"]
if check.get("description"):
processed["description"] = check["description"]
if human_gate:
processed["human_gate"] = True
# For failures, try to get log snippet
if processed["status"] == "fail":
if failed_runs is None:
failed_runs = get_failed_runs(branch)
# Find matching run by workflow name
workflow_name = processed["workflow"] or processed["name"]
matching_run = next(
(r for r in failed_runs if workflow_name in r.get("name", "")),
None
)
if matching_run:
logs = get_run_logs(matching_run["databaseId"])
if logs:
processed["log_snippet"] = extract_failure_snippet(logs)
processed["run_id"] = matching_run["databaseId"]
processed_checks.append(processed)
# Build output
output = {
"pr": {
"number": pr_number,
"url": pr_info.get("url", ""),
"branch": branch,
"base": pr_info.get("baseRefName", ""),
"is_draft": bool(pr_info.get("isDraft")),
"review_decision": pr_info.get("reviewDecision", ""),
},
"summary": {
"total": len(processed_checks),
"passed": sum(1 for c in processed_checks if c["status"] == "pass"),
"failed": sum(1 for c in processed_checks if c["status"] == "fail"),
"pending": sum(1 for c in processed_checks if c["status"] == "pending"),
"actionable_pending": sum(
1
for c in processed_checks
if c["status"] == "pending" and not c.get("human_gate")
),
"human_gate_pending": sum(
1
for c in processed_checks
if c["status"] == "pending" and c.get("human_gate")
),
"skipped": sum(1 for c in processed_checks if c["status"] in ("skipping", "cancel")),
},
"checks": processed_checks,
}
if pr_info.get("isDraft") and not processed_checks:
output["action_required"] = "Draft PR has no registered checks; do not wait for CI indefinitely"
elif not processed_checks:
output["action_required"] = "No registered checks; monitor before reporting NO_CHECKS_REGISTERED"
elif output["summary"]["actionable_pending"]:
output["action_required"] = "Wait for actionable checks to finish; poll feedback while waiting"
elif output["summary"]["failed"]:
output["action_required"] = "Address failed checks"
elif output["summary"]["pending"] and not output["summary"]["actionable_pending"]:
output["action_required"] = "Only human review or approval gates remain pending"
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()
Requirements and compatibility
Written for: Claude Code
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Claude Code frontmatter, Needs a command-line tool, Needs Python
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Sentry
- Publisher
- Sentry (Official)
- Source
- getsentry/skills · skills/iterate-pr · commit e94f222
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Sentry or Sentry.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download iterate-pr.zip