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.yaml
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/
56 changes: 56 additions & 0 deletions .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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

- 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*`
});
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
44 changes: 44 additions & 0 deletions dangerous.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Example of a safer approach (still needs thorough argument validation):
import subprocess
import shlex
import re

# Define allowed commands and their expected arguments/patterns
ALLOWED_COMMANDS = {
"ls": {
"args_whitelist": ["-l", "-a", "-h", "--color=auto"],
"paths_regex": r"^[a-zA-Z0-9_\-./]+$" # Basic, needs refinement for security
},
"cat": {
"args_whitelist": [],
"paths_regex": r"^[a-zA-Z0-9_\-./]+\.txt$" # Only allow specific file types
}
}

def run_safe_command(command_name, user_args_string):
if command_name not in ALLOWED_COMMANDS:
raise ValueError(f"Command '{command_name}' is not allowed.")

allowed_config = ALLOWED_COMMANDS[command_name]
args = shlex.split(user_args_string)
validated_args = []

for arg in args:
if arg.startswith('-'):
if arg not in allowed_config.get("args_whitelist", []):
raise ValueError(f"Argument '{arg}' not allowed for command '{command_name}'.")
elif "paths_regex" in allowed_config:
if not re.fullmatch(allowed_config["paths_regex"], arg):
raise ValueError(f"Path argument '{arg}' not valid for command '{command_name}'.")
else: # No path validation defined, potentially unsafe
raise ValueError(f"Argument '{arg}' type not handled for command '{command_name}'.")
validated_args.append(arg)

full_command = [command_name] + validated_args
try:
result = subprocess.run(full_command, capture_output=True, text=True, check=True)
return result.stdout, result.stderr
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Command failed: {e.stderr}") from e
except FileNotFoundError:
raise RuntimeError(f"Command '{command_name}' not found.")
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# 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
54 changes: 54 additions & 0 deletions scripts/ai_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from google import genai
import sys
import os

# Initialize the client with API key from environment variable
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
raise ValueError(
"GEMINI_API_KEY environment variable not set. "
"Please set it with: $env:GEMINI_API_KEY='your-api-key' (PowerShell) "
"or export GEMINI_API_KEY='your-api-key' (Unix/Mac)"
)

client = genai.Client(api_key=api_key)

def review_code(diff_text):
"""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") as f:
diff_content = f.read()
else:
diff_content = sys.stdin.read()

review = review_code(diff_content)
print(review)
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(9, 9) == 81
assert multiply(-1, -1) == 1
assert multiply(-1, 1) == -1


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