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
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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/
59 changes: 59 additions & 0 deletions .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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 fetch origin ${{ github.event.pull_request.base.ref }} --depth=1
git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} > pr_diff.txt
echo "Diff file size:" && wc -c pr_diff.txt


- name: Run AI review
id: ai-review
env:
GOOGLE_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

- 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*`
});
5 changes: 5 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ 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
11 changes: 11 additions & 0 deletions dangerous.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import subprocess
import shlex
import os

def run_command(user_input):
"""Run a shell command safely."""
args = shlex.split(user_input)
subprocess.call(args, shell=False)

API_KEY = os.environ.get("API_KEY")

2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Production dependencies (none for this simple app)

google-genai>=1.0.0
# Development/testing dependencies
pytest>=7.0.0
build>=1.0.0
45 changes: 45 additions & 0 deletions scripts/ai_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from google import genai
import os
import sys

# Gemini SDK looks for GOOGLE_API_KEY by default, but we support both.
api_key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")
client = genai.Client(api_key=api_key)

def review_code(diff_text: str) -> str:
"""Send a code diff to Gemini for review."""
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.

Code diff to review:

{diff_text}

Provide your review in a clear, structured format."""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt
)
return response.text

if __name__ == "__main__":
if len(sys.argv) > 1:
diff_file = sys.argv[1]
with open(diff_file, "r", encoding="utf-8") as f:
diff_content = f.read()
else:
diff_content = sys.stdin.read()

print(review_code(diff_content))
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
18 changes: 17 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 @@ -22,3 +22,19 @@ def test_reverse(self):
def test_is_even(self):
assert is_even(4) is True
assert is_even(3) is False


class TestMultiply:
"""Tests for the multiply function."""

def test_multiply_positive_numbers(self):
"""Test multiplying two positive numbers."""
assert multiply(9, 9) == 81

def test_multiply_by_zero(self):
"""Test multiplying by zero."""
assert multiply(5, 0) == 0

def test_multiply_negative_numbers(self):
"""Test multiplying negative numbers."""
assert multiply(-2, 3) == -6