12 Commits

Author SHA1 Message Date
john 62ba8e0ff0 Bug fixing
ai/code-review No issues found
hello-world/pipeline/head This commit looks good
2026-05-03 21:39:24 +01:00
john 57e44e5bc9 Bug fixing
ai/code-review No issues found
hello-world/pipeline/head This commit looks good
2026-05-03 21:38:45 +01:00
john f39884310d Bug fixing
ai/code-review No issues found
hello-world/pipeline/head This commit looks good
2026-05-03 21:37:02 +01:00
john 4f05b6f6c0 Bug fixing
ai/code-review AI review reported findings
hello-world/pipeline/head This commit looks good
2026-05-03 21:35:53 +01:00
john d53655f4f4 New source
ai/code-review AI review reported findings
hello-world/pipeline/head There was a failure building this commit
2026-05-03 21:27:57 +01:00
john 959c4304b3 Update prompt
ai/code-review AI review reported findings
hello-world/pipeline/head This commit looks good
2026-05-03 21:27:03 +01:00
john 76292c8393 Make more efficient
ai/code-review AI review reported findings
hello-world/pipeline/head This commit looks good
2026-05-03 21:25:21 +01:00
john b504842293 Switching to x.ai for reviews
ai/code-review No issues found
hello-world/pipeline/head This commit looks good
2026-05-03 21:22:07 +01:00
john 4973316155 Update (#2)
ai/code-review AI review reported findings
hello-world/pipeline/head This commit looks good
Reviewed-on: #2
Co-authored-by: John Burton <john.burton@jbmail.com>
Co-committed-by: John Burton <john.burton@jbmail.com>
2026-05-03 09:15:56 +00:00
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 183 additions and 16 deletions
Vendored
+5 -2
View File
@@ -1,8 +1,11 @@
pipeline { pipeline {
agent any agent any
environment { environment {
OPENAI_API_KEY = credentials('OPENAI_API_KEY') XAI_API_KEY = credentials('XAI_API_KEY')
CODEX_MODEL = 'gpt-4' GITEA_TOKEN = credentials('GITEA_TOKEN')
GITEA_URL = 'https://git.jb9.uk'
XAI_MODEL = 'grok-4.3'
AI_REVIEW_FAIL_ON_FINDINGS = 'false'
} }
stages { stages {
+22
View File
@@ -1,5 +1,27 @@
# Hello World C++ Project # Hello World C++ Project
## Jenkins AI Review
The Jenkins pipeline can run an x.ai-powered review and report back to Gitea.
Required Jenkins secrets and variables:
- `XAI_API_KEY`
- `GITEA_TOKEN`
- `GITEA_URL`
Optional variables:
- `AI_REVIEW_MODEL`
- `XAI_MODEL`
- `XAI_BASE_URL`
- `AI_REVIEW_FAIL_ON_FINDINGS`
- `GITEA_REPO_OWNER`
- `GITEA_REPO_NAME`
- `GITEA_PR_NUMBER`
The default review model is `grok-4.3`, using the OpenAI Python SDK against `https://api.x.ai/v1`.
## Build ## Build
```bash ```bash
+147 -8
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 = ""
@@ -67,7 +174,12 @@ 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 as well as style and usage of c++23. "
"Bring to attention any patterns that are generally considered error-prone or hard to maintain. "
"Highlight any places where the code could be simplified or made more efficient. "
"Provide suggestions for improving readability and maintainability. "
"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}"
) )
@@ -81,12 +193,19 @@ def request_review(diff_text: str) -> str:
"The 'openai' package is not installed. Install it with 'pip install openai'." "The 'openai' package is not installed. Install it with 'pip install openai'."
) from exc ) from exc
api_key = os.getenv("OPENAI_API_KEY") api_key = os.getenv("XAI_API_KEY") or os.getenv("OPENAI_API_KEY")
if not api_key: if not api_key:
raise RuntimeError("OPENAI_API_KEY is not set.") raise RuntimeError("XAI_API_KEY is not set.")
model = os.getenv("CODEX_MODEL") or os.getenv("OPENAI_MODEL") or "gpt-4.1" model = (
client = OpenAI(api_key=api_key) os.getenv("AI_REVIEW_MODEL")
or os.getenv("XAI_MODEL")
or os.getenv("CODEX_MODEL")
or os.getenv("OPENAI_MODEL")
or "grok-4.3"
)
base_url = os.getenv("XAI_BASE_URL") or "https://api.x.ai/v1"
client = OpenAI(api_key=api_key, base_url=base_url)
response = client.responses.create( response = client.responses.create(
model=model, model=model,
@@ -117,15 +236,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 +265,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
+9 -6
View File
@@ -1,10 +1,13 @@
#include <iostream> #include <iostream>
int add_3(int cx) {
return 2 + cx;
}
int main() { int main()
std::cout << "Hello, world!" << add_3(5) << std::endl; {
return 0; auto the_max_value = 100;
int total = 0;
for (int v = 0; v < the_max_value; ++v) {
total += v;
}
std::cout << "Total: " << total << std::endl;
} }