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; --publish posts a summary note, updated in place, and a thread on each finding in the diff. Later pipelines review only what changed. Who may override or dismiss, which earlier state counts and what text may be posted follow the rules for GitHub pull requests, checked by the same tests on both platforms.

GitLab support is new in 0.2.0. Beyond 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 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 counting for nothing. A dismissal by another member has not been tried live yet.

Set up

  1. Create a token for ocra with the api scope 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, which has no project access tokens, a personal access token of a dedicated account (for example ocra-review-bot, a Developer on the project). Not your own: ocra would post as you, and since a merge request's author never counts as a maintainer, you would lose your dismissals and overrides on your own merge requests.

    ocra posts notes and threads, resolves its threads, and reads through GraphQL who last edited a comment. CI_JOB_TOKEN cannot post comments.

  2. Add it as a masked CI/CD variable GITLAB_TOKEN. Leave it unprotected, or pipelines for merge requests from unprotected branches cannot read it. Every pipeline of the project can then read it: keep the role at Developer and set an expiry date (who controls the pipeline).

  3. Add your model key the same way, for example GEMINI_API_KEY: one that serves only these reviews, with a spending limit at the provider.

  4. 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.7.0
    - ocra review --mr "$CI_MERGE_REQUEST_IID" --publish

Or use the container image, with no install step. Pin it by digest (docker buildx imagetools inspect ghcr.io/jma49/ocra:0.7.0 prints it) and check its provenance (Stability):

ocra-review:
  image:
    name: ghcr.io/jma49/ocra:0.7.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" --publish
  • GIT_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, a new push cancels the previous review; one cancelled before it publishes posts nothing.
  • --ignore-scripts keeps install scripts from running with your variables; ocra finds the OpenCode binary without them.
  • Models come from the target branch's .ocra/config.json or OCRA_MODEL_* variables. Other options, such as --max-cost-usd 2 or --reviewers correctness,security, go on the ocra review line.

What gets posted

  • A thread on each finding on an added line, or an unchanged line next to the change. A finding GitLab will not place stays in the summary.
  • One summary note, updated in place, with the same content as on GitHub.
  • Resolved threads when the code a finding quoted is gone.
  • Suggestions you can apply (planned: no reviewer produces fixes yet, ADR-0029): a finding's fix ends its thread with a suggestion:-N+0 block from the thread's line back to the first fixed line (Apply suggestion), only when the thread covers exactly the fixed lines, at most 100 lines above it, and with the code exactly as written or not at all, as on GitHub.

Maintainers stay in charge. A maintainer here is anyone with the Developer role or higher other than the author, not only GitLab's Maintainer role; they dismiss a finding by resolving its thread or answering with /ocra dismiss or a reply opening with "won't fix", "by design" or "false positive". Their other replies reach the judge when 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 resolver, so any push would count as a dismissal. There, and whenever GitLab does not report the setting, resolving ocra's thread dismisses nothing: answer it instead. ocra reads the setting on every run, and the 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 does 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:

/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 runs nothing from the merge request: configuration, .ocra/rules.json and AGENTS.md come from its base commit, and repository plugins never load. Every posted line that starts with a slash gets a zero-width space before it, so a finding cannot become a quick action such as /merge or /approve, which GitLab would run with the token's rights. The pipeline that runs ocra is another matter (next section).

Who controls the pipeline

A merge request pipeline runs the .gitlab-ci.yml of the merge request's branch, and every job in it can read the project's unprotected CI/CD variables, GITLAB_TOKEN and your model key among them.

  • From a fork: the pipeline runs in the fork, without your variables (merge requests from forks). The rules on this page hold.
  • From a branch of your project: the author, a Developer, 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 a later review would trust. For such merge requests the review is advice its author can forge: keep an approval rule with human approvers, and never make ocra's job the only gate to your default branch.

What you can do today:

  • Limit what the token and key are worth. At the Developer role the token 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.
  • 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. The token is still a project variable its jobs can read, so posting as ocra remains possible.

An untried design that keeps the token and key from developers altogether, with the review in a separate project, is in the threat model.

Self-managed GitLab

  • In GitLab CI, CI_API_V4_URL and CI_PROJECT_ID are set for you. Elsewhere, set CI_API_V4_URL (for example https://gitlab.example.com/api/v4) and pass --project <id|path> or let ocra read it from the origin remote. Without CI_API_V4_URL ocra uses GitLab.com, and stops when origin is on another host, so your instance's token never goes to GitLab.com. It warns when CI_API_V4_URL and origin are on different hosts.
  • For a private certificate authority, point NODE_EXTRA_CA_CERTS at its certificate.
  • The token needs the api scope: read_api cannot post comments.

Merge requests from forks

A fork's merge request pipeline runs in the fork, without the parent's variables, so the review stops naming the missing GITLAB_TOKEN. On GitLab Premium and Ultimate, a maintainer can run the pipeline in the parent project after reading the change; the job then has the parent's variables, which is acceptable only because ocra runs nothing from the merge request, so 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 review

Run it from a clone of the project; ocra fetches missing merge request commits.

Edit on GitHub

On this page