AI code review in GitLab CI: a merge request comment via the API
A GitLab CI job takes the merge request diff, sends it to Claude through Zukko and leaves a comment with findings in the MR, updating it on every push.
5 min · · development · автоматизация
AI code review in GitLab CI doesn't replace a reviewer, but it takes some routine off their plate: the model spots unhandled errors, missing None checks and suspicious SQL queries before a human opens the MR. Below is a job for .gitlab-ci.yml and a Python script: it takes the merge request diff, sends it to Claude through Zukko and leaves a single comment in the MR that is updated on every new push.
How it works
- On a merge request event, GitLab runs a pipeline with the
ai-reviewjob. - The script computes the diff between the MR base (
CI_MERGE_REQUEST_DIFF_BASE_SHA) and the current commit. - The diff goes to the model through Zukko's OpenAI-compatible API (
https://api.zukko.pro/v1). - The answer is posted as a comment via the GitLab Notes API:
POST /projects/:id/merge_requests/:merge_request_iid/notes. If a comment from a previous run exists, it is updated withPUT .../notes/:note_idso the discussion doesn't get cluttered.
GitLab passes all the identifiers to the job itself via predefined variables:
| Variable | Contents |
|---|---|
CI_API_V4_URL | GitLab API v4 address (works on self-hosted too) |
CI_PROJECT_ID | project ID |
CI_MERGE_REQUEST_IID | MR number within the project |
CI_MERGE_REQUEST_DIFF_BASE_SHA | base commit of the merge request diff |
CI_MERGE_REQUEST_TITLE | MR title |
CI_MERGE_REQUEST_DRAFT | true if the MR is a draft (GitLab 17.10+) |
CI_MERGE_REQUEST_* variables exist only in merge request pipelines, which is why the job runs on the rule CI_PIPELINE_SOURCE == "merge_request_event".
Tokens and CI/CD variables
You need two secrets.
A GitLab token for comments. The most convenient is a project access token (project settings → Access tokens) with the api scope and a role allowed to comment on MRs, for example Developer. Comments will come from the project's bot user rather than your account.
A Zukko key of the form fetch_..., created in the dashboard. Create a separate key for CI with daily and monthly dollar limits and a list of allowed models, so a pipeline bug can't spend more than the limit. More in key limits.
Add both values under Settings → CI/CD → Variables as GITLAB_REVIEW_TOKEN and ZUKKO_API_KEY, with the Masked flag so they don't show up in logs. With the Protected flag, a variable is visible only to pipelines on protected branches, and MR branches are usually unprotected, so the job wouldn't get it. Keep the flip side in mind: an unprotected variable can be read by anyone who can push branches to the project, so give the tokens minimal rights.
One more nuance: pipelines for MRs from forks run in the fork project by default, and the main project's variables are not available there.
The job in .gitlab-ci.yml
ai-review:
stage: test
image: python:3.12
variables:
GIT_DEPTH: 0
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_DRAFT != "true"
script:
- pip install --quiet openai requests
- python ci/ai_review.py
allow_failure: true
timeout: 10mWhat matters here:
GIT_DEPTH: 0fetches the full history; otherwise the MR base commit may be missing from the clone andgit difffails.- The
python:3.12image (notslim) already includesgit. allow_failure: truemeans the MR can still be merged if the API is down or the balance runs out.- Draft MRs are skipped until the author removes the Draft status.
The ai_review.py script
Put the file at ci/ai_review.py:
import os
import subprocess
import requests
from openai import OpenAI
API = os.environ["CI_API_V4_URL"]
PROJECT = os.environ["CI_PROJECT_ID"]
MR_IID = os.environ["CI_MERGE_REQUEST_IID"]
BASE_SHA = os.environ["CI_MERGE_REQUEST_DIFF_BASE_SHA"]
GITLAB = {"PRIVATE-TOKEN": os.environ["GITLAB_REVIEW_TOKEN"]}
MODEL = os.getenv("REVIEW_MODEL", "claude-sonnet-5")
MAX_DIFF_CHARS = int(os.getenv("REVIEW_MAX_DIFF_CHARS", "60000"))The reasoning behind the code:
- Exclusions. Lock files and minified code almost never need review, and they eat a lot of tokens.
EXCLUDEis a list of ordinary git pathspecs; extend it for your project. - Size cap.
MAX_DIFF_CHARSprotects against huge MRs (mass renames, generated code). For MRs that still don't fit, split the diff by file and send several requests. - One comment. The
<!-- ai-review -->marker at the start of the comment is invisible in the UI, but the script uses it to find and update its previous comment. - Errors. If Zukko returns an error (401 key, 402 balance, 403 model not allowed for the key, 429 rate), the SDK raises an exception, the job fails with a clear message in the log, and
allow_failurekeeps the MR unblocked.
Cost and model choice
Every push to the MR branch reruns the review, so count by pushes. Example: a diff of 8,000 input tokens and an answer of 800 output tokens on claude-sonnet-5 ($0.30 / $0.30 per 1M tokens in Zukko):
(8,000 + 800) × $0.30 / 1,000,000 = $0.00264 per run. At 200 runs a month, 200 × $0.00264 = $0.528.
On glm-5.3-flash ($0.04 / $0.04) the same volume costs 200 × 8,800 × $0.04 / 1,000,000 = $0.0704 a month. The model is switched with the REVIEW_MODEL variable, no script changes; the current list is at GET https://api.zukko.pro/v1/models or on the "Models" page in the dashboard. Test which model finds more real problems in your code on your own tasks: run 10–15 already closed MRs with known bugs through two models and compare what each one found.
FAQ
The job fails with "fatal: bad object" on git diff
The MR base commit is missing from the clone. Check that the job sets GIT_DEPTH: 0 and that the project settings (CI/CD → General pipelines) don't set a small clone depth that overrides it.
The comment doesn't appear, the log shows 401 or 403 from GitLab
The token is missing, expired or lacks the api scope. For a project access token, check the role too: Guest may not be allowed to comment on MRs. If the variable is marked Protected, it simply isn't present in a pipeline for an unprotected branch.
Can it comment on specific lines?
The Notes API creates a general MR comment. For line comments GitLab has the Discussions API with a position in the diff, which is harder: you need to map line numbers from the model's answer to diff versions. Start with a general comment.
Is it safe to send code to an external API?
That's your company's call: the diff goes to the model through Zukko. If the repository sometimes contains secrets, add config files and .env to EXCLUDE, or better, keep secrets out of the repository entirely. API formats and authentication are covered in the Zukko documentation.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Translating subtitles with a neural network: SRT without broken timecodesThe main risk when translating SRT with a neural network is a broken file structure. A scheme where timecodes never reach the model, and a Python script with validation.
- Editing texts with a neural network: proofreading, style and cuttingDifferent kinds of editing need different prompts. Wording for proofreading, style and cutting, how to get edits with explanations and how not to lose the author's voice.
- A neural network for writing scripts: Bash, PowerShell and PythonA model writes scripts for routine fast, but running them blindly is risky. How to state the task, demand a safe dry-run mode and check the result before launch.
- Dify in Docker: installation, first-run setup and connecting modelsInstall Dify with Docker Compose, create the admin account and connect Claude, GPT and DeepSeek models on one key. Plus what to do when containers will not start.