-
Notifications
You must be signed in to change notification settings - Fork 3
Updated Gemini Automatic Code Review Agent #268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
ab90cce
fb77f9f
628aa93
73058e3
eb4a76e
87d5aba
3d38454
df76590
8cbe531
cac8706
4c9acf2
03fbca4
9fff58b
19fb76a
045e89b
5663add
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # Code Reviewer Instructions & Domain Knowledge | ||
|
|
||
| ## Model & Infrastructure Rules | ||
| - We use the `gemini-3.6-flash` model endpoint across our CI/CD workflows. It is valid and working; do not suggest changing model versions. | ||
|
|
||
| ## FRC Robotics Best Practices | ||
| - **WPILib Command-Based:** Verify subsystems require commands properly (`addRequirements(this)`). | ||
| - **CAN Bus Safety:** Ensure CAN IDs and motor controller configurations avoid blocking calls in periodic loops. | ||
| - **Null Safety:** Check that sensors and encoders initialized in constructor are null-checked before `.get()`. | ||
| - **Educational Tone:** Explain *why* something is an issue rather than just giving a diff. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| name: Gemini FRC Code Review | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened] | ||
| issue_comment: | ||
| types: [created] | ||
|
|
||
| jobs: | ||
| gemini-review: | ||
| if: | | ||
| github.event_name == 'pull_request' || | ||
| (github.event.issue.pull_request && startsWith(github.event.comment.body, '/gemini')) | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| issues: write | ||
|
|
||
| steps: | ||
| # Step 1: Acknowledge Command with Reactions API (if triggered by comment) | ||
| - name: Acknowledge Command | ||
| if: ${{ github.event_name == 'issue_comment' }} | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \ | ||
| -f content="eyes" | ||
|
|
||
| # Step 2: Fetch PR Details, Diff, and Rules | ||
| - name: Fetch PR Details, Diff, and Rules | ||
| id: fetch_data | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| if [ "${{ github.event_name }}" = "pull_request" ]; then | ||
| PR_NUM="${{ github.event.pull_request.number }}" | ||
| else | ||
| PR_NUM="${{ github.event.issue.number }}" | ||
| fi | ||
|
|
||
| echo "PR_NUMBER=$PR_NUM" >> $GITHUB_ENV | ||
|
|
||
| # Get PR Head Commit SHA (required for inline comments) | ||
| COMMIT_SHA=$(gh pr view "$PR_NUM" --repo "${{ github.repository }}" --json headRefOid -q .headRefOid) | ||
| echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_ENV | ||
|
|
||
| # Download PR diff | ||
| gh pr diff "$PR_NUM" --repo "${{ github.repository }}" > pr_diff.txt | ||
| head -c 20000 pr_diff.txt > trimmed_diff.txt | ||
|
markpete marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Fetch reviewer-rules.md if present | ||
|
markpete marked this conversation as resolved.
|
||
| gh api "repos/${{ github.repository }}/contents/.github/reviewer-rules.md" \ | ||
| -H "Accept: application/vnd.github.raw+json" > rules.txt 2>/dev/null || touch rules.txt | ||
|
|
||
| # Step 3: Run Gemini & Post Inline Review Comments | ||
| - name: Post Inline Gemini Review | ||
| env: | ||
| GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| USER_COMMENT: ${{ github.event.comment.body }} | ||
| run: | | ||
| python3 - << 'EOF' | ||
| import urllib.request, urllib.error, json, os, sys, time, subprocess | ||
|
|
||
| api_key = os.environ.get('GEMINI_API_KEY', '').strip() | ||
| pr_number = os.environ.get('PR_NUMBER', '') | ||
| commit_sha = os.environ.get('COMMIT_SHA', '') | ||
| repo = os.environ.get('GITHUB_REPOSITORY', '') | ||
|
|
||
| if not api_key: | ||
| print('Error: GEMINI_API_KEY secret is not set.') | ||
| sys.exit(1) | ||
|
|
||
| models_to_try = ['gemini-3.6-flash', 'gemini-3.5-flash', 'gemini-2.0-flash'] | ||
|
|
||
| try: | ||
| with open('trimmed_diff.txt', 'r', encoding='utf-8', errors='replace') as f: | ||
| diff_text = f.read() | ||
| except Exception as e: | ||
| print(f'Error reading diff: {e}') | ||
| sys.exit(1) | ||
|
|
||
| rules_text = "" | ||
| if os.path.exists('rules.txt'): | ||
| with open('rules.txt', 'r', encoding='utf-8', errors='replace') as rf: | ||
| content = rf.read().strip() | ||
| if content: | ||
| rules_text = f"\n\nPROJECT RULES & SKILLS:\n{content}" | ||
|
|
||
| if not diff_text.strip(): | ||
| print('No code changes detected.') | ||
| sys.exit(0) | ||
|
|
||
| system_prompt = f"""You are a lead mentor for an FRC robotics team reviewing a pull request diff. | ||
| Provide feedback on WPILib best practices, thread safety, motor safety, logic bugs, and null safety. | ||
| {rules_text} | ||
|
|
||
| CRITICAL OUTPUT FORMAT: | ||
| You must respond ONLY with a raw JSON object (no markdown code blocks, no backticks, no extra text) matching this schema: | ||
| {{ | ||
| "summary": "High-level summary for the PR description", | ||
| "comments": [ | ||
| {{ | ||
| "path": "path/to/file.java", | ||
| "line": 42, | ||
| "body": "Your inline comment or suggestion here." | ||
| }} | ||
| ] | ||
| }} | ||
|
|
||
| NOTE: The 'line' must correspond to a valid line number in the NEW modified file (+ lines in diff). Only comment on lines that need improvement or praise.""" | ||
|
|
||
| prompt = f"{system_prompt}\n\nDiff to Review:\n{diff_text}" | ||
| data = json.dumps({'contents': [{'parts': [{'text': prompt}]}]}).encode('utf-8') | ||
|
|
||
| response_json = None | ||
| for model in models_to_try: | ||
| url = f'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent' | ||
| req = urllib.request.Request( | ||
| url, | ||
| data=data, | ||
| headers={ | ||
| 'Content-Type': 'application/json', | ||
| 'x-goog-api-key': api_key | ||
| } | ||
| ) | ||
| try: | ||
| with urllib.request.urlopen(req) as res: | ||
| res_data = json.loads(res.read().decode('utf-8')) | ||
|
markpete marked this conversation as resolved.
markpete marked this conversation as resolved.
|
||
| raw_text = res_data['candidates'][0]['content']['parts'][0]['text'].strip() | ||
| # Clean potential markdown wraps from response | ||
| if raw_text.startswith("```json"): | ||
| raw_text = raw_text[7:] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Trailing whitespace or newlines after the closing backticks can cause |
||
| if raw_text.startswith("```"): | ||
| raw_text = raw_text[3:] | ||
| if raw_text.endswith("```"): | ||
| raw_text = raw_text[:-3] | ||
| response_json = json.loads(raw_text.strip()) | ||
| break | ||
| except urllib.error.HTTPError as e: | ||
| if e.code in [503, 429, 404]: | ||
| time.sleep(1) | ||
| continue | ||
| else: | ||
| print(f'HTTP Error {e.code}') | ||
| break | ||
| except Exception as e: | ||
| print(f'Parsing error: {e}') | ||
| break | ||
|
|
||
| if not response_json: | ||
| print('Failed to generate structured review.') | ||
| sys.exit(0) | ||
|
|
||
| # 1. Post High-level Summary Comment | ||
| summary = response_json.get('summary', 'PR reviewed.') | ||
| subprocess.run([ | ||
| 'gh', 'pr', 'comment', pr_number, | ||
| '--repo', repo, | ||
| '--body', f"🤖 **Gemini Code Review Summary**\n\n{summary}" | ||
| ]) | ||
|
|
||
| # 2. Post Inline Comments | ||
| comments = response_json.get('comments', []) | ||
| for c in comments: | ||
| file_path = c.get('path') | ||
| line_num = c.get('line') | ||
| body_msg = c.get('body') | ||
|
|
||
| if file_path and line_num and body_msg: | ||
| # Use GitHub REST API for pull request review comments | ||
| cmd = [ | ||
| 'gh', 'api', | ||
| f'repos/{repo}/pulls/{pr_number}/comments', | ||
| '-X', 'POST', | ||
| '-f', f'body=🤖 {body_msg}', | ||
| '-f', f'commit_id={commit_sha}', | ||
| '-f', f'path={file_path}', | ||
| '-F', f'line={line_num}', | ||
| '-f', 'side=RIGHT' | ||
| ] | ||
|
markpete marked this conversation as resolved.
|
||
| subprocess.run(cmd) | ||
|
|
||
| EOF | ||
Uh oh!
There was an error while loading. Please reload this page.