diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml new file mode 100644 index 00000000..a57b696d --- /dev/null +++ b/.github/workflows/pr-review.yml @@ -0,0 +1,90 @@ +# GitHub Action: Automated PR Review +# Triggers on pull request events and posts a structured review comment. +# +# Usage: Copy this file to your repo's .github/workflows/ directory. +# Set GITHUB_TOKEN (automatic) or a PAT with repo scope for cross-repo reviews. + +name: PR Review Agent + +on: + pull_request: + types: [opened, synchronize] + +permissions: + contents: read + pull-requests: write + +jobs: + review: + runs-on: ubuntu-latest + name: Automated PR Review + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Fetch PR diff + id: diff + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_URL="${{ github.event.pull_request.html_url }}" + echo "pr_url=${PR_URL}" >> $GITHUB_OUTPUT + + # Fetch diff via GitHub API + curl -s -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Accept: application/vnd.github.v3.diff" \ + "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" \ + > /tmp/pr.diff + + echo "Diff saved: $(wc -l < /tmp/pr.diff) lines" + + - name: Generate review + id: review + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python3 ${{ github.workspace }}/claude_review.py \ + --pr "${{ steps.diff.outputs.pr_url }}" \ + --output /tmp/review.md + + # Store review content for the comment step + cat /tmp/review.md >> $GITHUB_OUTPUT + + - name: Post review comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const review = fs.readFileSync('/tmp/review.md', 'utf8'); + + // Find existing bot comment to update + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(c => + c.user.type === 'Bot' && c.body.includes('Automated PR Review') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: review, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: review, + }); + } diff --git a/agents/pr-reviewer/README.md b/agents/pr-reviewer/README.md new file mode 100644 index 00000000..2c4c68d9 --- /dev/null +++ b/agents/pr-reviewer/README.md @@ -0,0 +1,172 @@ +# ๐Ÿค– claude-review โ€” Automated PR Review Agent + +A CLI tool and GitHub Action that analyzes GitHub Pull Requests and generates structured Markdown review reports. + +> **$150 Bounty Submission** โ€” [Issue #4](https://github.com/claude-builders-bounty/claude-builders-bounty/issues/4) + +## Features + +- ๐Ÿ” **CLI tool** โ€” Review any GitHub PR from the command line +- ๐Ÿค– **GitHub Action** โ€” Auto-review PRs on every push +- ๐Ÿ“‹ **Structured output** โ€” Summary, risks, suggestions, confidence score +- ๐Ÿš€ **Zero dependencies** โ€” Pure Python standard library +- ๐Ÿ”’ **Security detection** โ€” Flags common vulnerabilities in diffs + +## Quick Start + +### CLI Usage + +```bash +# Install (no pip needed โ€” single file) +git clone https://github.com/zlplzp123wyt/claude-builders-bounty +cd claude-builders-bounty +pip install -e . # or just run python3 claude_review.py directly + +# Review a PR +claude-review --pr https://github.com/psf/requests/pull/7310 + +# Save to file +claude-review --pr https://github.com/owner/repo/pull/42 -o review.md + +# JSON output +claude-review --pr https://github.com/owner/repo/pull/42 --json + +# Read diff from stdin +git diff | claude-review --stdin --pr https://github.com/owner/repo/pull/42 +``` + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `GITHUB_TOKEN` | GitHub personal access token (optional, increases rate limits) | + +```bash +export GITHUB_TOKEN=ghp_xxxxxxxxxxxx +claude-review --pr https://github.com/owner/repo/pull/42 +``` + +### GitHub Action + +Add this workflow to your repository (`.github/workflows/pr-review.yml`): + +```yaml +name: PR Review Agent + +on: + pull_request: + types: [opened, synchronize] + +permissions: + contents: read + pull-requests: write + +jobs: + review: + runs-on: ubuntu-latest + name: Automated PR Review + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Generate review + id: review + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python3 ${{ github.workspace }}/claude_review.py \ + --pr "${{ github.event.pull_request.html_url }}" \ + --output /tmp/review.md + + - name: Post review comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const review = fs.readFileSync('/tmp/review.md', 'utf8'); + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: context.issue.number, + }); + const botComment = comments.find(c => + c.user.type === 'Bot' && c.body.includes('Automated PR Review') + ); + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, repo: context.repo.repo, + comment_id: botComment.id, body: review, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: context.issue.number, body: review, + }); + } +``` + +## Output Format + +Each review includes: + +### ๐Ÿ“‹ Summary of Changes +2โ€“3 sentence overview: PR title, author, files changed, line counts, and purpose. + +### โš ๏ธ Identified Risks +- ๐Ÿ”’ Security risks (hardcoded secrets, eval/exec, SQL injection, XSS) +- ๐Ÿ“ Large PR warnings (>500 lines) +- ๐Ÿงช Missing test coverage +- ๐Ÿ“ฆ Dependency changes +- โš™๏ธ Configuration/deployment changes +- ๐Ÿ—‘๏ธ Removed tests +- โš ๏ธ TODO/FIXME markers + +### ๐Ÿ’ก Improvement Suggestions +- Debug statement removal +- Bare except clause warnings +- Magic number extraction +- Missing comments on complex code +- Documentation updates + +### ๐Ÿ“Š Confidence Score +- ๐ŸŸข **High** โ€” Small, well-scoped changes +- ๐ŸŸก **Medium** โ€” Moderate complexity, manual review recommended +- ๐Ÿ”ด **Low** โ€” High complexity/risk, thorough review needed + +## Sample Output + +See [`examples/`](examples/) for real-world review outputs: + +- **sample_review_1.md** โ€” [psf/requests#7310](https://github.com/psf/requests/pull/7310) (DigestAuth FIPS fix) +- **sample_review_2.md** โ€” [fastapi/fastapi#15263](https://github.com/fastapi/fastapi/pull/15263) (pygments bump) + +## CLI Arguments + +| Argument | Description | +|----------|-------------| +| `--pr URL` | GitHub PR URL (required) | +| `-o`, `--output FILE` | Output file path (default: stdout) | +| `--json` | Output as JSON instead of Markdown | +| `--stdin` | Read diff from stdin instead of fetching | +| `--token TOKEN` | GitHub token (or use `GITHUB_TOKEN` env) | + +## How It Works + +1. **Parse** the GitHub PR URL to extract owner, repo, and PR number +2. **Fetch** the PR diff and metadata via GitHub API +3. **Analyze** the diff for security issues, size, test coverage, patterns +4. **Generate** a structured Markdown report with summary, risks, suggestions, and confidence + +## Requirements + +- Python 3.8+ +- Internet access (to fetch PRs from GitHub API) +- No external dependencies + +## License + +MIT diff --git a/agents/pr-reviewer/claude_review.py b/agents/pr-reviewer/claude_review.py new file mode 100644 index 00000000..ea6bcead --- /dev/null +++ b/agents/pr-reviewer/claude_review.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 +""" +claude-review: A CLI tool that reviews GitHub PRs and outputs structured Markdown reports. + +Usage: + claude-review --pr https://github.com/owner/repo/pull/123 + claude-review --pr https://github.com/owner/repo/pull/123 --output review.md + cat diff.txt | claude-review --stdin --pr https://github.com/owner/repo/pull/123 +""" + +import argparse +import json +import os +import re +import subprocess +import sys +import urllib.request +import urllib.error +from typing import Optional + + +def parse_pr_url(url: str) -> dict: + """Parse a GitHub PR URL into owner, repo, and PR number.""" + pattern = r"https?://github\.com/([^/]+)/([^/]+)/pull/(\d+)" + match = re.match(pattern, url.strip().rstrip("/")) + if not match: + raise ValueError(f"Invalid PR URL: {url}") + return { + "owner": match.group(1), + "repo": match.group(2), + "pr_number": int(match.group(3)), + } + + +def fetch_pr_diff(owner: str, repo: str, pr_number: int, token: Optional[str] = None) -> str: + """Fetch the diff of a GitHub PR using the GitHub API.""" + url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}" + headers = { + "Accept": "application/vnd.github.v3.diff", + "User-Agent": "claude-review/1.0", + } + if token: + headers["Authorization"] = f"token {token}" + + req = urllib.request.Request(url, headers=headers) + try: + with urllib.request.urlopen(req) as response: + return response.read().decode("utf-8") + except urllib.error.HTTPError as e: + if e.code == 404: + raise RuntimeError(f"PR not found: {owner}/{repo}#{pr_number}") + elif e.code == 403: + raise RuntimeError("GitHub API rate limit exceeded. Set GITHUB_TOKEN to increase limits.") + raise RuntimeError(f"GitHub API error: {e.code} {e.reason}") + + +def fetch_pr_metadata(owner: str, repo: str, pr_number: int, token: Optional[str] = None) -> dict: + """Fetch PR metadata (title, description, author, etc.).""" + url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}" + headers = { + "Accept": "application/vnd.github.v3+json", + "User-Agent": "claude-review/1.0", + } + if token: + headers["Authorization"] = f"token {token}" + + req = urllib.request.Request(url, headers=headers) + try: + with urllib.request.urlopen(req) as response: + return json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError: + return {} + + +def analyze_diff(diff: str, metadata: dict) -> dict: + """ + Analyze a PR diff and return structured review data. + This performs static analysis without requiring an LLM. + """ + if not diff or diff.strip() == "": + return { + "summary": "Empty diff โ€” no changes detected.", + "risks": [], + "improvements": ["Verify that the PR contains actual changes."], + "confidence": "Low", + } + + lines = diff.split("\n") + added_lines = [l for l in lines if l.startswith("+") and not l.startswith("+++")] + removed_lines = [l for l in lines if l.startswith("-") and not l.startswith("---")] + + # Parse files changed + files_changed = [] + current_file = None + for line in lines: + if line.startswith("diff --git"): + parts = line.split(" b/") + if len(parts) > 1: + current_file = parts[1].strip() + files_changed.append({"name": current_file, "additions": 0, "deletions": 0}) + elif line.startswith("+") and not line.startswith("+++") and files_changed: + files_changed[-1]["additions"] += 1 + elif line.startswith("-") and not line.startswith("---") and files_changed: + files_changed[-1]["deletions"] += 1 + + title = metadata.get("title", "Unknown PR") + body = (metadata.get("body") or "")[:500] + author = metadata.get("user", {}).get("login", "unknown") + total_additions = sum(f["additions"] for f in files_changed) + total_deletions = sum(f["deletions"] for f in files_changed) + + # Build summary + file_names = [f["name"] for f in files_changed] + file_categories = categorize_files(file_names) + summary_parts = [ + f'"{title}" by @{author} modifies {len(files_changed)} file(s) ' + f"(+{total_additions}/โˆ’{total_deletions} lines)." + ] + if file_categories: + summary_parts.append(f"Changes span: {', '.join(file_categories)}.") + if body: + clean_body = body.replace("\n", " ").strip() + if clean_body: + summary_parts.append(f"Purpose: {clean_body[:200]}.") + + # Identify risks + risks = identify_risks(diff, files_changed, added_lines, removed_lines) + + # Suggestions + improvements = suggest_improvements(diff, files_changed, added_lines, removed_lines) + + # Confidence + confidence = compute_confidence(files_changed, total_additions, total_deletions, risks) + + return { + "summary": " ".join(summary_parts), + "risks": risks, + "improvements": improvements, + "confidence": confidence, + "stats": { + "files_changed": len(files_changed), + "additions": total_additions, + "deletions": total_deletions, + "files": file_names[:20], + }, + } + + +def categorize_files(files: list) -> list: + """Categorize changed files into groups.""" + categories = set() + for f in files: + if any(f.endswith(ext) for ext in [".yml", ".yaml"]): + categories.add("CI/CD configuration") + elif any(f.endswith(ext) for ext in [".md", ".txt", ".rst"]): + categories.add("documentation") + elif any(f.endswith(ext) for ext in [".test.js", ".spec.js", ".test.ts", ".spec.ts", ".test.py", "_test.go"]): + categories.add("tests") + elif any(f.endswith(ext) for ext in [".css", ".scss", ".html", ".jsx", ".tsx", ".svelte", ".vue"]): + categories.add("frontend/UI") + elif any(f.endswith(ext) for ext in [".py", ".js", ".ts", ".go", ".rs", ".java", ".rb"]): + categories.add("source code") + elif any(f.endswith(ext) for ext in [".json", ".toml", ".cfg", ".ini"]): + categories.add("configuration") + elif any(f.endswith(ext) for ext in [".sh", ".bash"]): + categories.add("scripts") + elif "dockerfile" in f.lower() or "docker-compose" in f.lower(): + categories.add("Docker/containerization") + else: + categories.add("other") + return sorted(categories) + + +def identify_risks(diff: str, files: list, added: list, removed: list) -> list: + """Identify potential risks in the diff.""" + risks = [] + added_text = "\n".join(added) + removed_text = "\n".join(removed) + + # Security risks + security_patterns = [ + (r"(?i)(password|secret|api_key|token)\s*=\s*['\"]", "Hardcoded secrets/credentials detected"), + (r"(?i)eval\s*\(", "Use of eval() โ€” potential code injection risk"), + (r"(?i)exec\s*\(", "Use of exec() โ€” potential code execution risk"), + (r"(?i)subprocess\.call\(.*shell\s*=\s*True", "Shell injection risk: subprocess with shell=True"), + (r"(?i)innerHTML\s*=", "Direct innerHTML assignment โ€” XSS risk"), + (r"(?i)dangerouslySetInnerHTML", "React dangerouslySetInnerHTML usage โ€” XSS risk"), + (r"(?i)disable.*ssl|verify\s*=\s*False", "SSL verification disabled"), + (r"(?i)(?:SELECT|INSERT|UPDATE|DELETE).*%s|\.format\(|f\".*SELECT", "Possible SQL injection vulnerability"), + ] + for pattern, message in security_patterns: + if re.search(pattern, added_text): + risks.append(f"๐Ÿ”’ **Security**: {message}") + + # Size risks + if len(added) + len(removed) > 500: + risks.append(f"๐Ÿ“ **Large PR**: {len(added)} additions and {len(removed)} deletions across {len(files)} files โ€” consider splitting into smaller PRs") + + # Test coverage + test_files = [f for f in files if any(t in f["name"].lower() for t in ["test", "spec"])] + source_files = [f for f in files if f not in test_files] + if source_files and not test_files: + risks.append("๐Ÿงช **No tests**: Source code changed but no test files updated โ€” potential regression risk") + + # Dependency changes + dep_files = [f for f in files if any(d in f["name"] for d in ["package.json", "requirements.txt", "Pipfile", "go.mod", "Cargo.toml", "Gemfile", "pom.xml"])] + if dep_files: + risks.append(f"๐Ÿ“ฆ **Dependency change**: {', '.join(f['name'] for f in dep_files)} modified โ€” review for breaking changes or vulnerabilities") + + # Config/deployment changes + config_files = [f for f in files if any(c in f["name"].lower() for c in [".env", "config", "settings", "deploy", "kubernetes", "helm"])] + if config_files: + risks.append(f"โš™๏ธ **Config/Deploy change**: {', '.join(f['name'] for f in config_files)} โ€” verify environment-specific values") + + # Deleted tests + deleted_tests = [l for l in removed if re.search(r"(?i)(def test_|it\(|describe\(|@Test)", l)] + if deleted_tests: + risks.append(f"๐Ÿ—‘๏ธ **Test removal**: {len(deleted_tests)} test function(s) removed โ€” ensure intentional") + + # TODO/FIXME/HACK + todos = [l for l in added if re.search(r"(?i)(TODO|FIXME|HACK|XXX|WORKAROUND)", l)] + if todos: + risks.append(f"โš ๏ธ **Technical debt**: {len(todos)} TODO/FIXME/HACK marker(s) added") + + # Catch-all for small PR with no detected risks + if not risks: + risks.append("โœ… No significant risks detected") + + return risks + + +def suggest_improvements(diff: str, files: list, added: list, removed: list) -> list: + """Suggest improvements based on the diff.""" + suggestions = [] + added_text = "\n".join(added) + + # Logging + print_stmts = len(re.findall(r"(?i)(console\.log|print\(|System\.out\.print|fmt\.Print|puts )", added_text)) + if print_stmts > 0: + suggestions.append(f"๐Ÿงน Remove or replace {print_stmts} debug print/log statement(s) with proper logging") + + # Error handling + bare_except = len(re.findall(r"except\s*:", added_text)) + if bare_except > 0: + suggestions.append("๐ŸŽฏ Use specific exception types instead of bare `except:` clauses") + + # Magic numbers + magic = re.findall(r"(? 50: + has_comments = any(l.strip().startswith(("#", "//", "*", "/*", '"""', "'''")) for l in added) + if not has_comments: + suggestions.append("๐Ÿ“ Add comments to explain complex logic in large changes") + + # Consistency + if "TODO" in added_text or "FIXME" in added_text: + suggestions.append("๐Ÿ“‹ Address or track TODO/FIXME items before merging") + + # Documentation + doc_files = [f for f in files if any(d in f["name"] for d in [".md", ".rst", "README"])] + source_files = [f for f in files if f not in doc_files] + if source_files and not doc_files and len(added) > 20: + suggestions.append("๐Ÿ“š Consider updating documentation to reflect code changes") + + # Catch-all + if not suggestions: + suggestions.append("๐Ÿ‘ Code changes look clean โ€” no specific improvements suggested") + + return suggestions + + +def compute_confidence(files: list, additions: int, deletions: int, risks: list) -> str: + """Compute review confidence score.""" + score = 100 + + # Penalize large changes + total = additions + deletions + if total > 1000: + score -= 40 + elif total > 500: + score -= 25 + elif total > 200: + score -= 15 + + # Penalize many files + if len(files) > 20: + score -= 30 + elif len(files) > 10: + score -= 15 + + # Penalize security risks + security_risks = [r for r in risks if "๐Ÿ”’" in r] + score -= len(security_risks) * 25 + + # Penalize other significant risks + other_risks = [r for r in risks if r != "โœ… No significant risks detected"] + score -= len(other_risks) * 5 + + if score >= 75: + return "High" + elif score >= 45: + return "Medium" + else: + return "Low" + + +def format_markdown(review: dict, pr_url: str) -> str: + """Format the review as structured Markdown.""" + confidence_emoji = {"High": "๐ŸŸข", "Medium": "๐ŸŸก", "Low": "๐Ÿ”ด"} + emoji = confidence_emoji.get(review["confidence"], "โšช") + + lines = [ + "## ๐Ÿค– Automated PR Review", + "", + "### ๐Ÿ“‹ Summary of Changes", + "", + review["summary"], + "", + "### โš ๏ธ Identified Risks", + "", + ] + + for risk in review["risks"]: + lines.append(f"- {risk}") + + lines.extend([ + "", + "### ๐Ÿ’ก Improvement Suggestions", + "", + ]) + + for suggestion in review["improvements"]: + lines.append(f"- {suggestion}") + + lines.extend([ + "", + "### ๐Ÿ“Š Confidence Score", + "", + f"{emoji} **{review['confidence']}** โ€” ", + ]) + + if review["confidence"] == "High": + lines.append("Changes are well-scoped and reviewable.") + elif review["confidence"] == "Medium": + lines.append("Moderate complexity โ€” manual review recommended for flagged items.") + else: + lines.append("High complexity or risk โ€” thorough manual review strongly recommended.") + + if "stats" in review: + stats = review["stats"] + lines.extend([ + "", + "
", + "๐Ÿ“Š Diff Statistics", + "", + f"- **Files changed:** {stats['files_changed']}", + f"- **Lines added:** {stats['additions']}", + f"- **Lines removed:** {stats['deletions']}", + f"- **Files:** {', '.join(stats['files'][:10])}{'...' if len(stats['files']) > 10 else ''}", + "", + "
", + ]) + + lines.extend([ + "", + "---", + f"*Generated by [claude-review](https://github.com/zlplzp123wyt/claude-builders-bounty) โ€ข [PR Link]({pr_url})*", + ]) + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser( + description="Review GitHub PRs and output structured Markdown reports", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + claude-review --pr https://github.com/facebook/react/pull/1000 + claude-review --pr https://github.com/vercel/next.js/pull/5000 --output review.md + claude-review --pr https://github.com/owner/repo/pull/42 --json + """, + ) + parser.add_argument("--pr", required=True, help="GitHub PR URL") + parser.add_argument("--output", "-o", help="Output file path (default: stdout)") + parser.add_argument("--json", action="store_true", help="Output as JSON instead of Markdown") + parser.add_argument("--stdin", action="store_true", help="Read diff from stdin instead of fetching from GitHub") + parser.add_argument("--token", help="GitHub token (or set GITHUB_TOKEN env var)") + + args = parser.parse_args() + + # Validate PR URL + try: + pr_info = parse_pr_url(args.pr) + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + token = args.token or os.environ.get("GITHUB_TOKEN") + + # Fetch diff + if args.stdin: + diff = sys.stdin.read() + else: + print(f"Fetching PR: {pr_info['owner']}/{pr_info['repo']}#{pr_info['pr_number']}...", file=sys.stderr) + diff = fetch_pr_diff(pr_info["owner"], pr_info["repo"], pr_info["pr_number"], token) + + # Fetch metadata + metadata = fetch_pr_metadata(pr_info["owner"], pr_info["repo"], pr_info["pr_number"], token) + + # Analyze + print("Analyzing changes...", file=sys.stderr) + review = analyze_diff(diff, metadata) + + # Output + if args.json: + output = json.dumps(review, indent=2, ensure_ascii=False) + else: + output = format_markdown(review, args.pr) + + if args.output: + with open(args.output, "w") as f: + f.write(output) + print(f"Review written to {args.output}", file=sys.stderr) + else: + print(output) + + +if __name__ == "__main__": + main() diff --git a/agents/pr-reviewer/examples/sample_review_1.md b/agents/pr-reviewer/examples/sample_review_1.md new file mode 100644 index 00000000..93ef75c3 --- /dev/null +++ b/agents/pr-reviewer/examples/sample_review_1.md @@ -0,0 +1,31 @@ +## ๐Ÿค– Automated PR Review + +### ๐Ÿ“‹ Summary of Changes + +"Move DigestAuth hash algorithms to use usedforsecurity=False" by @nateprewitt modifies 1 file(s) (+5/โˆ’5 lines). Changes span: source code. Purpose: This PR moves the DigestAuth hash algorithms to use `usedforsecurity=False`. This is to both avoid unnecessarily breaking DigestAuth on FIPS enabled systems and to make it clearer to security reporter. + +### โš ๏ธ Identified Risks + +- ๐Ÿงช **No tests**: Source code changed but no test files updated โ€” potential regression risk + +### ๐Ÿ’ก Improvement Suggestions + +- ๐Ÿ‘ Code changes look clean โ€” no specific improvements suggested + +### ๐Ÿ“Š Confidence Score + +๐ŸŸข **High** โ€” +Changes are well-scoped and reviewable. + +
+๐Ÿ“Š Diff Statistics + +- **Files changed:** 1 +- **Lines added:** 5 +- **Lines removed:** 5 +- **Files:** src/requests/auth.py + +
+ +--- +*Generated by [claude-review](https://github.com/zlplzp123wyt/claude-builders-bounty) โ€ข [PR Link](https://github.com/psf/requests/pull/7310)* \ No newline at end of file diff --git a/agents/pr-reviewer/examples/sample_review_2.md b/agents/pr-reviewer/examples/sample_review_2.md new file mode 100644 index 00000000..bf51b611 --- /dev/null +++ b/agents/pr-reviewer/examples/sample_review_2.md @@ -0,0 +1,31 @@ +## ๐Ÿค– Automated PR Review + +### ๐Ÿ“‹ Summary of Changes + +"โฌ† Bump pygments from 2.19.2 to 2.20.0" by @dependabot[bot] modifies 1 file(s) (+3/โˆ’3 lines). Changes span: other. Purpose: Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to 2.20.0.
Release notes

Sourced from pyg. + +### โš ๏ธ Identified Risks + +- ๐Ÿงช **No tests**: Source code changed but no test files updated โ€” potential regression risk + +### ๐Ÿ’ก Improvement Suggestions + +- ๐Ÿ”ข Consider extracting magic numbers into named constants for readability + +### ๐Ÿ“Š Confidence Score + +๐ŸŸข **High** โ€” +Changes are well-scoped and reviewable. + +

+๐Ÿ“Š Diff Statistics + +- **Files changed:** 1 +- **Lines added:** 3 +- **Lines removed:** 3 +- **Files:** uv.lock + +
+ +--- +*Generated by [claude-review](https://github.com/zlplzp123wyt/claude-builders-bounty) โ€ข [PR Link](https://github.com/fastapi/fastapi/pull/15263)* \ No newline at end of file diff --git a/agents/pr-reviewer/requirements.txt b/agents/pr-reviewer/requirements.txt new file mode 100644 index 00000000..d033758e --- /dev/null +++ b/agents/pr-reviewer/requirements.txt @@ -0,0 +1 @@ +# No external dependencies required โ€” uses Python standard library only diff --git a/agents/pr-reviewer/setup.py b/agents/pr-reviewer/setup.py new file mode 100644 index 00000000..8f0f4c2e --- /dev/null +++ b/agents/pr-reviewer/setup.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +"""Setup script for claude-review.""" +from setuptools import setup + +setup( + name="claude-review", + version="1.0.0", + description="CLI tool that reviews GitHub PRs and outputs structured Markdown reports", + author="claude-review", + py_modules=["claude_review"], + entry_points={ + "console_scripts": [ + "claude-review=claude_review:main", + ], + }, + python_requires=">=3.8", +)