This guide is for developers and CI operators who connect Open Code Review Toolkit to a GitLab merge-request pipeline and need to understand what happens after the first review. Installation remains in gitlab.md; the complete environment contract is in configuration.md.
The toolkit reads the previous OCR-owned notes and discussions before it writes anything. It fingerprints new findings, removes findings already owned or suppressed by reviewers, and publishes:
- inline GitLab discussions when a finding has a valid diff position;
- bounded fallback notes when GitLab cannot accept a position, for example after relevant lines moved outside the current diff;
- one
## Open Code Reviewnote whose single bold outcome line combines review health with the published, omitted, or reviewer-suppressed finding state; incomplete coverage and warnings remain explicit below it, with operational posting, commit, token/tool, and used-MCP metadata under a collapsed technical-details disclosure.
Category and severity belong to each individual finding, not to the review
outcome line. They use private-safe text labels by default. Operators may opt in
to static Shields.io images with OCR_POST_BADGES=shields; only closed,
normalized OCR enums enter the fixed-host URL and the same label remains as alt
text. If only one field is present, the image path uses an explicit category
or severity label rather than an ambiguous value-only badge. Unknown metadata never becomes a URL. Because rendering can contact an
external image service directly or through a GitLab proxy, keep text mode for
installations that must avoid that disclosure or dependency.
An actionable GitLab suggestion is stricter than an ordinary finding. The
toolkit reads the exact reviewed head blob and requires existing_code to
match the stated inclusive line range before it renders a replacement fence.
For this comparison CRLF and CR are normalized to LF and one optional terminal
newline is ignored. The replacement must describe one contiguous edit: a
synthetic ellipsis bridge, unified-diff-prefixed text, unsafe Markdown fence,
quick action, invalid range, or unavailable source suppresses only the
actionable fence. The explanatory finding remains visible with a bounded reason
that does not reproduce repository content. Exact no-op suggestions are also
suppressed.
OCR_MAX_POST_COMMENTS limits individually published findings. The default is 50 and the hard limit is 200. Omitted findings are counted in the summary rather than silently disappearing.
OCR_MAX_TOKENS_BUDGET can set an aggregate input-plus-output token ceiling for the OCR diff review. The default 0 is unlimited. A positive ceiling is approximate rather than a hard billing cutoff because already-running work may complete; when it stops further dispatch, completed findings remain publishable and unreviewed files stay explicit as budget-attributed failed coverage. Such a run is partial and cannot automatically approve.
The outcome wording distinguishes skipped, complete, complete-with-warnings, incomplete, token-budget, and failed reviews while preserving the finding state in that same line. A complete clean review is visibly positive; a complete review with findings or only reviewer-suppressed findings is neutral; warning, partial, budget, and failed states never look clean. Findings withheld by the posting limit remain counted even when the limit allows no individual finding note. OCR 1.8.5 and later manifest failures provide the canonical failed-file receipt; legacy warnings are a bounded fallback, and summary.files_reviewed is never treated as proof of successful coverage. Zero-valued counters and configured-but-unused MCP servers are omitted. Status and aggregate semantic-category emoji are enabled by default and can be disabled together with OCR_POST_EMOJI=false; finding labels remain text unless their separate badge mode is enabled.
OCR_AUTO_APPROVE=true is the default. Approval is a separate transaction only
after every current review note publishes. A review is eligible only with a supported review-time approval receipt, a supported complete manifest, no warnings, failures, waivers, token-budget stop, or omitted findings, and at most three findings. Receipt v2 makes a run ineligible whenever mutable author-controlled MR title, description, labels, or source-branch context was admitted; comments and summaries still publish normally. Historical receipt v1 remains readable, but ordinary current reviews emit receipt v2. Every finding must have
severity exactly low and category exactly style, documentation, or
maintainability. A complete zero-finding review is eligible. Four findings,
malformed metadata, or any other severity/category are not eligible.
Before writing, the toolkit repeatedly reads the MR and its bounded diff-version
list. It requires an open MR, a current head equal to the reviewed 40-character
SHA, detailed_merge_status outside checking and approvals_syncing, and a
non-null patch_id_sha. It then passes that exact SHA to GitLab's approve API
and confirms the authenticated user in approval readback. A moved head is a
normal skipped result and is never retried against the new commit.
Approve and summary-update writes are not retried after timeout, connection loss, 5xx, or another ambiguous response. GitLab remains authoritative for eligible approvers, required groups, Code Owners, protected-branch rules, and password or SAML reauthentication. A rejected or failed approval never rolls back the already published advisory review.
The summary records exactly one bounded state: approved, not eligible,
disabled, skipped, or failed. With advisory OCR_STRICT_POSTING=false, an
approval-management failure leaves the published review successful but visibly
failed; with OCR_STRICT_POSTING=true, it also returns a nonzero exit code.
The transaction is deliberately add-only because GitLab's unapprove endpoint cannot bind removal to an immutable reviewed SHA at mutation time. The toolkit therefore never removes an existing approval, even when the authenticated bot user approved earlier. Ineligible, partial, skipped, legacy, and disabled runs do not make an approval write. Configure GitLab's project-owned reset or invalidation policy when approvals must be withdrawn after new commits. Human discussion replies remain ownership boundaries for notes but do not independently block approval.
flowchart LR
finding[OCR finding] --> position{Valid diff position?}
position -- No --> fallback[Fallback MR note]
position -- Yes --> open[Open OCR discussion]
open --> action{Reviewer action before rerun}
action -- No action --> replace[Replace after successful rerun]
replace --> open
action -- Human reply --> owned[Human-owned and suppressed]
action -- Resolve in GitLab --> resolved[Resolved and suppressed]
action -- OCR command --> command{Command}
command -- suppress --> suppressed[Open and suppressed]
command -- resolve --> pending[Resolve requested]
pending -- Publish succeeds --> resolved
If posting fails, the transition does not complete: the previous review and every human-owned, suppressed, or resolve-requested discussion keep their prior state. Matching findings remain suppressed on later runs.
An untouched open OCR discussion is bot-owned. A successful rerun replaces bot-owned notes with the current review instead of accumulating stale copies. Once a person replies, the discussion becomes human-owned: the toolkit preserves the complete conversation and suppresses a finding at the recorded inline position or with a compatible fingerprint.
A discussion resolved with GitLab's normal Resolve action is also preserved and suppresses a matching future finding. The toolkit never reopens a discussion.
Reply inside an OCR-created discussion with exactly one command. Commands are case-insensitive, but the whole reply must contain only the command and optional surrounding whitespace. A command inside prose or a code block is ignored. If reviewers post several recognized commands, the newest one wins. Bot and GitLab system notes cannot issue commands.
| Command | Discussion after the command | Matching finding on future runs |
|---|---|---|
/ocr suppress |
Remains open | Suppressed |
/ocr resolve |
Resolved after the next successful posting transaction | Suppressed |
| Ordinary human reply | Remains in its current state and becomes human-owned | Matching position or fingerprint suppressed |
| GitLab Resolve action | Resolved | Suppressed |
The command is applied when the next pipeline reads the discussion. /ocr resolve waits until all notes created for the current review have published successfully before resolving the old discussion. A failed run therefore does not close it prematurely.
/ocr keep and /ocr skip were removed in 0.2.0 and are not aliases. An existing reply containing an old command still counts as an ordinary human reply: its conversation is preserved and matching future findings remain suppressed, but it does not request automatic resolution.
Every OCR note contains an invisible toolkit marker and a stable finding fingerprint. The current fingerprint combines the repository path, normalized finding text, and the existing-code fragment when OCR supplies one. This lets suppression survive an ordinary line shift. Backward-compatible fingerprints keep review decisions made by earlier toolkit versions usable.
Suppression checks both the recorded inline position and compatible fingerprints. A new finding anchored to the same recorded path and line is suppressed even if its text changes; elsewhere, the fingerprint prevents ordinary line movement from bypassing the decision. Suppression is intentionally not a permanent rule for an entire file: a materially different explanation, code fragment, path, or duplicate occurrence at another location can become a new finding and receive a new discussion. When identical findings occur more than once, occurrence-aware fingerprints prevent suppressing every occurrence after a reviewer acts on only one of them.
OCR_POST_MODE=draft is the safe default. The toolkit creates this run's notes as GitLab draft notes, publishes only those drafts one by one, and removes replaceable notes from the previous successful review only after every publish succeeds. If creation fails, drafts from the current attempt are removed and the previous review remains visible. Draft mode avoids exposing an incomplete review during the creation phase, but GitLab does not provide an atomic bulk-publish transaction.
OCR_POST_MODE=direct writes notes immediately. It exists as an emergency compatibility override. The toolkit still performs best-effort rollback, but an ambiguous network timeout can mean GitLab accepted a write that the runner cannot confirm. Prefer draft for normal CI.
OCR_STRICT_POSTING=false is the advisory default: an OCR, posting, or approval-management error remains visible in the job log and, when possible, in an MR note, but the posting helper exits successfully. Set OCR_STRICT_POSTING=true when OCR review is a required merge gate so OCR failures, an unavailable GitLab API, an unsafe previous-state snapshot, an invalid OCR result, failed publication, or failed approval management make the job fail.
Run OCR through ocr-ci review --result PATH --stderr PATH -- .... This wrapper does not post to GitLab: it creates private artifacts and, on failure, prints only a bounded redacted stderr excerpt to the runner log. Pass the paths and captured exit code to ocr-ci post afterward. Set OCR_POST_ERROR_DETAILS=1 only when that safe excerpt should also appear in the merge-request failure note.
Use a dedicated project access token with api scope and at least the Developer role. Store it in GITLAB_API_TOKEN. The toolkit needs to read merge-request notes, discussions, diff refs, approval state, and the current token identity; create and delete its own notes or drafts; publish drafts; resolve discussions requested by reviewers; and, unless opted out, approve as that dedicated identity. GitLab must separately consider the identity eligible under the project's approval rules.
The toolkit calls GET /user before posting and refuses to write if it cannot identify the token owner. It treats a note as bot-owned only when both the invisible OCR marker and the actual GitLab author ID match. Text that merely imitates an OCR marker is not enough to claim or delete another user's note.
Before a rerun, the toolkit takes a bounded snapshot of OCR-owned notes, discussions, and drafts. If it cannot collect that state reliably, it refuses to publish a replacement so resolved, suppressed, and human-owned decisions are not lost. Reads and writes have bounded response sizes and timeouts; writes are retried only when retrying is safe.
The previous review is deleted only after the new review has been created and, in draft mode, published. A definite write failure rolls back notes known to belong to the current attempt. An ambiguous draft-publish failure does not delete possibly published notes because the runner cannot prove which writes GitLab accepted.
When GitLab rejects an inline position as invalid, the toolkit moves that finding into one or more bounded fallback notes. Ambiguous write failures do not use fallback, because doing so could duplicate a discussion that GitLab already accepted.
Source and base merge-request SHAs define the reviewed range. A merge-result commit is not treated as the source branch head. The summary records the reviewed SHA and warns when the current MR head has moved, so reviewers can distinguish a current review from a stale pipeline result.