GitLab merge requests
Review merge requests on GitLab.com or self-managed GitLab with ocra review --mr.
ocra review --mr <iid> reviews a GitLab merge request, on GitLab.com or a self-managed instance. With --publish it posts the review:
- one summary note, updated in place on every push;
- a thread on each finding in the diff.
Later pipelines review only what changed since the last review. The rules are the ones for GitHub pull requests: who may override or dismiss, which earlier state counts, and what text may be posted. The same tests check them on both platforms.
GitLab support is new in 0.2.0. Besides the tests against a fake GitLab API, it was checked on GitLab.com Free on 2026-10-04 (0.3.0, a dedicated account's personal access token, a scripted model): threads on added and on unchanged lines, the summary updated in place, an incremental second run without repeated comments, no live mention, link or quick action from model text, and the author's resolution, override and edit of ocra's summary all counting for nothing. A dismissal by another member has not been tried live yet.
Set up
-
Create a token for ocra with the
apiscope and the Developer role:- on GitLab Premium or Ultimate, and on self-managed GitLab, a project access token (Settings → Access tokens), which gets a bot user of its own;
- on GitLab.com Free, where project access tokens are not available, a personal access token of a dedicated account (for example
ocra-review-bot, invited to the project as Developer). Do not use your own account's token: ocra would then post as you, and since it never takes a merge request's author for a reviewer, your own merge requests would lose your dismissals and overrides.
ocra posts notes and threads, resolves its threads, and reads, through GraphQL, who last edited a comment.
CI_JOB_TOKENcannot post comments. -
Add it as a CI/CD variable named
GITLAB_TOKEN, masked. Leave it unprotected: pipelines for merge requests from unprotected branches cannot read protected variables. So every pipeline of the project can read the token: keep its role at Developer and give it an expiry date (who controls the pipeline). -
Add your model key the same way, for example
GEMINI_API_KEY: a key that serves only these reviews, with a spending limit at the provider. -
Add the job to
.gitlab-ci.yml:
ocra-review:
stage: test
image: node:22
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
variables:
GIT_DEPTH: 0
interruptible: true
# The verdict is advice: a blocking one (exit code 1) does not fail the
# pipeline. An incomplete review (exit code 3) still does.
allow_failure:
exit_codes: [1]
script:
- npm install -g --ignore-scripts @open-cr-agent/cli@0.2.0
- ocra review --mr "$CI_MERGE_REQUEST_IID" --publishThe job can also use the container image, which needs no install step. Pin it by digest, which docker buildx imagetools inspect ghcr.io/jma49/ocra:0.2.0 prints, and check its build provenance as Stability shows:
ocra-review:
image:
name: ghcr.io/jma49/ocra:0.2.0@sha256:<digest>
# The job's script runs in a shell, not through the image's ocra entrypoint.
entrypoint: [""]
# (rules, variables, interruptible and allow_failure as above)
script:
- ocra review --mr "$CI_MERGE_REQUEST_IID" --publishGIT_DEPTH: 0 gives ocra the history between the target branch and the merge request; a shallow clone stops with a message saying so. With interruptible: true and the project's Auto-cancel redundant pipelines setting, a new push cancels the review still running for the previous one. A review cancelled before it publishes posts nothing. --ignore-scripts keeps install scripts from running with your variables in the environment; ocra finds the OpenCode binary without them.
Models come from the target branch's .ocra/config.json, or from OCRA_MODEL_* variables. Other options go on the ocra review line, such as --max-cost-usd 2 or --reviewers correctness,security.
What gets posted
- A thread on each finding on a line of the diff: on an added line, or on an unchanged line next to the change. A finding GitLab will not place stays in the summary.
- One summary note, updated in place: verdict, summary, findings outside the diff, what was fixed since the last review, and coverage, cost and the run id.
- Resolved threads when the code a finding quoted is gone.
Reviewers stay in charge. When someone with the Developer role or higher, other than the merge request's author, resolves one of ocra's threads, or answers it with /ocra dismiss or a reply that opens with "won't fix", "by design" or "false positive", the finding is dismissed. Other replies from them reach the judge the next time the finding comes back. A comment someone else edited counts for nothing.
If the project resolves outdated threads on push (Settings → Merge requests), GitLab records whoever pushed as the one who resolved a thread, so a push by anyone would count as a reviewer's resolution. In such a project, and whenever GitLab does not say how the setting stands, resolving one of ocra's threads dismisses nothing: answer it instead. ocra reads the setting on every run, and its summary says when a resolved thread did not dismiss its finding.
GitLab has no "request changes". The job's exit code is the gate: with allow_failure as above, only an incomplete review fails the pipeline. Without it, a blocking verdict fails it too, and Pipelines must succeed then blocks merging.
Overriding a blocking verdict
Someone with the Developer role or higher, other than the author, can let one commit pass with a comment of its own:
/ocra override <full head commit id> <reason>It works as on GitHub: only for that commit, only from a comment nobody else edited, and never from ocra's own summary.
What the merge request cannot change
ocra itself runs nothing from the merge request: the configuration, .ocra/rules.json and AGENTS.md are read from the commit the merge request is based on, and repository plugins never load. The pipeline that runs ocra is another matter: for a merge request from a branch of your project, its author wrote it (next section). Every line ocra posts that starts with a slash gets a zero-width space before it, so text in a finding cannot become a quick action such as /merge or /approve, which GitLab would run with the token's rights.
Who controls the pipeline
A merge request pipeline runs the .gitlab-ci.yml of the merge request's own branch, and every job in it can read the project's CI/CD variables that are not protected, GITLAB_TOKEN and your model key among them. What that means depends on where the merge request comes from:
- From a fork: the pipeline runs in the fork, without your variables, so there is nothing to take (merge requests from forks). The rules on this page hold.
- From a branch of your project: the author has the Developer role and wrote the pipeline. They can remove or change the review job, read the token and the model key, and post as ocra, including a summary that a later review would trust. For such merge requests the review is advice that its author can forge. Keep an approval rule with human approvers, and never make ocra's job the only thing between a merge request and your default branch.
What you can do today:
- Limit what the token and the key are worth. The token keeps the Developer role, so it can do nothing a developer cannot, apart from posting as ocra; give it an expiry date. Use a model key that serves only these reviews, with a spending limit at the provider.
- On GitLab Ultimate, run the job from a pipeline execution policy. The project's pipeline can then neither remove nor change it, and the policy's variables win over the project's and the pipeline's. The token is still a project variable that the project's own jobs can read, so posting as ocra remains possible.
A setup that would keep the token and the key away from developers altogether, with the review running in a separate project, is in the threat model. It has not been tried yet.
Self-managed GitLab
- In GitLab CI,
CI_API_V4_URLandCI_PROJECT_IDare set for you. Outside CI, setCI_API_V4_URL(for examplehttps://gitlab.example.com/api/v4) and pass--project <id|path>, or let ocra read the project from theoriginremote. WithoutCI_API_V4_URL, ocra uses GitLab.com, and stops whenoriginis on another host, so a token for your instance never goes to GitLab.com. It warns whenCI_API_V4_URLandoriginare on different hosts. - For an instance with a private certificate authority, point
NODE_EXTRA_CA_CERTSat its certificate. - The token needs the
apiscope there too:read_apicannot post comments.
Merge requests from forks
A pipeline for a merge request from a fork runs in the fork's project, without the parent's CI/CD variables. The review then stops and names the missing GITLAB_TOKEN. On GitLab Premium and Ultimate, a maintainer can run the pipeline in the parent project instead, after reading the change. The job then has the parent's variables, which is acceptable only because ocra runs nothing from the merge request; never add a step that does. ocra does not automate this.
From the command line
export GITLAB_TOKEN=... # a token with the api scope
ocra review --mr 42 --project group/project # review and print
ocra review --mr 42 --project group/project --publish # also post the reviewRun it from a clone of the project; ocra fetches the merge request's commits if they are missing.