diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 474ef35..08ed39d 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -3,6 +3,15 @@ name: AI Code Reviewer on: pull_request: types: [opened, synchronize, reopened] + paths: + - '**.py' + - 'pants.toml' + - '3rdparty/**' + - 'scripts/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true permissions: pull-requests: write diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..db5ed11 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +# Pre-commit configuration for Pants Build Monorepo +# See https://pre-commit.com for more information + +repos: + - repo: local + hooks: + - id: pants-fmt + name: Pants Format + entry: ./pants fmt + language: system + types: [python] + + - id: pants-lint + name: Pants Lint + entry: ./pants lint + language: system + types: [python] diff --git a/3rdparty/requirements.txt b/3rdparty/requirements.txt index 9e78b65..2dd8216 100644 --- a/3rdparty/requirements.txt +++ b/3rdparty/requirements.txt @@ -1,2 +1,3 @@ # Third-party Python dependencies for the data engineering projects. pyspark>=3.3.0,<4.0.0 +pre-commit>=3.0.0 diff --git a/3rdparty/user_reqs.lock b/3rdparty/user_reqs.lock index 4096fa2..b6c7bda 100644 --- a/3rdparty/user_reqs.lock +++ b/3rdparty/user_reqs.lock @@ -6,9 +6,10 @@ // { // "version": 3, // "valid_for_interpreter_constraints": [ -// "CPython==3.10.*" +// "CPython==3.10.9" // ], // "generated_with_requirements": [ +// "pre-commit>=3.0.0", // "pyspark<4.0.0,>=3.3.0" // ], // "manylinux": "manylinux2014", @@ -27,6 +28,140 @@ "locked_resolves": [ { "locked_requirements": [ + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", + "url": "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", + "url": "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz" + } + ], + "project_name": "cfgv", + "requires_dists": [], + "requires_python": ">=3.10", + "version": "3.5.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", + "url": "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", + "url": "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz" + } + ], + "project_name": "distlib", + "requires_dists": [], + "requires_python": null, + "version": "0.4.3" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", + "url": "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", + "url": "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz" + } + ], + "project_name": "filelock", + "requires_dists": [], + "requires_python": ">=3.10", + "version": "3.29.4" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", + "url": "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", + "url": "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz" + } + ], + "project_name": "identify", + "requires_dists": [ + "ukkonen; extra == \"license\"" + ], + "requires_python": ">=3.10", + "version": "2.6.19" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", + "url": "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", + "url": "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz" + } + ], + "project_name": "nodeenv", + "requires_dists": [], + "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7", + "version": "1.10.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", + "url": "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", + "url": "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz" + } + ], + "project_name": "platformdirs", + "requires_dists": [], + "requires_python": ">=3.10", + "version": "4.10.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", + "url": "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", + "url": "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz" + } + ], + "project_name": "pre-commit", + "requires_dists": [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0" + ], + "requires_python": ">=3.10", + "version": "4.6.0" + }, { "artifacts": [ { @@ -73,6 +208,129 @@ ], "requires_python": ">=3.8", "version": "3.5.8" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", + "url": "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", + "url": "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz" + } + ], + "project_name": "python-discovery", + "requires_dists": [ + "covdefaults>=2.3; extra == \"testing\"", + "coverage>=7.5.4; extra == \"testing\"", + "filelock>=3.15.4", + "furo>=2025.12.19; extra == \"docs\"", + "platformdirs<5,>=4.3.6", + "pytest-mock>=3.14; extra == \"testing\"", + "pytest>=8.3.5; extra == \"testing\"", + "setuptools>=75.1; extra == \"testing\"", + "sphinx-autodoc-typehints>=3.6.3; extra == \"docs\"", + "sphinx>=9.1; extra == \"docs\"", + "sphinxcontrib-mermaid>=2; extra == \"docs\"", + "sphinxcontrib-towncrier>=0.4; extra == \"docs\"", + "towncrier>=25.8; extra == \"docs\"" + ], + "requires_python": ">=3.8", + "version": "1.4.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", + "url": "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", + "url": "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", + "url": "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", + "url": "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", + "url": "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", + "url": "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", + "url": "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", + "url": "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl" + } + ], + "project_name": "pyyaml", + "requires_dists": [], + "requires_python": ">=3.8", + "version": "6.0.3" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", + "url": "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "url": "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz" + } + ], + "project_name": "typing-extensions", + "requires_dists": [], + "requires_python": ">=3.9", + "version": "4.15.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", + "url": "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", + "url": "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz" + } + ], + "project_name": "virtualenv", + "requires_dists": [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.24.2; python_version >= \"3.10\"", + "filelock<=3.19.1,>=3.16.1; python_version < \"3.10\"", + "platformdirs<5,>=3.9.1", + "python-discovery>=1.4.2", + "typing-extensions>=4.13.2; python_version < \"3.11\"" + ], + "requires_python": ">=3.9", + "version": "21.5.1" } ], "platform_tag": null @@ -85,10 +343,11 @@ "pip_version": "24.0", "prefer_older_binary": false, "requirements": [ + "pre-commit>=3.0.0", "pyspark<4.0.0,>=3.3.0" ], "requires_python": [ - "==3.10.*" + "==3.10.9" ], "resolver_version": "pip-2020-resolver", "style": "universal", diff --git a/README.md b/README.md index 3f4001e..7ef3de4 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ I manage this monorepo using **Pantsbuild (Pants)**, targeting **Python 3.10.x** * **Target Language:** Python 3.10.9 (configured via `.python-version` and Pyenv) * **Format & Lint:** [Ruff](https://github.com/astral-sh/ruff) (unified linter and formatter) * **Dependency Management:** Single shared lockfile (`3rdparty/user_reqs.lock`) +* **Git Hooks:** [pre-commit](https://pre-commit.com/) (runs Pants formatting and linting locally) +* **CI Reviewer:** Gemini API code reviewer via GitHub Actions and `google-generativeai` --- @@ -19,7 +21,11 @@ I manage this monorepo using **Pantsbuild (Pants)**, targeting **Python 3.10.x** ```text data-engineering/ +├── .github/ +│ └── workflows/ +│ └── ai-review.yml # CI pipeline for automated AI code reviews ├── .gitignore # Python, Pants, and OS ignore rules +├── .pre-commit-config.yaml # Configures the local pre-commit hook ├── .python-version # Sets project-local Python to 3.10.9 ├── pants # Pants launcher binary (scie-pants) ├── pants.toml # Main configuration for Pants and tool backends @@ -28,7 +34,10 @@ data-engineering/ │ ├── BUILD # Configures global dependency targets │ ├── requirements.txt # Lists project requirements (pandas, PySpark, etc.) │ └── user_reqs.lock # Pants generated dependency lockfile -└── projects/ # Directory containing all sub-projects +├── projects/ # Directory containing all sub-projects +└── scripts/ + ├── BUILD # Configures scripts targets for Pants + └── ai_pr_reviewer.py # Python script that runs Gemini AI code reviews ``` --- @@ -51,6 +60,12 @@ Pants manages its own virtual environments, but I like to export one symlinked t source .venv/bin/activate ``` +### 3. Set Up Pre-commit Hooks +The project uses `pre-commit` to automatically run Pants formatters and linters on staged files. Run the following command inside the virtual environment to install the hooks: +```bash +pre-commit install +``` + --- ## ⚡ CLI Cheatsheet diff --git a/agents.md b/agents.md index f9bd56a..d8ab861 100644 --- a/agents.md +++ b/agents.md @@ -18,12 +18,17 @@ This repository is a **Python Data Engineering Monorepo** managed by the **Pants * **Data Processing:** PySpark (`3.5.8`) ### Project Structure +* `.github/workflows/ai-review.yml`: Automated Gemini AI PR reviewer workflow on GitHub Actions. +* `.pre-commit-config.yaml`: Pre-commit hooks configuration to run `./pants fmt` and `./pants lint` locally. * `3rdparty/`: Contains requirements (`requirements.txt`) and Pants lockfiles (`user_reqs.lock`). * `projects/essentials/`: First sub-project (derived generic implementation from *Hello Modern Data Pipelines*, Chapter 2). * `word_count.py`: Local text processing WordCount Spark script. * `employee_partition_by_hire_date.py`: Local partitioning Spark script. * `input_data/`: Small CSV/txt sample inputs. * `output_data/`: Automatically generated Spark output targets (ignored by git). +* `scripts/`: Python utility scripts. + * `ai_pr_reviewer.py`: The AI code reviewer script powered by the Gemini API. + * `BUILD`: Pants build definition for the scripts directory. * `.venv`: A root-level symlink pointing to the current active Pants-generated virtual environment. --- diff --git a/scripts/BUILD b/scripts/BUILD new file mode 100644 index 0000000..db46e8d --- /dev/null +++ b/scripts/BUILD @@ -0,0 +1 @@ +python_sources() diff --git a/scripts/ai_pr_reviewer.py b/scripts/ai_pr_reviewer.py index 1f216c0..0bdf926 100644 --- a/scripts/ai_pr_reviewer.py +++ b/scripts/ai_pr_reviewer.py @@ -9,6 +9,7 @@ # Max character limit for diff payload to stay within token limits MAX_DIFF_CHARACTERS = 150000 + def get_ignore_spec() -> pathspec.PathSpec: """Loads .gitignore patterns and appends custom file exclusion wildcards.""" ignore_patterns = [] @@ -23,17 +24,10 @@ def get_ignore_spec() -> pathspec.PathSpec: ignore_patterns.extend(lines) 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_patterns.extend(["*.lock", "*.png", "*.jpg", "*.jpeg", "*.zip", "*.pdf"]) + return pathspec.PathSpec.from_lines("gitwildmatch", ignore_patterns) @@ -41,40 +35,48 @@ def build_diff_content(pr: PullRequest, ignore_spec: pathspec.PathSpec) -> str: """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.") + print( + "Max character limit reached during diff generation. Stopping file retrieval.", + file=sys.stderr, + ) 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: if len(file.patch) > 30000: 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)") + print( + f"Skipping patch for {file.filename} (exceeds 30,000 character limit)", + file=sys.stderr, + ) else: patch_str = f"{file_header}{file.patch}\n" else: - patch_str = f"{file_header}[File modified, but no patch details available]\n" - + patch_str = ( + f"{file_header}[File modified, but no patch details available]\n" + ) + diff_content.append(patch_str) current_size += len(patch_str) - + 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) - + 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" @@ -87,12 +89,9 @@ def generate_review(gemini_api_key: str, model_name: str, diff: str) -> str: "- 💡 Key Feedback & Recommendations (with before/after code blocks)\n" "- ✅ Verdict (Approve, Comment, Request Changes)" ) - - model = genai.GenerativeModel( - model_name, - system_instruction=system_instruction - ) - + + 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 @@ -104,8 +103,11 @@ def post_review(pr: PullRequest, review_body: str) -> None: 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.") + print( + "This is expected for Pull Requests from external forks where GITHUB_TOKEN has read-only access.", + file=sys.stderr, + ) + print("Exiting gracefully with code 0.", file=sys.stderr) sys.exit(0) @@ -121,17 +123,26 @@ def main(): # 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).") + print( + "Warning: GEMINI_API_KEY is missing. Skipping AI Review (expected for external forks).", + file=sys.stderr, + ) 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) + print( + "Error: Missing required environment variables (GITHUB_TOKEN, PR_NUMBER, or REPO_NAME).", + file=sys.stderr, + ) sys.exit(1) 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) + print( + f"Error: PR_NUMBER '{pr_number_str}' is not a valid integer.", + file=sys.stderr, + ) sys.exit(1) # Initialize client and fetch PR @@ -153,16 +164,23 @@ def main(): 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) + print( + f"Warning: Failed to generate review via Gemini API: {e}", file=sys.stderr + ) + print( + "Exiting gracefully with code 0 to avoid failing the CI build.", + file=sys.stderr, + ) + sys.exit(0) # Post review print("Posting review back to GitHub...") 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(f"Warning: Failed to post review comment to GitHub: {e}", file=sys.stderr) + print("Exiting gracefully with code 0.", file=sys.stderr) + sys.exit(0) print("Successfully posted PR review!")