From f1a481aa057d817363f0eb8f125939c4020bae94 Mon Sep 17 00:00:00 2001 From: Rajkumar Jain Date: Thu, 29 Jan 2026 11:23:56 +0100 Subject: [PATCH 1/9] Add multiply function, tests and CI workflow --- .github/workflows/ci.yaml | 26 ++++++++++++++++++++++++++ app.py | 4 ++++ tests/test_app.py | 7 ++++++- 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..d0863fdd --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,26 @@ +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 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/tests/test_app.py b/tests/test_app.py index 79c3e093..b2d8597f 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 + assert multiply(-1, -1) == 1 + assert multiply(-1, 1) == -1 + class TestStrings: """Tests for string functions.""" From 048da4e304187e8909f7b9232994e7960c9a6403 Mon Sep 17 00:00:00 2001 From: Rajkumar Jain Date: Thu, 29 Jan 2026 11:29:34 +0100 Subject: [PATCH 2/9] test case update --- tests/test_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index b2d8597f..aa0f2d3e 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -13,7 +13,7 @@ def test_add_negative(self): assert add(-1, -1) == -2 def test_multiply(self): - assert multiply(2, 3) == 6 + assert multiply(9, 9) == 18 assert multiply(-1, -1) == 1 assert multiply(-1, 1) == -1 From 65d7e422d5eaf6b5087bbbc6bb57b5bac146a713 Mon Sep 17 00:00:00 2001 From: Rajkumar Jain Date: Thu, 29 Jan 2026 11:32:29 +0100 Subject: [PATCH 3/9] fix the break --- tests/test_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index aa0f2d3e..4073b8d9 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -13,7 +13,7 @@ def test_add_negative(self): assert add(-1, -1) == -2 def test_multiply(self): - assert multiply(9, 9) == 18 + assert multiply(9, 9) == assert multiply(-1, -1) == 1 assert multiply(-1, 1) == -1 From 69eee6bcd1d94315e07c132833b24ca307b7317b Mon Sep 17 00:00:00 2001 From: Rajkumar Jain Date: Thu, 29 Jan 2026 11:33:59 +0100 Subject: [PATCH 4/9] fix the test --- tests/test_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index 4073b8d9..651e182f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -13,7 +13,7 @@ def test_add_negative(self): assert add(-1, -1) == -2 def test_multiply(self): - assert multiply(9, 9) == + assert multiply(9, 9) == 81 assert multiply(-1, -1) == 1 assert multiply(-1, 1) == -1 From 97d8c6c68f349267aeb521f4af1a8a2c28123c76 Mon Sep 17 00:00:00 2001 From: Rajkumar Jain Date: Thu, 29 Jan 2026 11:41:05 +0100 Subject: [PATCH 5/9] build and artifact added to action --- .github/workflows/ci.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d0863fdd..15fc3e56 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,3 +24,12 @@ jobs: - 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/ \ No newline at end of file From e9b0c6c3389373256de69552f7593a66439fde3b Mon Sep 17 00:00:00 2001 From: Rajkumar Jain Date: Thu, 29 Jan 2026 15:38:51 +0100 Subject: [PATCH 6/9] Add AI code review script and PR workflow --- .github/workflows/pr-review.yml | 56 +++++++++++++++++++++++++++++++++ requirements.txt | 4 +-- scripts/ai_review.py | 54 +++++++++++++++++++++++++++++++ scripts/sample_diff.txt | 17 ++++++++++ 4 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pr-review.yml create mode 100644 scripts/ai_review.py create mode 100644 scripts/sample_diff.txt diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml new file mode 100644 index 00000000..1bbebca3 --- /dev/null +++ b/.github/workflows/pr-review.yml @@ -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<> $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*` + }); diff --git a/requirements.txt b/requirements.txt index 5f456517..3dafa171 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/scripts/ai_review.py b/scripts/ai_review.py new file mode 100644 index 00000000..47806445 --- /dev/null +++ b/scripts/ai_review.py @@ -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) \ No newline at end of file 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 From f6e2ae03ffa24764d550bb4055027fead06f0509 Mon Sep 17 00:00:00 2001 From: Rajkumar Jain Date: Thu, 29 Jan 2026 15:43:09 +0100 Subject: [PATCH 7/9] add command execution feature --- dangerous.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 dangerous.py diff --git a/dangerous.py b/dangerous.py new file mode 100644 index 00000000..5d477bcb --- /dev/null +++ b/dangerous.py @@ -0,0 +1,7 @@ +import subprocess + +def run_command(user_input): + """Run a shell command from user input.""" + subprocess.call(user_input, shell=True) + +API_KEY = "sk-live-abc123def456" From b5eb8394b8597db717ae8986aa17262073ded0d9 Mon Sep 17 00:00:00 2001 From: Rajkumar Jain Date: Thu, 29 Jan 2026 16:56:54 +0100 Subject: [PATCH 8/9] fix security issue --- dangerous.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dangerous.py b/dangerous.py index 5d477bcb..01b4f6bd 100644 --- a/dangerous.py +++ b/dangerous.py @@ -1,7 +1,12 @@ import subprocess +import shlex +import os def run_command(user_input): """Run a shell command from user input.""" - subprocess.call(user_input, shell=True) + args = shlex.split( user_input) + subprocess.call(args,shell=False) + +API_KEY = os.environ.get("API_KEY") + -API_KEY = "sk-live-abc123def456" From 1b2c7b25d01826518e71f66def40f1eff3064d11 Mon Sep 17 00:00:00 2001 From: Rajkumar Jain Date: Thu, 29 Jan 2026 17:05:35 +0100 Subject: [PATCH 9/9] code update --- dangerous.py | 52 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/dangerous.py b/dangerous.py index 01b4f6bd..6dbe71c9 100644 --- a/dangerous.py +++ b/dangerous.py @@ -1,12 +1,44 @@ +# Example of a safer approach (still needs thorough argument validation): import subprocess import shlex -import os - -def run_command(user_input): - """Run a shell command from user input.""" - args = shlex.split( user_input) - subprocess.call(args,shell=False) - -API_KEY = os.environ.get("API_KEY") - - +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.") \ No newline at end of file