Compare commits

...

3 Commits

Author SHA1 Message Date
john e34b3570ab Update code
ai/code-review No issues found
hello-world/pipeline/pr-master This commit looks good
hello-world/pipeline/head This commit looks good
2026-05-03 09:50:29 +01:00
john 5db8dd5bc7 Provoke changes
ai/code-review AI review reported findings
hello-world/pipeline/pr-master This commit looks good
2026-05-03 09:47:44 +01:00
john a791e4c15c Update
hello-world/pipeline/head This commit looks good
ai/code-review AI review reported findings
hello-world/pipeline/pr-master This commit looks good
2026-05-03 09:41:47 +01:00
4 changed files with 157 additions and 6 deletions
Vendored
+3
View File
@@ -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 {
+18
View File
@@ -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
+132 -3
View File
@@ -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 = ""
@@ -68,6 +175,8 @@ def build_prompt(diff_text: str) -> str:
return ( return (
"Review the following git diff. Focus on correctness, regressions, missing validation, " "Review the following git diff. Focus on correctness, regressions, missing validation, "
"build/test issues, and security concerns. " "build/test issues, and security concerns. "
"No need to comment on removed code unless it seems like it would cause a problem. "
"Do not review the scripts in the scripts directory, as they are not part of the main codebase. "
"Return either 'No issues found.' or a short flat list where each item includes severity, file, and issue.\n\n" "Return either 'No issues found.' or a short flat list where each item includes severity, file, and issue.\n\n"
f"{truncated_diff}{suffix}" f"{truncated_diff}{suffix}"
) )
@@ -117,15 +226,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 +255,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
+4 -3
View File
@@ -1,10 +1,11 @@
#include <iostream> #include <iostream>
int add_3(int cx) { // Add six
return 2 + cx; int add_6(int cx) {
return 3 + cx;
} }
int main() { int main() {
std::cout << "Hello, world!" << add_3(5) << std::endl; std::cout << "Hello, world!" << add_6(5) << std::endl;
return 0; return 0;
} }