Skip to content

Commit 991eae3

Browse files
Add gitlab example (#2251)
Co-authored-by: juston <[email protected]>
1 parent 337575d commit 991eae3

File tree

1 file changed

+264
-0
lines changed

1 file changed

+264
-0
lines changed

examples/codex/build_code_review_with_codex_sdk.md

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,270 @@ jobs:
336336
shell: bash
337337
```
338338

339+
## Gitlab Example
340+
GitLab doesn’t have a direct equivalent to the GitHub Action, but you can run codex exec inside GitLab CI/CD to perform automated code reviews.
341+
342+
However, the GitHub Action includes an important [safety strategy](https://github.com/openai/codex-action?tab=readme-ov-file#safety-strategy): it drops sudo permissions so Codex cannot access its own OpenAI API key. This isolation is critical—especially for public repositories where sensitive secrets (like your OpenAI API key) may be present—because it prevents Codex from reading or exfiltrating credentials during execution.
343+
Before running this job, configure your GitLab project:
344+
345+
1. Go to **Project → Settings → CI/CD**.
346+
2. Expand the **Variables** section.
347+
3. Add these variables:
348+
- `OPENAI_API_KEY`
349+
- `GITLAB_TOKEN`
350+
4. Mark them as masked/protected as appropriate.
351+
5. Add the following GitLab example job to your `.gitlab-ci.yml` file at the root of your repository so it runs during merge request pipelines.
352+
353+
Please be mindful with your API key on public repositories.
354+
355+
```yaml
356+
stages:
357+
- review
358+
359+
codex-structured-review:
360+
stage: review
361+
image: ubuntu:22.04
362+
rules:
363+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
364+
variables:
365+
PR_NUMBER: $CI_MERGE_REQUEST_IID
366+
REPOSITORY: "$CI_PROJECT_PATH"
367+
BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA"
368+
HEAD_SHA: "$CI_COMMIT_SHA"
369+
370+
before_script:
371+
- apt-get update -y
372+
- apt-get install -y git curl jq
373+
- |
374+
if ! command -v codex >/dev/null 2>&1; then
375+
ARCH="$(uname -m)"
376+
case "$ARCH" in
377+
x86_64) CODEX_PLATFORM="x86_64-unknown-linux-musl" ;;
378+
aarch64|arm64) CODEX_PLATFORM="aarch64-unknown-linux-musl" ;;
379+
*)
380+
echo "Unsupported architecture: $ARCH"
381+
exit 1
382+
;;
383+
esac
384+
385+
CODEX_VERSION="${CODEX_VERSION:-latest}"
386+
if [ -n "${CODEX_DOWNLOAD_URL:-}" ]; then
387+
CODEX_URL="$CODEX_DOWNLOAD_URL"
388+
elif [ "$CODEX_VERSION" = "latest" ]; then
389+
CODEX_URL="https://github.com/openai/codex/releases/latest/download/codex-${CODEX_PLATFORM}.tar.gz"
390+
else
391+
CODEX_URL="https://github.com/openai/codex/releases/download/${CODEX_VERSION}/codex-${CODEX_PLATFORM}.tar.gz"
392+
fi
393+
394+
TMP_DIR="$(mktemp -d)"
395+
curl -fsSL "$CODEX_URL" -o "$TMP_DIR/codex.tar.gz"
396+
tar -xzf "$TMP_DIR/codex.tar.gz" -C "$TMP_DIR"
397+
install -m 0755 "$TMP_DIR"/codex-* /usr/local/bin/codex
398+
rm -rf "$TMP_DIR"
399+
fi
400+
- git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
401+
- git fetch origin $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
402+
- git checkout $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
403+
404+
script:
405+
- echo "Running Codex structured review for MR !${PR_NUMBER}"
406+
407+
# Generate structured output schema
408+
- |
409+
cat <<'JSON' > codex-output-schema.json
410+
{
411+
"$schema": "http://json-schema.org/draft-07/schema#",
412+
"title": "Codex Structured Review",
413+
"type": "object",
414+
"additionalProperties": false,
415+
"required": [
416+
"overall_correctness",
417+
"overall_explanation",
418+
"overall_confidence_score",
419+
"findings"
420+
],
421+
"properties": {
422+
"overall_correctness": {
423+
"type": "string",
424+
"description": "Overall verdict for the merge request."
425+
},
426+
"overall_explanation": {
427+
"type": "string",
428+
"description": "Explanation backing up the verdict."
429+
},
430+
"overall_confidence_score": {
431+
"type": "number",
432+
"minimum": 0,
433+
"maximum": 1,
434+
"description": "Confidence level for the verdict."
435+
},
436+
"findings": {
437+
"type": "array",
438+
"description": "Collection of actionable review findings.",
439+
"items": {
440+
"type": "object",
441+
"additionalProperties": false,
442+
"required": [
443+
"title",
444+
"body",
445+
"confidence_score",
446+
"code_location"
447+
],
448+
"properties": {
449+
"title": {
450+
"type": "string"
451+
},
452+
"body": {
453+
"type": "string"
454+
},
455+
"confidence_score": {
456+
"type": "number",
457+
"minimum": 0,
458+
"maximum": 1
459+
},
460+
"code_location": {
461+
"type": "object",
462+
"additionalProperties": false,
463+
"required": [
464+
"absolute_file_path",
465+
"relative_file_path",
466+
"line_range"
467+
],
468+
"properties": {
469+
"absolute_file_path": {
470+
"type": "string"
471+
},
472+
"relative_file_path": {
473+
"type": "string"
474+
},
475+
"line_range": {
476+
"type": "object",
477+
"additionalProperties": false,
478+
"required": [
479+
"start",
480+
"end"
481+
],
482+
"properties": {
483+
"start": {
484+
"type": "integer",
485+
"minimum": 1
486+
},
487+
"end": {
488+
"type": "integer",
489+
"minimum": 1
490+
}
491+
}
492+
}
493+
}
494+
}
495+
}
496+
},
497+
"default": []
498+
}
499+
}
500+
}
501+
JSON
502+
503+
# Build Codex review prompt
504+
- |
505+
PROMPT_PATH="codex-prompt.md"
506+
TEMPLATE_PATH="${REVIEW_PROMPT_PATH:-review_prompt.md}"
507+
if [ -n "$TEMPLATE_PATH" ] && [ -f "$TEMPLATE_PATH" ]; then
508+
cat "$TEMPLATE_PATH" > "$PROMPT_PATH"
509+
else
510+
{
511+
printf '%s\n' "You are acting as a reviewer for a proposed code change..."
512+
printf '%s\n' "Focus on issues that impact correctness, performance, security..."
513+
printf '%s\n' "Flag only actionable issues introduced by this merge request..."
514+
printf '%s\n' "Provide an overall correctness verdict..."
515+
} > "$PROMPT_PATH"
516+
fi
517+
{
518+
echo ""
519+
echo "Repository: ${REPOSITORY}"
520+
echo "Merge Request #: ${PR_NUMBER}"
521+
echo "Base SHA: ${BASE_SHA}"
522+
echo "Head SHA: ${HEAD_SHA}"
523+
echo ""
524+
echo "Changed files:"
525+
git --no-pager diff --name-status "${BASE_SHA}" "${HEAD_SHA}"
526+
echo ""
527+
echo "Unified diff (context=5):"
528+
git --no-pager diff --unified=5 "${BASE_SHA}" "${HEAD_SHA}"
529+
} >> "$PROMPT_PATH"
530+
531+
# Run Codex exec CLI
532+
- |
533+
printenv OPENAI_API_KEY | codex login --with-api-key && \
534+
codex exec --output-schema codex-output-schema.json \
535+
--output-last-message codex-output.json \
536+
--sandbox read-only \
537+
- < codex-prompt.md
538+
539+
# Inspect structured Codex output
540+
- |
541+
if [ -s codex-output.json ]; then
542+
jq '.' codex-output.json || true
543+
else
544+
echo "Codex output file missing"; exit 1
545+
fi
546+
547+
# Publish inline comments to GitLab MR
548+
- |
549+
findings_count=$(jq '.findings | length' codex-output.json)
550+
if [ "$findings_count" -eq 0 ]; then
551+
echo "No findings from Codex; skipping comments."
552+
exit 0
553+
fi
554+
555+
jq -c \
556+
--arg base "$BASE_SHA" \
557+
--arg start "$BASE_SHA" \
558+
--arg head "$HEAD_SHA" '
559+
.findings[] | {
560+
body: (.title + "\n\n" + .body + "\n\nConfidence: " + (.confidence_score | tostring)),
561+
position: {
562+
position_type: "text",
563+
base_sha: $base,
564+
start_sha: $start,
565+
head_sha: $head,
566+
new_path: (.code_location.relative_file_path // .code_location.absolute_file_path),
567+
new_line: .code_location.line_range.end
568+
}
569+
}
570+
' codex-output.json > findings.jsonl
571+
572+
while IFS= read -r payload; do
573+
curl -sS --request POST \
574+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
575+
--header "Content-Type: application/json" \
576+
--data "$payload" \
577+
"https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/merge_requests/${PR_NUMBER}/discussions"
578+
done < findings.jsonl
579+
580+
# Publish overall summary comment
581+
- |
582+
overall_state=$(jq -r '.overall_correctness' codex-output.json)
583+
overall_body=$(jq -r '.overall_explanation' codex-output.json)
584+
confidence=$(jq -r '.overall_confidence_score' codex-output.json)
585+
586+
summary="**Codex automated review**\n\nVerdict: ${overall_state}\nConfidence: ${confidence}\n\n${overall_body}"
587+
588+
curl -sS --request POST \
589+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
590+
--header "Content-Type: application/json" \
591+
--data "$(jq -n --arg body "$summary" '{body: $body}')" \
592+
"https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/merge_requests/${PR_NUMBER}/notes"
593+
594+
artifacts:
595+
when: always
596+
paths:
597+
- codex-output.json
598+
- codex-prompt.md
599+
600+
```
601+
602+
339603
## Jenkins Example
340604

341605
We can use the same approach to scripting a job with Jenkins. Once again, comments highlight key stages of the workflow:

0 commit comments

Comments
 (0)