diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d43c223f --- /dev/null +++ b/.github/workflows/ci.yml @@ -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/ + diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml new file mode 100644 index 00000000..d8838ab8 --- /dev/null +++ b/.github/workflows/pr-review.yml @@ -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<> $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}`); diff --git a/app.py b/app.py index 8f2f7ae1..6fffa848 100644 --- a/app.py +++ b/app.py @@ -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 diff --git a/dangerous.py b/dangerous.py new file mode 100644 index 00000000..bac1cd37 --- /dev/null +++ b/dangerous.py @@ -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" + diff --git a/dangerous2.py b/dangerous2.py new file mode 100644 index 00000000..5853ae91 --- /dev/null +++ b/dangerous2.py @@ -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" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5f456517..80eb1728 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 + + diff --git a/scripts/ai_review.py b/scripts/ai_review.py new file mode 100644 index 00000000..9e2c227e --- /dev/null +++ b/scripts/ai_review.py @@ -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: + Where 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) + diff --git a/scripts/sample_diff.txt b/scripts/sample_diff.txt new file mode 100644 index 00000000..d4b5984b --- /dev/null +++ b/scripts/sample_diff.txt @@ -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 diff --git a/tests/test_app.py b/tests/test_app.py index 79c3e093..034e834a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -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: @@ -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."""