AI Code Review in GitLab CI/CD: A Complete Setup Guide
Add AI code review to every GitLab merge request. Run Claude, Codex, or Gemini on your diffs automatically and block risky code before it merges.
Most bugs caught in code review share the same origin story: a reviewer spotted something in the diff that automated checks missed. The problem is that human reviewers are expensive, inconsistent, and unavailable at 2 AM on a Friday when a hotfix needs to ship.
Adding AI code review to your GitLab CI/CD pipeline puts a sharp-eyed reviewer on every merge request — one that checks for logic errors, security vulnerabilities, and off-by-one mistakes at the exact moment a developer opens an MR, without waiting for a teammate to free up.
This guide walks through setting up AI code review in GitLab CI using 2ndOpinion, from a basic single-job setup to a full pipeline that blocks merges on high-risk diffs.
Prerequisites
You need two things before writing any pipeline YAML:
- A 2ndOpinion API key — get one free at get2ndopinion.dev/pricing (the free tier includes 5 credits/month; Pro gives you 100)
- A GitLab project with CI/CD enabled
Store your API key as a CI/CD variable in GitLab: Settings → CI/CD → Variables → Add variable. Name it SECOND_OPINION_API_KEY and mark it as masked so it never appears in job logs.
Basic Setup: Review Every Merge Request
The minimal integration calls the /api/gateway/opinion endpoint with the diff from the current MR. Add this job to your .gitlab-ci.yml:
ai-code-review:
stage: test
image: alpine:latest
before_script:
- apk add --no-cache curl jq git
script:
- |
DIFF=$(git diff origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...HEAD)
if [ -z "$DIFF" ]; then
echo "No diff found, skipping review."
exit 0
fi
RESPONSE=$(curl -s -X POST https://get2ndopinion.dev/api/gateway/opinion \
-H "Content-Type: application/json" \
-H "X-2ndOpinion-Key: ${SECOND_OPINION_API_KEY}" \
-d "{
\"diff\": $(echo "$DIFF" | jq -Rs .),
\"model\": \"claude\",
\"context\": \"GitLab MR ${CI_MERGE_REQUEST_IID}: ${CI_MERGE_REQUEST_TITLE}\"
}")
echo "$RESPONSE" | jq .
only:
- merge_requests
That's the whole thing. When a developer opens or updates an MR, GitLab runs this job, sends the diff to Claude, and prints the structured review in the job log.
Understanding the Response
The API returns a JSON object with a recommendation field (accept, review, or reject), a riskLevel (low, medium, high), a summary, and an array of risks:
{
"recommendation": "review",
"riskLevel": "medium",
"summary": "The change adds a new user lookup endpoint but lacks input validation on the email parameter.",
"risks": [
{
"category": "security",
"severity": "high",
"description": "Email parameter passed directly to query without sanitization.",
"file": "src/routes/users.ts",
"line": 42
}
]
}
Each risk includes the file and line number, so developers can navigate directly to the problem from the job log.
Blocking Merges on High-Risk Code
Printing the review is useful. Failing the pipeline when the AI flags serious issues is better. Update the script section to exit with a non-zero code when risk is high:
script:
- |
DIFF=$(git diff origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...HEAD)
if [ -z "$DIFF" ]; then
echo "No diff found, skipping review."
exit 0
fi
RESPONSE=$(curl -s -X POST https://get2ndopinion.dev/api/gateway/opinion \
-H "Content-Type: application/json" \
-H "X-2ndOpinion-Key: ${SECOND_OPINION_API_KEY}" \
-d "{
\"diff\": $(echo "$DIFF" | jq -Rs .),
\"model\": \"claude\"
}")
echo "$RESPONSE" | jq .
RISK=$(echo "$RESPONSE" | jq -r '.riskLevel // "low"')
RECOMMENDATION=$(echo "$RESPONSE" | jq -r '.recommendation // "accept"')
echo "Risk level: $RISK"
echo "Recommendation: $RECOMMENDATION"
if [ "$RECOMMENDATION" = "reject" ]; then
echo "AI review returned REJECT. Fix the flagged issues before merging."
exit 1
fi
Set the job as a required check in Settings → Merge requests → Merge checks and no one can merge an AI-rejected MR without bypassing the pipeline. For most teams, this is the right default — you want the AI to be a hard gate on reject, but only a soft warning on review.
Using Consensus Review for Critical Branches
For merges into main or production, you probably want more than one model's opinion. The /api/gateway/consensus endpoint runs Claude, Codex, and Gemini in parallel and returns a unified report based on where the models agree.
ai-consensus-review:
stage: test
image: alpine:latest
before_script:
- apk add --no-cache curl jq git
script:
- |
DIFF=$(git diff origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...HEAD)
RESPONSE=$(curl -s -X POST https://get2ndopinion.dev/api/gateway/consensus \
-H "Content-Type: application/json" \
-H "X-2ndOpinion-Key: ${SECOND_OPINION_API_KEY}" \
-d "{\"diff\": $(echo "$DIFF" | jq -Rs .)}")
echo "$RESPONSE" | jq .
RECOMMENDATION=$(echo "$RESPONSE" | jq -r '.finalRecommendation // "accept"')
if [ "$RECOMMENDATION" = "reject" ]; then
exit 1
fi
only:
- merge_requests
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
Consensus review costs 3 credits instead of 1 and takes slightly longer, so it makes sense to use it selectively — only on MRs targeting main, or only when the diff exceeds a certain size.
Saving Reviews as Job Artifacts
Job logs disappear. If you want to keep a permanent record of every AI review, save the output as an artifact:
ai-code-review:
stage: test
image: alpine:latest
before_script:
- apk add --no-cache curl jq git
script:
- |
DIFF=$(git diff origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...HEAD)
curl -s -X POST https://get2ndopinion.dev/api/gateway/opinion \
-H "Content-Type: application/json" \
-H "X-2ndOpinion-Key: ${SECOND_OPINION_API_KEY}" \
-d "{\"diff\": $(echo "$DIFF" | jq -Rs .), \"model\": \"claude\"}" \
| jq . > review.json
cat review.json
jq -r '.summary' review.json
artifacts:
paths:
- review.json
expire_in: 30 days
only:
- merge_requests
Artifacts are accessible from the GitLab pipeline UI and downloadable by anyone with project access. This is especially useful for compliance workflows where you need a paper trail of pre-merge code reviews.
Choosing the Right Model
All three models are available via the model parameter (claude, codex, gemini). Here's how they tend to differ for code review:
- Claude — Strong reasoning about intent, good at catching subtle logic errors and explaining why something is risky
- Codex — Deep familiarity with common code patterns, quick to spot deviations from idiomatic style
- Gemini — Broad context window, useful for larger diffs across many files
If you're not sure which to use, start with claude. If you're reviewing larger refactors that touch dozens of files, gemini handles the extra context well. For teams with a mix of experienced and junior developers, Claude's explanations are the most readable.
Complete Pipeline Configuration
Here is a full .gitlab-ci.yml that combines the above patterns — single-model review on all MRs, consensus review only on merges to main:
stages:
- test
.review-base:
stage: test
image: alpine:latest
before_script:
- apk add --no-cache curl jq git
only:
- merge_requests
ai-code-review:
extends: .review-base
script:
- |
DIFF=$(git diff origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...HEAD)
[ -z "$DIFF" ] && echo "Empty diff, skipping." && exit 0
RESPONSE=$(curl -s -X POST https://get2ndopinion.dev/api/gateway/opinion \
-H "Content-Type: application/json" \
-H "X-2ndOpinion-Key: ${SECOND_OPINION_API_KEY}" \
-d "{\"diff\": $(echo "$DIFF" | jq -Rs .), \"model\": \"claude\"}" \
| tee review.json)
jq . review.json
[ "$(jq -r '.recommendation' review.json)" = "reject" ] && exit 1 || exit 0
artifacts:
paths: [review.json]
expire_in: 30 days
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME != "main"'
ai-consensus-review:
extends: .review-base
script:
- |
DIFF=$(git diff origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...HEAD)
[ -z "$DIFF" ] && echo "Empty diff, skipping." && exit 0
RESPONSE=$(curl -s -X POST https://get2ndopinion.dev/api/gateway/consensus \
-H "Content-Type: application/json" \
-H "X-2ndOpinion-Key: ${SECOND_OPINION_API_KEY}" \
-d "{\"diff\": $(echo "$DIFF" | jq -Rs .)}" \
| tee consensus.json)
jq . consensus.json
[ "$(jq -r '.finalRecommendation' consensus.json)" = "reject" ] && exit 1 || exit 0
artifacts:
paths: [consensus.json]
expire_in: 30 days
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
Copy this into your project root, add the SECOND_OPINION_API_KEY CI variable, and every MR gets an AI reviewer from the first push.
What to Expect
Teams that add AI code review to CI consistently see the same pattern: the first few reviews surface issues that were already known but never prioritized, and then within a few weeks it starts catching things reviewers were genuinely missing — missing null checks, subtle race conditions, hardcoded credentials, endpoints missing auth. The value compounds as the pipeline becomes a habit.
The free tier at get2ndopinion.dev/pricing is enough to try this on a small project before committing. If you want to test the output format before wiring up your pipeline, the playground lets you paste a diff and see a real review in seconds.