Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
10 changes: 10 additions & 0 deletions .github/reviewer-rules.md
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.
186 changes: 186 additions & 0 deletions .github/workflows/gemini-review.yml
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'))
Comment thread
markpete marked this conversation as resolved.
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
Comment thread
markpete marked this conversation as resolved.
Outdated

# Fetch reviewer-rules.md if present
Comment thread
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'))
Comment thread
markpete marked this conversation as resolved.
Comment thread
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:]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Trailing whitespace or newlines after the closing backticks can cause raw_text.endswith("```") to evaluate to False. Calling raw_text = raw_text.strip() right before checking endswith("```") ensures markdown code block formatting is reliably stripped before JSON parsing.

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'
]
Comment thread
markpete marked this conversation as resolved.
subprocess.run(cmd)

EOF
Loading