Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install dependencies
run: pip install -r requirements.txt

- name: Run tests
run: pytest -v

- name: Build package
run: python -m build

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: python-package
path: dist/

101 changes: 101 additions & 0 deletions .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: AI Code Review

on:
pull_request:
branches: [main]

permissions:
contents: read
pull-requests: write

jobs:
review:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install dependencies
run: pip install -r requirements.txt

- name: Get PR diff
run: |
git diff origin/main...HEAD > pr_diff.txt

- name: Run AI review
id: ai-review
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
run: |
REVIEW=$(python scripts/ai_review.py pr_diff.txt)
echo "review<<EOF" >> $GITHUB_OUTPUT
echo "$REVIEW" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "severity=$(cat severity.txt)" >> $GITHUB_OUTPUT


- name: Post review comment
uses: actions/github-script@v7
env:
REVIEW: ${{ steps.ai-review.outputs.review }}
with:
script: |
const review = process.env.REVIEW;

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## 🤖 AI Code Review\n\n${review}\n\n---\n*Powered by Gemini AI*`
});


- name: Apply severity label
uses: actions/github-script@v7
with:
script: |
const severity = '${{ steps.ai-review.outputs.severity }}';

const labelMap = {
'CRITICAL': 'ai-review: critical',
'WARNING': 'ai-review: warning',
'GOOD': 'ai-review: looks-good'
};

const label = labelMap[severity] || labelMap['WARNING'];

// Remove any existing ai-review labels
const currentLabels = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});

for (const existingLabel of currentLabels.data) {
if (existingLabel.name.startsWith('ai-review:')) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: existingLabel.name
});
}
}

// Add the new severity label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: [label]
});

console.log(`Applied label: ${label}`);
4 changes: 4 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ def is_even(n: int) -> bool:
def reverse_string(s: str) -> str:
"""Reverse a string."""
return s[::-1]

def multiply(a: int, b: int) -> int:
"""Multiply two numbers together."""
return a * b
8 changes: 8 additions & 0 deletions dangerous.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import subprocess

def run_command(user_input):
subprocess.call(user_input, shell=True)

# Exposed API Key
API_KEY = "sk-live-abc123def45678"

7 changes: 7 additions & 0 deletions dangerous2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import subprocess

def run_command(user_input):
subprocess.call(user_input, shell=True)

# Exposed API Key
API_KEY = "sk-live-abc123def45678"
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Production dependencies (none for this simple app)
# Production dependencies
google-genai>=1.0.0

# Development/testing dependencies
pytest>=7.0.0
build>=1.0.0


85 changes: 85 additions & 0 deletions scripts/ai_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from google import genai
import sys

client = genai.Client()

# Define a function that takes a code diff as input
def gemini_code_review(diff:str):

# Write a multi-line f-string prompt that includes {diff_text}
# Tell Gemini to act as a code reviewer and focus on security, bugs, performance
prompt = f"""You are an expert code reviewer. Review the following code diff and provide feedback.

Focus on:
1. Security vulnerabilities
2. Bug risks
3. Performance issues
4. Best practice violations

For each issue found, provide:
- Severity: HIGH / MEDIUM / LOW
- Description of the issue
- Suggested fix

If the code looks good, say so.

IMPORTANT: At the very end of your review, add a severity summary line in exactly this format:
SEVERITY_SUMMARY: <level>
Where <level> is one of: CRITICAL, WARNING, GOOD

Use CRITICAL if any HIGH severity issues exist.
Use WARNING if only MEDIUM or LOW severity issues exist.
Use GOOD if no issues found.

Code diff to review:

{diff_text}


Provide your review in a clear, structured format, ending with the SEVERITY_SUMMARY line."""


# Send the prompt to the model and get a response
response = client.models.generate_content(
model="gemini-3-flash-preview", contents=prompt
)

# Return just the text from the response
return response.text

def parse_severity(review_text):
"""Extract severity level from the review output."""
for line in review_text.strip().split("\n"):
if line.strip().startswith("SEVERITY_SUMMARY:"):
level = line.split(":", 1)[1].strip().upper()
if level in ("CRITICAL", "WARNING", "GOOD"):
return level
return "WARNING" # Default to WARNING if parsing fails



# Only run this code when the script is executed directly
if __name__ == "__main__":

# Check if a filename was passed as a command-line argument
if len(sys.argv) > 1:

# Get the filename from sys.argv and read the file
diff_file = sys.argv[1]
with open(diff_file, "r") as f:
diff_content = f.read()

# If no filename was passed, read from standard input
else:
diff_content = sys.stdin.read()

# Call the review function and print the result
review = gemini_code_review(diff_content)
severity = parse_severity(review)

print(review)
# Open severity.txt for writing
with open("severity.txt", "w") as f:
# Write the severity string to the file
f.write(severity)

17 changes: 17 additions & 0 deletions scripts/sample_diff.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
diff --git a/app.py b/app.py
index 1234567..abcdefg 100644
--- a/app.py
+++ b/app.py
@@ -1,5 +1,12 @@
"""Simple utility functions"""

+import sqlite3
+
+def get_user(username):
+ conn = sqlite3.connect("users.db")
+ query = f"SELECT * FROM users WHERE name = '{username}'"
+ return conn.execute(query).fetchone()
+
def add(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
7 changes: 6 additions & 1 deletion tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Tests for app.py - you'll add more!"""

from app import add, is_even, reverse_string
from app import add, is_even, reverse_string, multiply


class TestMath:
Expand All @@ -12,6 +12,11 @@ def test_add_positive(self):
def test_add_negative(self):
assert add(-1, -1) == -2

def test_multiply(self):
assert multiply(2, 3) == 6




class TestStrings:
"""Tests for string functions."""
Expand Down