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 -q

- 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*`
});
1 change: 1 addition & 0 deletions ai-cicd-github
Submodule ai-cicd-github added at f54e82
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):
"""Mutltiply two numbers"""
return a * b
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Production dependencies (none for this simple app)
google-genai>=1.0.0

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

# Configure your Gemini API key
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))

def review_code(diff_text: str) -> str:
"""Send a code diff to Gemini and return its review."""
prompt = f"""You are a code reviewer. Review the following code diff and check for:
- Bugs or errors
- Security vulnerabilities (e.g. SQL injection)
- Style and readability issues

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__":
# Check if a filename was passed as a command-line argument
if len(sys.argv) > 1:
diff_file = sys.argv[1]
with open(diff_file, "r") as f:
diff_content = f.read()
else:
# If no file was provided, read from stdin (e.g. piped input)
diff_content = sys.stdin.read()

review = review_code(diff_content)
print(review)
90 changes: 90 additions & 0 deletions scripts/generate_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import ast
from google import genai
import sys
import os


def extract_functions(file_path):
#Read the source code
with open(file_path, "r") as f:
source = f.read()

#parse it into a tree
tree = ast.parse(source)

#Walk through the tree to find node
function = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
func_info = {
"name": node.name,
"arguments": [ag.arg for arg in node args.args],
"docstring": ast.get_docstring(node),
"source": ast.get_source_segment(source, node),
}
functions.append(func_info)
return function


def generate_tests_for_function(func_info):
client = genai.Client(api_key=os.environ.get('GEMINI_API_KEY'))
prompt = f"""
You are an expert Python developer. Generate 3 - 5 meaningful pytest test cases for the following function.

Function name: {func_info['name']}
Arguments: {func_info['arguments']}
Docstring: {func_info['docstring']}
Source: {func_info['source']}

Rules:
- Do not write placeholder tests like assert True or assert False
- Each tests must actually call the function with real arguments.
- Test edge cases like empty input, negative numbers, or none where relevant
- Each testfunction must have a descriptive name explaining what it tests
- Only return the code, no explanations
"""

response = client.models.generate_content(
model="gemini-2.0-flash",
contents=prompt
)

return response.txt



def main():
file_paths = sys.argv[1:]

all_tests = []

for file_path in file_paths:
if not file_path.endswith(".py"):
print(f"Skipping {file_path} — not a Python file")
continue

if "test" in file_path:
print(f"Skipping {file_path} — looks like a test file")
continue

functions = extract_functions(file_path)

for func in functions:
if func["name"].startswith("_"):
print(f"Skipping private function: {func['name']}")
continue

print(f"Generating tests for {func['name']}...")
tests = generate_tests_for_function(func)
all_tests.append(tests)

os.mkdir("tests", exist_ok=True)

with open("tests/test_generated.py", "w") as f:
f.write("\n\n".join(all_tests))

print("Done! Tests written to tests/test_generated.py")


if __name__ == '__main__':
main()
16 changes: 16 additions & 0 deletions scripts/scripts/sample_diff.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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
16 changes: 15 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,17 @@ def test_reverse(self):
def test_is_even(self):
assert is_even(4) is True
assert is_even(3) is False


class TestMultiply:
"""Test for multiply function"""

def test_multiply_positive_numbers(self):
assert multiply(4, 4) == 16

def test_multiply_by_zero(self):
assert multiply(4, 0) == 0

def test_multiply_negatives_numbers(self):
assert multiply(-5, -10) == 50