@@ -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())
|
||||
Reference in New Issue
Block a user