Update
This commit is contained in:
Vendored
+3
@@ -2,7 +2,10 @@ pipeline {
|
|||||||
agent any
|
agent any
|
||||||
environment {
|
environment {
|
||||||
OPENAI_API_KEY = credentials('OPENAI_API_KEY')
|
OPENAI_API_KEY = credentials('OPENAI_API_KEY')
|
||||||
|
GITEA_TOKEN = credentials('GITEA_TOKEN')
|
||||||
|
GITEA_URL = 'https://git.jb9.uk'
|
||||||
CODEX_MODEL = 'gpt-4'
|
CODEX_MODEL = 'gpt-4'
|
||||||
|
AI_REVIEW_FAIL_ON_FINDINGS = 'false'
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# Hello World C++ Project
|
# Hello World C++ Project
|
||||||
|
|
||||||
|
## Jenkins AI Review
|
||||||
|
|
||||||
|
The Jenkins pipeline can run an OpenAI-powered review and report back to Gitea.
|
||||||
|
|
||||||
|
Required Jenkins secrets and variables:
|
||||||
|
|
||||||
|
- `OPENAI_API_KEY`
|
||||||
|
- `GITEA_TOKEN`
|
||||||
|
- `GITEA_URL`
|
||||||
|
|
||||||
|
Optional variables:
|
||||||
|
|
||||||
|
- `CODEX_MODEL`
|
||||||
|
- `AI_REVIEW_FAIL_ON_FINDINGS`
|
||||||
|
- `GITEA_REPO_OWNER`
|
||||||
|
- `GITEA_REPO_NAME`
|
||||||
|
- `GITEA_PR_NUMBER`
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+130
-3
@@ -1,12 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from urllib import error, parse, request
|
||||||
|
|
||||||
|
|
||||||
MAX_DIFF_CHARS = 12000
|
MAX_DIFF_CHARS = 12000
|
||||||
|
STATUS_CONTEXT = "ai/code-review"
|
||||||
|
|
||||||
|
|
||||||
def run_git_command(*args: str) -> str:
|
def run_git_command(*args: str) -> str:
|
||||||
@@ -59,6 +62,110 @@ def collect_diff() -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_commit() -> Optional[str]:
|
||||||
|
return os.getenv("GIT_COMMIT") or try_git_command("rev-parse", "HEAD")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_repo_from_remote() -> tuple[Optional[str], Optional[str]]:
|
||||||
|
remote_url = try_git_command("remote", "get-url", "origin")
|
||||||
|
if not remote_url:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
cleaned = remote_url.strip()
|
||||||
|
if cleaned.endswith(".git"):
|
||||||
|
cleaned = cleaned[:-4]
|
||||||
|
|
||||||
|
if cleaned.startswith("git@") and ":" in cleaned:
|
||||||
|
cleaned = cleaned.split(":", 1)[1]
|
||||||
|
elif "://" in cleaned:
|
||||||
|
parsed = parse.urlparse(cleaned)
|
||||||
|
cleaned = parsed.path.lstrip("/")
|
||||||
|
|
||||||
|
parts = [part for part in cleaned.split("/") if part]
|
||||||
|
if len(parts) < 2:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
return parts[-2], parts[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_gitea_repo() -> tuple[Optional[str], Optional[str]]:
|
||||||
|
owner = os.getenv("GITEA_REPO_OWNER")
|
||||||
|
name = os.getenv("GITEA_REPO_NAME")
|
||||||
|
if owner and name:
|
||||||
|
return owner, name
|
||||||
|
|
||||||
|
return parse_repo_from_remote()
|
||||||
|
|
||||||
|
|
||||||
|
def post_gitea_json(api_path: str, payload: dict) -> None:
|
||||||
|
base_url = os.getenv("GITEA_URL")
|
||||||
|
token = os.getenv("GITEA_TOKEN")
|
||||||
|
if not base_url or not token:
|
||||||
|
return
|
||||||
|
|
||||||
|
url = f"{base_url.rstrip('/')}{api_path}"
|
||||||
|
data = json.dumps(payload).encode("utf-8")
|
||||||
|
req = request.Request(
|
||||||
|
url,
|
||||||
|
data=data,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with request.urlopen(req) as response:
|
||||||
|
response.read()
|
||||||
|
except error.HTTPError as exc:
|
||||||
|
details = exc.read().decode("utf-8", errors="replace")
|
||||||
|
raise RuntimeError(f"Gitea API request failed: HTTP {exc.code} {details}") from exc
|
||||||
|
except error.URLError as exc:
|
||||||
|
raise RuntimeError(f"Gitea API request failed: {exc.reason}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def publish_commit_status(state: str, description: str) -> None:
|
||||||
|
base_url = os.getenv("GITEA_URL")
|
||||||
|
token = os.getenv("GITEA_TOKEN")
|
||||||
|
if not base_url or not token:
|
||||||
|
return
|
||||||
|
|
||||||
|
owner, repo = get_gitea_repo()
|
||||||
|
commit = get_current_commit()
|
||||||
|
if not owner or not repo or not commit:
|
||||||
|
return
|
||||||
|
|
||||||
|
build_url = os.getenv("BUILD_URL")
|
||||||
|
payload = {
|
||||||
|
"state": state,
|
||||||
|
"context": STATUS_CONTEXT,
|
||||||
|
"description": description[:255],
|
||||||
|
}
|
||||||
|
if build_url:
|
||||||
|
payload["target_url"] = build_url
|
||||||
|
|
||||||
|
post_gitea_json(f"/api/v1/repos/{owner}/{repo}/statuses/{commit}", payload)
|
||||||
|
|
||||||
|
|
||||||
|
def publish_pr_comment(body: str) -> None:
|
||||||
|
base_url = os.getenv("GITEA_URL")
|
||||||
|
token = os.getenv("GITEA_TOKEN")
|
||||||
|
if not base_url or not token:
|
||||||
|
return
|
||||||
|
|
||||||
|
owner, repo = get_gitea_repo()
|
||||||
|
pr_number = os.getenv("GITEA_PR_NUMBER") or os.getenv("CHANGE_ID")
|
||||||
|
if not owner or not repo or not pr_number:
|
||||||
|
return
|
||||||
|
|
||||||
|
post_gitea_json(
|
||||||
|
f"/api/v1/repos/{owner}/{repo}/issues/{pr_number}/comments",
|
||||||
|
{"body": body},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_prompt(diff_text: str) -> str:
|
def build_prompt(diff_text: str) -> str:
|
||||||
truncated_diff = diff_text[:MAX_DIFF_CHARS]
|
truncated_diff = diff_text[:MAX_DIFF_CHARS]
|
||||||
suffix = ""
|
suffix = ""
|
||||||
@@ -117,15 +224,28 @@ def request_review(diff_text: str) -> str:
|
|||||||
return response.output_text.strip()
|
return response.output_text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def classify_review(review: str) -> tuple[str, str]:
|
||||||
|
normalized = (review or "").strip().lower()
|
||||||
|
if normalized == "no issues found.":
|
||||||
|
return "success", "No issues found"
|
||||||
|
return "failure", "AI review reported findings"
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
diff_text = collect_diff()
|
diff_text = collect_diff()
|
||||||
if not diff_text:
|
if not diff_text:
|
||||||
print("No git changes detected. Skipping AI review.")
|
message = "No git changes detected. Skipping AI review."
|
||||||
|
print(message)
|
||||||
|
publish_commit_status("success", "No changes to review")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
review = request_review(diff_text)
|
review = request_review(diff_text)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
try:
|
||||||
|
publish_commit_status("error", "AI review failed")
|
||||||
|
except Exception as gitea_exc:
|
||||||
|
print(f"Gitea reporting failed: {gitea_exc}", file=sys.stderr)
|
||||||
print(f"AI review failed: {exc}", file=sys.stderr)
|
print(f"AI review failed: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@@ -133,8 +253,15 @@ def main() -> int:
|
|||||||
print(review or "No issues found.")
|
print(review or "No issues found.")
|
||||||
|
|
||||||
fail_on_findings = os.getenv("AI_REVIEW_FAIL_ON_FINDINGS", "false").lower() == "true"
|
fail_on_findings = os.getenv("AI_REVIEW_FAIL_ON_FINDINGS", "false").lower() == "true"
|
||||||
normalized = (review or "").strip().lower()
|
state, description = classify_review(review)
|
||||||
no_findings = normalized == "no issues found."
|
|
||||||
|
try:
|
||||||
|
publish_commit_status(state, description)
|
||||||
|
publish_pr_comment(f"AI review result:\n\n{review or 'No issues found.'}")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Gitea reporting failed: {exc}", file=sys.stderr)
|
||||||
|
|
||||||
|
no_findings = state == "success"
|
||||||
|
|
||||||
if fail_on_findings and not no_findings:
|
if fail_on_findings and not no_findings:
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
Reference in New Issue
Block a user