From 63b4be5255ff330cfc81618e055bc80403a46e95 Mon Sep 17 00:00:00 2001 From: "jameskinyua590@gmail.com" <20414083+JayKay24@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:36:17 +0300 Subject: [PATCH 1/7] docs: add Pull Request and bookkeeping workflow guidelines to agents.md Signed-off-by: jameskinyua590@gmail.com <20414083+JayKay24@users.noreply.github.com> --- agents.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/agents.md b/agents.md index 023b774..f9bd56a 100644 --- a/agents.md +++ b/agents.md @@ -52,3 +52,18 @@ This project is bootstrapped to work seamlessly with VS Code / Antigravity IDE w 2. **STRICTLY LOCAL SCOPE:** Do not write or modify any VS Code/IDE configurations outside the `data-engineering` project directory. 3. **PANTS COMPLIANCE:** Always follow the target-based workflow for Pants commands. 4. **SPARK COMPATIBILITY:** Run PySpark tasks using Java 17 via the `JAVA_HOME` configuration found in `.env`. + +--- + +## 4. Pull Request & Bookkeeping Workflow + +When implementing new features or bug fixes, follow this workflow to coordinate PR creation: + +1. **Local Implementation & Verification:** Implement changes locally, verify they run/test successfully, and document them in `README.md` and `agents.md`. +2. **User Review & Local Commit:** Present the changes to the user. The user will review the code locally and run `git commit` manually. +3. **PR Creation:** Once the user commits the changes, they will instruct you to create the Pull Request (or you can offer to do so). +4. **PR Formatting:** Use the `github` MCP server to create the PR. The PR must have a detailed description containing: + * **Summary:** A clear explanation of *why* the changes were made. + * **Key Changes:** A bulleted list of modified modules/files and what was updated. + * **Verification:** Documentation of successful test/run commands executed during verification. + From 90a2a0ca6fe61da33e20663929fe329f8daad62d Mon Sep 17 00:00:00 2001 From: "jameskinyua590@gmail.com" <20414083+JayKay24@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:15:13 +0300 Subject: [PATCH 2/7] feat: add automated GitHub Action for AI-powered pull request code reviews using Gemini Signed-off-by: jameskinyua590@gmail.com <20414083+JayKay24@users.noreply.github.com> --- .github/workflows/ai-review.yml | 37 ++++++++++ scripts/ai_pr_reviewer.py | 117 ++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 .github/workflows/ai-review.yml create mode 100644 scripts/ai_pr_reviewer.py diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml new file mode 100644 index 0000000..ccfe3b5 --- /dev/null +++ b/.github/workflows/ai-review.yml @@ -0,0 +1,37 @@ +name: AI Code Reviewer + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + pull-requests: write + contents: read + +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.10' + cache: 'pip' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install google-generativeai PyGithub pathspec + + - name: Run AI Reviewer + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO_NAME: ${{ github.repository }} + run: python scripts/ai_pr_reviewer.py diff --git a/scripts/ai_pr_reviewer.py b/scripts/ai_pr_reviewer.py new file mode 100644 index 0000000..597fe72 --- /dev/null +++ b/scripts/ai_pr_reviewer.py @@ -0,0 +1,117 @@ +import os +import sys +from github import Github +import google.generativeai as genai +import pathspec + +def main(): + # 1. Load Environment Variables + gemini_api_key = os.getenv("GEMINI_API_KEY") + github_token = os.getenv("GITHUB_TOKEN") + pr_number_str = os.getenv("PR_NUMBER") + repo_name = os.getenv("REPO_NAME") + + if not all([gemini_api_key, github_token, pr_number_str, repo_name]): + print("Error: Missing required environment variables.", file=sys.stderr) + sys.exit(1) + + pr_number = int(pr_number_str) + + # 2. Initialize GitHub Client and Fetch PR Diffs + print(f"Fetching PR #{pr_number} from GitHub repository: {repo_name}...") + g = Github(github_token) + repo = g.get_repo(repo_name) + pr = repo.get_pull(pr_number) + + # Load .gitignore patterns and append custom patterns (lockfiles, binaries) + ignore_patterns = [] + if os.path.exists(".gitignore"): + try: + with open(".gitignore", "r") as f: + ignore_patterns.extend(f.readlines()) + except Exception as e: + print(f"Warning: Failed to parse .gitignore: {e}", file=sys.stderr) + + # Custom wildcards for lockfiles and binary assets to skip + ignore_patterns.extend([ + "*.lock", + "*.png", + "*.jpg", + "*.jpeg", + "*.zip", + "*.pdf" + ]) + + ignore_spec = pathspec.PathSpec.from_lines("gitwildmatch", ignore_patterns) + print("Successfully compiled file ignore patterns.") + + # Get modified files and their diffs + files = pr.get_files() + diff_content = [] + + for file in files: + # Skip files matching ignore patterns + if ignore_spec.match_file(file.filename): + print(f"Skipping {file.filename} (ignored)") + continue + + file_header = f"=== File: {file.filename} ===\n" + # Use file patch/diff if available + if file.patch: + diff_content.append(f"{file_header}{file.patch}\n") + else: + diff_content.append(f"{file_header}[File modified, but no patch details available]\n") + + if not diff_content: + print("No code changes to review.") + sys.exit(0) + + full_diff = "\n".join(diff_content) + + # 3. Configure Gemini and Generate Review + print("Sending diff to Gemini API for analysis...") + genai.configure(api_key=gemini_api_key) + + # Using gemini-3.5-flash: fast, low latency, and highly capable + model = genai.GenerativeModel("gemini-3.5-flash") + + prompt = f""" +You are an expert Data Engineer and Python Code Reviewer. +Your task is to conduct a professional, constructive code review for the following Pull Request diff. + +Specific areas to analyze: +1. **PySpark & Data Engineering Best Practices**: Check for issues like unpartitioned writes, redundant/missing caching, expensive operations like `.collect()` on large datasets, and proper schema enforcement. +2. **Python Code Quality**: Verify Pythonic style, readability, naming conventions, docstrings, and comments (matching Ruff/PEP8 standards). +3. **Bugs & Edge Cases**: Look for logical bugs, incorrect relative paths, unhandled exceptions, or potential null pointer errors. + +Format your review in Markdown with the following sections: +- **🤖 AI PR Review Summary**: A brief, high-level summary of what the PR changes. +- **💡 Key Feedback & Recommendations**: Bullet points detailing specific improvements. Include code snippets for "Before" and "After" where applicable. +- **✅ Verdict**: One of the following: + - **Approve**: The code looks clean, optimized, and ready to merge. + - **Comment**: Needs minor cleanup, formatting, or documentation. + - **Request Changes**: Critical bugs or severe PySpark performance risks that should be resolved before merging. + +PR Diff: +{full_diff} +""" + + try: + response = model.generate_content(prompt) + review_markdown = response.text + except Exception as e: + print(f"Error calling Gemini API: {e}", file=sys.stderr) + sys.exit(1) + + # 4. Post the Review Comment back to the Pull Request + print("Posting review comment to Pull Request...") + try: + # Create a top-level review comment on the PR conversation + pr.create_issue_comment(review_markdown) + print("Review successfully posted!") + except Exception as e: + print(f"Error posting comment to GitHub: {e}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() From 32f1aa115174308a6020d5bcfe8882c0cd2414ca Mon Sep 17 00:00:00 2001 From: "jameskinyua590@gmail.com" <20414083+JayKay24@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:26:18 +0300 Subject: [PATCH 3/7] refactor: modularize ai_pr_reviewer logic and add size-based diff truncation Signed-off-by: jameskinyua590@gmail.com <20414083+JayKay24@users.noreply.github.com> --- scripts/ai_pr_reviewer.py | 126 ++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 54 deletions(-) diff --git a/scripts/ai_pr_reviewer.py b/scripts/ai_pr_reviewer.py index 597fe72..d2a042c 100644 --- a/scripts/ai_pr_reviewer.py +++ b/scripts/ai_pr_reviewer.py @@ -1,33 +1,20 @@ import os import sys +from typing import List from github import Github +from github.PullRequest import PullRequest import google.generativeai as genai import pathspec -def main(): - # 1. Load Environment Variables - gemini_api_key = os.getenv("GEMINI_API_KEY") - github_token = os.getenv("GITHUB_TOKEN") - pr_number_str = os.getenv("PR_NUMBER") - repo_name = os.getenv("REPO_NAME") - - if not all([gemini_api_key, github_token, pr_number_str, repo_name]): - print("Error: Missing required environment variables.", file=sys.stderr) - sys.exit(1) - - pr_number = int(pr_number_str) +# Max character limit for diff payload to stay within token limits +MAX_DIFF_CHARACTERS = 150000 - # 2. Initialize GitHub Client and Fetch PR Diffs - print(f"Fetching PR #{pr_number} from GitHub repository: {repo_name}...") - g = Github(github_token) - repo = g.get_repo(repo_name) - pr = repo.get_pull(pr_number) - - # Load .gitignore patterns and append custom patterns (lockfiles, binaries) +def get_ignore_spec() -> pathspec.PathSpec: + """Loads .gitignore patterns and appends custom file exclusion wildcards.""" ignore_patterns = [] if os.path.exists(".gitignore"): try: - with open(".gitignore", "r") as f: + with open(".gitignore", "r", encoding="utf-8") as f: ignore_patterns.extend(f.readlines()) except Exception as e: print(f"Warning: Failed to parse .gitignore: {e}", file=sys.stderr) @@ -42,39 +29,42 @@ def main(): "*.pdf" ]) - ignore_spec = pathspec.PathSpec.from_lines("gitwildmatch", ignore_patterns) - print("Successfully compiled file ignore patterns.") - - # Get modified files and their diffs - files = pr.get_files() - diff_content = [] - - for file in files: + return pathspec.PathSpec.from_lines("gitwildmatch", ignore_patterns) + + +def build_diff_content(pr: PullRequest, ignore_spec: pathspec.PathSpec) -> str: + """Retrieves and filters PR file diffs, handling size limits.""" + diff_content: List[str] = [] + for file in pr.get_files(): # Skip files matching ignore patterns if ignore_spec.match_file(file.filename): print(f"Skipping {file.filename} (ignored)") continue file_header = f"=== File: {file.filename} ===\n" - # Use file patch/diff if available if file.patch: diff_content.append(f"{file_header}{file.patch}\n") else: diff_content.append(f"{file_header}[File modified, but no patch details available]\n") - - if not diff_content: - print("No code changes to review.") - sys.exit(0) - + full_diff = "\n".join(diff_content) + + # Handle defensive size cutoff + if len(full_diff) > MAX_DIFF_CHARACTERS: + print(f"Warning: PR diff size ({len(full_diff)} chars) exceeds threshold. Truncating.") + full_diff = ( + full_diff[:MAX_DIFF_CHARACTERS] + + "\n\n... [TRUNCATED DUE TO EXTREME SIZE] ..." + ) + + return full_diff + - # 3. Configure Gemini and Generate Review - print("Sending diff to Gemini API for analysis...") +def generate_review(gemini_api_key: str, diff: str) -> str: + """Sends the PR diff to the Gemini API and returns the markdown review.""" genai.configure(api_key=gemini_api_key) - - # Using gemini-3.5-flash: fast, low latency, and highly capable model = genai.GenerativeModel("gemini-3.5-flash") - + prompt = f""" You are an expert Data Engineer and Python Code Reviewer. Your task is to conduct a professional, constructive code review for the following Pull Request diff. @@ -93,25 +83,53 @@ def main(): - **Request Changes**: Critical bugs or severe PySpark performance risks that should be resolved before merging. PR Diff: -{full_diff} +{diff} """ + response = model.generate_content(prompt) + return response.text - try: - response = model.generate_content(prompt) - review_markdown = response.text - except Exception as e: - print(f"Error calling Gemini API: {e}", file=sys.stderr) - sys.exit(1) - # 4. Post the Review Comment back to the Pull Request - print("Posting review comment to Pull Request...") - try: - # Create a top-level review comment on the PR conversation - pr.create_issue_comment(review_markdown) - print("Review successfully posted!") - except Exception as e: - print(f"Error posting comment to GitHub: {e}", file=sys.stderr) +def post_review(pr: PullRequest, review_body: str) -> None: + """Submits the review as an official Pull Request Review on GitHub.""" + pr.create_review(body=review_body, event="COMMENT") + + +def main(): + # Load Environment Variables + gemini_api_key = os.getenv("GEMINI_API_KEY") + github_token = os.getenv("GITHUB_TOKEN") + pr_number_str = os.getenv("PR_NUMBER") + repo_name = os.getenv("REPO_NAME") + + if not all([gemini_api_key, github_token, pr_number_str, repo_name]): + print("Error: Missing required environment variables.", file=sys.stderr) sys.exit(1) + pr_number = int(pr_number_str) + + # Initialize client and fetch PR + print(f"Connecting to repo {repo_name} and fetching PR #{pr_number}...") + g = Github(github_token) + repo = g.get_repo(repo_name) + pr = repo.get_pull(pr_number) + + # Compile ignore patterns and build diff + ignore_spec = get_ignore_spec() + diff = build_diff_content(pr, ignore_spec) + + if not diff.strip(): + print("No code changes to review.") + sys.exit(0) + + # Generate review + print("Generating review with Gemini...") + review_body = generate_review(gemini_api_key, diff) + + # Post review + print("Posting review back to GitHub...") + post_review(pr, review_body) + print("Successfully posted PR review!") + + if __name__ == "__main__": main() From d58886571f453eee04c2c4d836d76275c66239aa Mon Sep 17 00:00:00 2001 From: "jameskinyua590@gmail.com" <20414083+JayKay24@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:36:39 +0300 Subject: [PATCH 4/7] feat: pin dependency versions and add graceful handling for missing API keys and invalid PR numbers Signed-off-by: jameskinyua590@gmail.com <20414083+JayKay24@users.noreply.github.com> --- .github/workflows/ai-review.yml | 2 +- scripts/ai_pr_reviewer.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index ccfe3b5..0adadd4 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -26,7 +26,7 @@ jobs: - name: Install Dependencies run: | python -m pip install --upgrade pip - pip install google-generativeai PyGithub pathspec + pip install google-generativeai==0.8.3 PyGithub==2.5.0 pathspec==0.12.1 - name: Run AI Reviewer env: diff --git a/scripts/ai_pr_reviewer.py b/scripts/ai_pr_reviewer.py index d2a042c..383cf3e 100644 --- a/scripts/ai_pr_reviewer.py +++ b/scripts/ai_pr_reviewer.py @@ -101,11 +101,20 @@ def main(): pr_number_str = os.getenv("PR_NUMBER") repo_name = os.getenv("REPO_NAME") - if not all([gemini_api_key, github_token, pr_number_str, repo_name]): - print("Error: Missing required environment variables.", file=sys.stderr) + # Handle missing API Key gracefully (e.g. for PRs from external forks) + if not gemini_api_key: + print("Warning: GEMINI_API_KEY is missing. Skipping AI Review (expected for external forks).") + sys.exit(0) + + if not all([github_token, pr_number_str, repo_name]): + print("Error: Missing required environment variables (GITHUB_TOKEN, PR_NUMBER, or REPO_NAME).", file=sys.stderr) sys.exit(1) - pr_number = int(pr_number_str) + try: + pr_number = int(pr_number_str) + except ValueError: + print(f"Error: PR_NUMBER '{pr_number_str}' is not a valid integer.", file=sys.stderr) + sys.exit(1) # Initialize client and fetch PR print(f"Connecting to repo {repo_name} and fetching PR #{pr_number}...") From 0677f651f6820c1647b0d8155fcb9a594dcd2566 Mon Sep 17 00:00:00 2001 From: "jameskinyua590@gmail.com" <20414083+JayKay24@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:43:28 +0300 Subject: [PATCH 5/7] refactor: clean .gitignore patterns by stripping whitespace and ignoring comment lines Signed-off-by: jameskinyua590@gmail.com <20414083+JayKay24@users.noreply.github.com> --- scripts/ai_pr_reviewer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/ai_pr_reviewer.py b/scripts/ai_pr_reviewer.py index 383cf3e..8fda55e 100644 --- a/scripts/ai_pr_reviewer.py +++ b/scripts/ai_pr_reviewer.py @@ -15,7 +15,12 @@ def get_ignore_spec() -> pathspec.PathSpec: if os.path.exists(".gitignore"): try: with open(".gitignore", "r", encoding="utf-8") as f: - ignore_patterns.extend(f.readlines()) + lines = [ + line.strip() + for line in f + if line.strip() and not line.strip().startswith("#") + ] + ignore_patterns.extend(lines) except Exception as e: print(f"Warning: Failed to parse .gitignore: {e}", file=sys.stderr) From 1d662529bf0c1a65741d817d4d0ad9911f80b1f3 Mon Sep 17 00:00:00 2001 From: "jameskinyua590@gmail.com" <20414083+JayKay24@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:54:38 +0300 Subject: [PATCH 6/7] feat: add file patch size limiting, error handling, and configurable Gemini model support Signed-off-by: jameskinyua590@gmail.com <20414083+JayKay24@users.noreply.github.com> --- .github/workflows/ai-review.yml | 1 + scripts/ai_pr_reviewer.py | 28 ++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 0adadd4..474ef35 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -34,4 +34,5 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} REPO_NAME: ${{ github.repository }} + GEMINI_MODEL: ${{ vars.GEMINI_MODEL }} run: python scripts/ai_pr_reviewer.py diff --git a/scripts/ai_pr_reviewer.py b/scripts/ai_pr_reviewer.py index 8fda55e..a95d4fc 100644 --- a/scripts/ai_pr_reviewer.py +++ b/scripts/ai_pr_reviewer.py @@ -48,7 +48,12 @@ def build_diff_content(pr: PullRequest, ignore_spec: pathspec.PathSpec) -> str: file_header = f"=== File: {file.filename} ===\n" if file.patch: - diff_content.append(f"{file_header}{file.patch}\n") + # Skip overly large individual patches to ensure fairness across files + if len(file.patch) > 30000: + diff_content.append(f"{file_header}[File patch omitted: Exceeds single-file size limit]\n") + print(f"Skipping patch for {file.filename} (exceeds 30,000 character limit)") + else: + diff_content.append(f"{file_header}{file.patch}\n") else: diff_content.append(f"{file_header}[File modified, but no patch details available]\n") @@ -65,10 +70,10 @@ def build_diff_content(pr: PullRequest, ignore_spec: pathspec.PathSpec) -> str: return full_diff -def generate_review(gemini_api_key: str, diff: str) -> str: +def generate_review(gemini_api_key: str, model_name: str, diff: str) -> str: """Sends the PR diff to the Gemini API and returns the markdown review.""" genai.configure(api_key=gemini_api_key) - model = genai.GenerativeModel("gemini-3.5-flash") + model = genai.GenerativeModel(model_name) prompt = f""" You are an expert Data Engineer and Python Code Reviewer. @@ -105,6 +110,9 @@ def main(): github_token = os.getenv("GITHUB_TOKEN") pr_number_str = os.getenv("PR_NUMBER") repo_name = os.getenv("REPO_NAME") + gemini_model = os.getenv("GEMINI_MODEL") + if not gemini_model or not gemini_model.strip(): + gemini_model = "gemini-1.5-flash" # Handle missing API Key gracefully (e.g. for PRs from external forks) if not gemini_api_key: @@ -136,12 +144,20 @@ def main(): sys.exit(0) # Generate review - print("Generating review with Gemini...") - review_body = generate_review(gemini_api_key, diff) + print(f"Generating review with Gemini (model: {gemini_model})...") + try: + review_body = generate_review(gemini_api_key, gemini_model, diff) + except Exception as e: + print(f"Error generating review via Gemini API: {e}", file=sys.stderr) + sys.exit(1) # Post review print("Posting review back to GitHub...") - post_review(pr, review_body) + try: + post_review(pr, review_body) + except Exception as e: + print(f"Error posting review comment to GitHub: {e}", file=sys.stderr) + sys.exit(1) print("Successfully posted PR review!") From 7df347b81744e32b406c01e198232ccd9c7306a4 Mon Sep 17 00:00:00 2001 From: "jameskinyua590@gmail.com" <20414083+JayKay24@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:01:40 +0300 Subject: [PATCH 7/7] feat: implement incremental diff truncation and update review prompt to utilize system instructions with graceful error handling for external forks Signed-off-by: jameskinyua590@gmail.com <20414083+JayKay24@users.noreply.github.com> --- scripts/ai_pr_reviewer.py | 81 +++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/scripts/ai_pr_reviewer.py b/scripts/ai_pr_reviewer.py index a95d4fc..1f216c0 100644 --- a/scripts/ai_pr_reviewer.py +++ b/scripts/ai_pr_reviewer.py @@ -38,70 +38,75 @@ def get_ignore_spec() -> pathspec.PathSpec: def build_diff_content(pr: PullRequest, ignore_spec: pathspec.PathSpec) -> str: - """Retrieves and filters PR file diffs, handling size limits.""" + """Retrieves and filters PR file diffs, handling size limits and early exits.""" diff_content: List[str] = [] + current_size = 0 + for file in pr.get_files(): + # Optimization: Stop fetching additional diffs once we are past the limit + if current_size >= MAX_DIFF_CHARACTERS: + diff_content.append("\n\n... [TRUNCATED: MAX CHARACTER LIMIT REACHED] ...") + print("Max character limit reached during diff generation. Stopping file retrieval.") + break + # Skip files matching ignore patterns if ignore_spec.match_file(file.filename): print(f"Skipping {file.filename} (ignored)") continue file_header = f"=== File: {file.filename} ===\n" + patch_str = "" if file.patch: - # Skip overly large individual patches to ensure fairness across files if len(file.patch) > 30000: - diff_content.append(f"{file_header}[File patch omitted: Exceeds single-file size limit]\n") + patch_str = f"{file_header}[File patch omitted: Exceeds single-file size limit]\n" print(f"Skipping patch for {file.filename} (exceeds 30,000 character limit)") else: - diff_content.append(f"{file_header}{file.patch}\n") + patch_str = f"{file_header}{file.patch}\n" else: - diff_content.append(f"{file_header}[File modified, but no patch details available]\n") + patch_str = f"{file_header}[File modified, but no patch details available]\n" - full_diff = "\n".join(diff_content) - - # Handle defensive size cutoff - if len(full_diff) > MAX_DIFF_CHARACTERS: - print(f"Warning: PR diff size ({len(full_diff)} chars) exceeds threshold. Truncating.") - full_diff = ( - full_diff[:MAX_DIFF_CHARACTERS] - + "\n\n... [TRUNCATED DUE TO EXTREME SIZE] ..." - ) + diff_content.append(patch_str) + current_size += len(patch_str) - return full_diff + return "\n".join(diff_content) def generate_review(gemini_api_key: str, model_name: str, diff: str) -> str: """Sends the PR diff to the Gemini API and returns the markdown review.""" genai.configure(api_key=gemini_api_key) - model = genai.GenerativeModel(model_name) - prompt = f""" -You are an expert Data Engineer and Python Code Reviewer. -Your task is to conduct a professional, constructive code review for the following Pull Request diff. - -Specific areas to analyze: -1. **PySpark & Data Engineering Best Practices**: Check for issues like unpartitioned writes, redundant/missing caching, expensive operations like `.collect()` on large datasets, and proper schema enforcement. -2. **Python Code Quality**: Verify Pythonic style, readability, naming conventions, docstrings, and comments (matching Ruff/PEP8 standards). -3. **Bugs & Edge Cases**: Look for logical bugs, incorrect relative paths, unhandled exceptions, or potential null pointer errors. - -Format your review in Markdown with the following sections: -- **🤖 AI PR Review Summary**: A brief, high-level summary of what the PR changes. -- **💡 Key Feedback & Recommendations**: Bullet points detailing specific improvements. Include code snippets for "Before" and "After" where applicable. -- **✅ Verdict**: One of the following: - - **Approve**: The code looks clean, optimized, and ready to merge. - - **Comment**: Needs minor cleanup, formatting, or documentation. - - **Request Changes**: Critical bugs or severe PySpark performance risks that should be resolved before merging. - -PR Diff: -{diff} -""" + system_instruction = ( + "You are an expert Data Engineer and Python Code Reviewer.\n" + "Your task is to conduct a professional, constructive code review.\n\n" + "Specific areas to analyze:\n" + "1. PySpark & Data Engineering Best Practices (unpartitioned writes, redundant caching, .collect() issues).\n" + "2. Python Code Quality (Ruff/PEP8 standards, naming, docstrings).\n" + "3. Bugs & Edge Cases (logical bugs, unhandled exceptions).\n\n" + "Format your review in Markdown with the following sections:\n" + "- 🤖 AI PR Review Summary\n" + "- 💡 Key Feedback & Recommendations (with before/after code blocks)\n" + "- ✅ Verdict (Approve, Comment, Request Changes)" + ) + + model = genai.GenerativeModel( + model_name, + system_instruction=system_instruction + ) + + prompt = f"Please review the following PR Diff:\n\n{diff}" response = model.generate_content(prompt) return response.text def post_review(pr: PullRequest, review_body: str) -> None: - """Submits the review as an official Pull Request Review on GitHub.""" - pr.create_review(body=review_body, event="COMMENT") + """Submits the review as an official Pull Request Review on GitHub. Handles fork permissions gracefully.""" + try: + pr.create_review(body=review_body, event="COMMENT") + except Exception as e: + print(f"Warning: Failed to post PR review comment: {e}", file=sys.stderr) + print("This is expected for Pull Requests from external forks where GITHUB_TOKEN has read-only access.") + print("Exiting gracefully with code 0.") + sys.exit(0) def main():