by2ndOpinion Team

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.

gitlabci-cdautomationmerge-requeststutorialpipeline

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:

  1. A 2ndOpinion API key — get one free at get2ndopinion.dev/pricing (the free tier includes 5 credits/month; Pro gives you 100)
  2. 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.