From 10432a3e1c6fd8a3e36500b3da0d8414376e3795 Mon Sep 17 00:00:00 2001 From: John Burton Date: Sun, 3 May 2026 09:29:37 +0100 Subject: [PATCH] Add review script --- .../ai_code_review.cpython-312.pyc | Bin 0 -> 4950 bytes scripts/ai_code_review.py | 146 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 scripts/__pycache__/ai_code_review.cpython-312.pyc create mode 100644 scripts/ai_code_review.py diff --git a/scripts/__pycache__/ai_code_review.cpython-312.pyc b/scripts/__pycache__/ai_code_review.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ee98f98422b1c03e78fb5f1019ff1840458b4c2 GIT binary patch literal 4950 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 pm!JVfj@og0j@!j^B{tKE_p=AI7 literal 0 HcmV?d00001 diff --git a/scripts/ai_code_review.py b/scripts/ai_code_review.py new file mode 100644 index 0000000..9ac07f4 --- /dev/null +++ b/scripts/ai_code_review.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import sys +from typing import Optional + + +MAX_DIFF_CHARS = 12000 + + +def run_git_command(*args: str) -> str: + result = subprocess.run( + ["git", *args], + check=True, + capture_output=True, + text=True, + ) + return result.stdout.strip() + + +def try_git_command(*args: str) -> Optional[str]: + try: + return run_git_command(*args) + except subprocess.CalledProcessError: + return None + + +def resolve_base_commit() -> Optional[str]: + for env_name in ("GIT_PREVIOUS_SUCCESSFUL_COMMIT", "GIT_PREVIOUS_COMMIT"): + value = os.getenv(env_name) + if value: + return value + + change_target = os.getenv("CHANGE_TARGET") + if change_target: + base = try_git_command("merge-base", "HEAD", f"origin/{change_target}") + if base: + return base + + return try_git_command("rev-parse", "HEAD~1") + + +def collect_diff() -> str: + base_commit = resolve_base_commit() + if base_commit: + diff = try_git_command("diff", f"{base_commit}..HEAD", "--") + if diff: + return diff + + staged_diff = try_git_command("diff", "--cached", "--") + if staged_diff: + return staged_diff + + working_tree_diff = try_git_command("diff", "--") + if working_tree_diff: + return working_tree_diff + + return "" + + +def build_prompt(diff_text: str) -> str: + truncated_diff = diff_text[:MAX_DIFF_CHARS] + suffix = "" + if len(diff_text) > MAX_DIFF_CHARS: + suffix = "\n\nDiff was truncated to fit the token budget." + + return ( + "Review the following git diff. Focus on correctness, regressions, missing validation, " + "build/test issues, and security concerns. " + "Return either 'No issues found.' or a short flat list where each item includes severity, file, and issue.\n\n" + f"{truncated_diff}{suffix}" + ) + + +def request_review(diff_text: str) -> str: + try: + from openai import OpenAI + except ImportError as exc: + raise RuntimeError( + "The 'openai' package is not installed. Install it with 'pip install openai'." + ) from exc + + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + raise RuntimeError("OPENAI_API_KEY is not set.") + + model = os.getenv("CODEX_MODEL") or os.getenv("OPENAI_MODEL") or "gpt-4.1" + client = OpenAI(api_key=api_key) + + response = client.responses.create( + model=model, + input=[ + { + "role": "system", + "content": [ + { + "type": "input_text", + "text": ( + "You are a strict CI code reviewer. Be concise, concrete, and prioritize real defects." + ), + } + ], + }, + { + "role": "user", + "content": [ + { + "type": "input_text", + "text": build_prompt(diff_text), + } + ], + }, + ], + ) + + return response.output_text.strip() + + +def main() -> int: + diff_text = collect_diff() + if not diff_text: + print("No git changes detected. Skipping AI review.") + return 0 + + try: + review = request_review(diff_text) + except Exception as exc: + print(f"AI review failed: {exc}", file=sys.stderr) + return 1 + + print("AI review result:\n") + print(review or "No issues found.") + + fail_on_findings = os.getenv("AI_REVIEW_FAIL_ON_FINDINGS", "false").lower() == "true" + normalized = (review or "").strip().lower() + no_findings = normalized == "no issues found." + + if fail_on_findings and not no_findings: + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file