|
Some checks are pending
CI / test (push) Waiting to run
CI / cross-compile (amd64, darwin) (push) Waiting to run
CI / cross-compile (amd64, windows) (push) Waiting to run
CI / cross-compile (arm64, darwin) (push) Waiting to run
CI / cross-compile (arm64, linux) (push) Waiting to run
CI / cross-compile (arm64, windows) (push) Waiting to run
* refactor(examples): extract GitLab CI heredoc into post_review.py with unit tests Extracts the ~270-line inline heredoc from .gitlab-ci.yml into a standalone, testable post_review.py module, matching the publish() + make_poster() pattern established by gerrit_ci/ and gitflic_ci/. Key design decisions (from spec issue #1 and wayfinder tickets #3, #4, #5): - publish(result, diff_refs, post, config, sleep) — transport-agnostic - make_poster(api_base, token, auth_header, config) — GitLab REST transport - fetch_diff_refs(api_base, token, auth_header, config) — /versions GET with retry - Single config dict built by main() from env vars; no module-level config state - post() returns {success, rate_limit_remaining, is_rate_limit_exhausted} to preserve failure-pacing behavior (rate-limit vs non-rate-limit delays) - _sleep = time.sleep module-level pattern for testability All existing heredoc behavior preserved 1:1: - GitLab suggestion:-0+0 syntax and <details> fallback format - Retry on 429/403-rate-limit/5xx/408 with exponential backoff + ±25% jitter - Retry-After header honoring, MAX_RETRY_DELAY cap - Proactive RateLimit-Remaining throttling (success path only) - Failure pacing: rate-limit-exhausted → SUCCESS_DELAY, other → FAILURE_DELAY - PRIVATE-TOKEN vs JOB-TOKEN auth selection - Inline → fallback → summary ordering - Parse failure → post stderr as error note - All 6 env vars (OCR_RETRY_BASE_DELAY, OCR_MAX_RETRIES, OCR_MAX_RETRY_DELAY, OCR_SUCCESS_DELAY, OCR_FAILURE_DELAY, OCR_RATE_LIMIT_THRESHOLD) 48 unit tests (stdlib unittest, no network, no real time.sleep): - Seam 1: publish() with Recorder fake poster — inline/fallback/summary flow, proactive throttling, failure pacing - Seam 2: make_poster() with mocked urlopen + _sleep — retry/backoff/jitter, Retry-After, delay cap, auth headers, is_rate_limit_exhausted classification - fetch_diff_refs() with mocked urlopen — success/failure/retry - build_config() defaults and env overrides - Dry-run poster — no HTTP calls Implements #534. * fix(examples): address code review findings on gitlab_ci post_review - Add missing-required check for CI_PROJECT_ID and CI_MERGE_REQUEST_IID in main(), matching gerrit_ci/gitflic_ci pattern. The heredoc used os.environ[...] (KeyError on missing); the extraction silently used env.get(..., "") which constructs a malformed API URL. Now fails fast with a clear error message. - Change transient_base_delay from int 2 to float 2.0 to match spec config-dict type annotation. - Add 5 end-to-end tests for main()'s auth-header env resolution: PRIVATE-TOKEN when GITLAB_API_TOKEN set, JOB-TOKEN when only CI_JOB_TOKEN set, PRIVATE-TOKEN wins when both set, missing CI vars fails fast, missing token fails fast. Addresses review findings: #3 (TP, medium), #4 (Edge), #5 (TP, low). * fix(examples): handle URLError in gitlab_ci post_review retry logic _api_request_with_retry only caught HTTPError, not URLError. Network-layer failures (DNS resolution failure, connection refused, connection reset) raised URLError which propagated uncaught, crashing the script and losing all pending review comments. The original heredoc had the same gap, but the gerrit_ci sibling already handles this correctly (lines 250-259: retry on connection errors, propagate timeouts). This fix follows the gerrit_ci pattern adapted to our return-dict contract: - Add 'except urllib.error.URLError' handler after HTTPError handler - Timeout (socket.timeout/TimeoutError): return failure dict, don't retry (ambiguous — server may have processed the request) - Connection errors (DNS, refused, reset): retry with transient_base_delay backoff + ±25% jitter, same as 5xx/408 handling - Exhaustion: return failure dict with is_rate_limit_exhausted=False 3 new tests: - test_retry_urlerror_then_success: ConnectionRefused → retry → success - test_urlerror_exhausts_retries: 4 ConnectionRefused → failure after 4 attempts - test_urlerror_timeout_not_retried: socket.timeout → immediate failure, no retry Found by OCR (open-code-review) AI code review. * fix(examples): handle non-UTF-8 HTTP error bodies in gitlab_ci post_review e.read().decode('utf-8') raises UnicodeDecodeError when the GitLab server returns a non-UTF-8 error body (e.g., an HTML error page in latin-1 from a misconfigured proxy or load balancer). This exception propagated uncaught, crashing the entire posting loop — no further inline comments, fallback notes, or summary notes would be posted. Both gerrt_ci (line 242: decode('utf-8', 'replace')) and gitflic_ci (line 344: decode('utf-8', 'replace')) siblings already handle this correctly. The original heredoc had the same gap. Fix: add errors='replace' to both decode() calls (success path line 248 + error path line 260). For valid UTF-8 input (the normal case), behavior is identical. The error body is only used for keyword matching and logging, both of which work fine with replacement characters (U+FFFD). 1 new test: - test_non_utf8_error_body_does_not_crash: HTTPError with invalid UTF-8 body → no crash, returns failure dict Found by OCR (open-code-review) AI code review on PR #539. |
||
|---|---|---|
| .. | ||
| .gitlab-ci.yml | ||
| post_review.py | ||
| post_review_test.py | ||
| README.md | ||
OpenCodeReview - GitLab CI Demo
This demo shows how to integrate OpenCodeReview into your GitLab CI/CD pipeline to automatically review Merge Requests and post review comments as inline discussions.
How It Works
MR Created/Updated → GitLab Pipeline Triggered → OCR Reviews Diff → Discussions Posted on MR
- When a Merge Request is opened or updated, the pipeline triggers
- It installs OCR via npm in a
node:20Docker image - Runs
ocr review --from origin/<target> --to <commit_sha> --format json --audience agentto analyze the diff (uses commit SHA to support fork MRs) - Parses the JSON output and posts inline discussions on the MR using GitLab's Discussions API
Setup
1. Copy the pipeline and script files
Copy both .gitlab-ci.yml and post_review.py to your repository root (or a subdirectory — adjust the python3 post_review.py path in the YAML if you place the script elsewhere):
cp .gitlab-ci.yml post_review.py /path/to/your/repo/
Or use GitLab's include feature in your existing .gitlab-ci.yml (the script path is relative to the pipeline file's location):
include:
- local: 'ci_demo/gitlab_ci/.gitlab-ci.yml'
2. Configure CI/CD Variables
Go to your project's Settings → CI/CD → Variables and add:
| Variable | Required | Masked | Description |
|---|---|---|---|
OCR_LLM_URL |
Yes | No | LLM API endpoint URL (e.g., https://api.openai.com/v1/chat/completions) |
OCR_LLM_AUTH_TOKEN |
Yes | Yes | API authentication token |
OCR_LLM_MODEL |
Yes | No | Model name (e.g., gpt-4o) — OCR has no built-in default model and fails when this is unset |
GITLAB_API_TOKEN |
No | Yes | GitLab access token with api scope (falls back to CI_JOB_TOKEN if not set) |
Note: GitLab CI/CD does not support variables with values shorter than 8 characters, so
use_anthropiccannot be set as a CI variable. The pipeline sets it tofalseby default. If you need to use Anthropic Claude models, you'll need to modify the.gitlab-ci.ymlscript directly.The pipeline also configures
llm.extra_bodyto disable thinking mode for compatibility with various LLM providers.
3. Create a GitLab Access Token
You need a token with api scope to post discussions on MRs. Options:
- Project Access Token (recommended): Settings → Access Tokens → Create with
apiscope - Personal Access Token: User Settings → Access Tokens → Create with
apiscope - Group Access Token: For organization-wide usage
Note: The built-in
CI_JOB_TOKENhas limited API scope and may not support all discussion features (e.g., creating new threads on older GitLab versions). IfGITLAB_API_TOKENis not set, the pipeline falls back toCI_JOB_TOKENautomatically — but for best results, a dedicated token withapiscope is recommended.Tip: For Project Access Tokens and Group Access Tokens, the token name determines the bot name shown in MR discussions. For example, naming your token
OpenCodeReview Botwill make review comments appear as posted byOpenCodeReview Bot.
Example Output
When an MR is reviewed, comments appear as:
- Inline discussions: Directly on the changed lines in the MR diff view
- Summary note: A final note summarizing the total number of issues found
- Fallback notes: If inline posting fails for specific comments, they appear as regular MR notes with file/line references
Inline Discussion Example
Comments are posted using GitLab's Discussion API with position data, so they appear directly next to the relevant code in the "Changes" tab.
Supported LLM Providers
OCR supports both OpenAI and Anthropic API formats:
- OpenAI-compatible APIs (default):
- OpenAI (GPT-4o, GPT-4, etc.)
- Azure OpenAI
- Self-hosted models (vLLM, Ollama, etc.)
- Anthropic APIs (modify
.gitlab-ci.ymlto setuse_anthropic: true):- Anthropic Claude models
Customization
Use a specific OCR version
script:
- npm install -g @alibaba-group/open-code-review@1.0.0
Add custom review rules
Use the --rule flag to pass a custom rules JSON file:
script:
- ocr review --rule ./my-rules.json --from origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to $CI_COMMIT_SHA
Adjust retry and delay settings
When posting review discussions, the script includes rate-limit handling with exponential backoff (with jitter), Retry-After header support, and proactive throttling based on GitLab's RateLimit-Remaining response header. All API requests — including summary notes and MR version fetches — use the same retry logic. See GitLab Rate Limits for details on GitLab's rate-limiting policies and recommended handling. You can configure the retry and delay behavior via CI/CD Variables (Settings → CI/CD → Variables):
| Variable | Default | Description |
|---|---|---|
OCR_RETRY_BASE_DELAY |
2000 |
Base delay (ms) for exponential backoff when a rate-limit error is hit |
OCR_MAX_RETRIES |
3 |
Maximum retry attempts per discussion when rate-limited |
OCR_MAX_RETRY_DELAY |
60000 |
Maximum delay (ms) per single retry, caps both Retry-After and backoff |
OCR_SUCCESS_DELAY |
2000 |
Delay (ms) after a successful discussion post to pace subsequent requests |
OCR_FAILURE_DELAY |
1000 |
Delay (ms) after a non-rate-limit failure to pace subsequent requests |
OCR_RATE_LIMIT_THRESHOLD |
10 |
Proactively slow down when GitLab RateLimit-Remaining is at/below this value (set 0 to disable) |
These variables are optional — if not configured, sensible defaults are used. Consider increasing delays for self-hosted GitLab instances with aggressive rate-limit configurations or for large MRs that generate numerous review comments. The OCR_RATE_LIMIT_THRESHOLD variable enables proactive throttling: when GitLab reports low remaining quota in the RateLimit-Remaining response header, the script automatically doubles the pacing delay to avoid hitting 429 errors.
Limit concurrency
Adjust the --concurrency flag for large MRs to control the number of concurrent LLM requests:
script:
- ocr review --concurrency 5 --from origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to $CI_COMMIT_SHA
Provide background context
Use the --background flag to pass additional context that helps OCR better understand the purpose of the changes:
script:
- ocr review --background "$CI_MERGE_REQUEST_TITLE" --from origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to $CI_COMMIT_SHA
This is particularly useful when your MR titles follow semantic conventions (e.g., feat(auth): add OAuth2 support) that clearly summarize what the MR implements. The background information helps OCR provide more relevant and context-aware review comments.
Change the trigger events
By default, the pipeline uses only: [merge_requests], which triggers on all MR events (creation, updates, reopen). GitLab CI does not natively support fine-grained control to trigger only on MR creation.
To avoid re-reviewing on every push to an existing MR (and wasting LLM API tokens), you can check for existing OCR reviews before running ocr review. Use a wrapper script that skips the review step if OCR comments already exist:
script:
# Install OpenCodeReview
- npm install -g @alibaba-group/open-code-review
# Configure OCR
- |
: "${OCR_LLM_URL:?set OCR_LLM_URL in Settings -> CI/CD -> Variables}"
: "${OCR_LLM_AUTH_TOKEN:?set OCR_LLM_AUTH_TOKEN in Settings -> CI/CD -> Variables}"
: "${OCR_LLM_MODEL:?set OCR_LLM_MODEL in Settings -> CI/CD -> Variables}"
ocr config set llm.url "$OCR_LLM_URL"
ocr config set llm.auth_token "$OCR_LLM_AUTH_TOKEN"
ocr config set llm.model "$OCR_LLM_MODEL"
ocr config set llm.use_anthropic false
ocr config set llm.extra_body '{"thinking": {"type": "disabled"}}'
# Check for existing OCR reviews and run review only if not found
- |
python3 << 'WRAPPER_SCRIPT'
import json
import os
import subprocess
import sys
import urllib.request
GITLAB_URL = os.environ.get("CI_SERVER_URL", "https://gitlab.com")
PROJECT_ID = os.environ["CI_PROJECT_ID"]
MR_IID = os.environ["CI_MERGE_REQUEST_IID"]
API_TOKEN = os.environ["GITLAB_API_TOKEN"]
SOURCE_BRANCH = os.environ["CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"]
TARGET_BRANCH = os.environ["CI_MERGE_REQUEST_TARGET_BRANCH_NAME"]
# Check for existing OCR reviews
url = f"{GITLAB_URL}/api/v4/projects/{PROJECT_ID}/merge_requests/{MR_IID}/notes?per_page=100"
req = urllib.request.Request(url, headers={"PRIVATE-TOKEN": API_TOKEN})
with urllib.request.urlopen(req) as resp:
notes = json.loads(resp.read().decode("utf-8"))
for note in notes:
if "OpenCodeReview" in note.get("body", ""):
print("⏭️ OCR has already reviewed this MR. Skipping to save tokens.")
print("Delete previous OCR comments to re-trigger review.")
sys.exit(0)
# No existing review found - run OCR
print("🔍 No existing OCR review found. Running review...")
COMMIT_SHA = os.environ["CI_COMMIT_SHA"]
result = subprocess.run([
"ocr", "review",
"--from", f"origin/{TARGET_BRANCH}",
"--to", COMMIT_SHA,
"--format", "json",
"--audience", "agent"
], capture_output=True, text=True)
# Save output for the posting script
with open("/tmp/ocr-result.json", "w") as f:
f.write(result.stdout)
with open("/tmp/ocr-stderr.log", "w") as f:
f.write(result.stderr)
print("OCR review completed.")
WRAPPER_SCRIPT
# Post review comments to MR
- python3 post_review.py /tmp/ocr-result.json
The key logic: the Python wrapper checks for existing OCR comments before running ocr review. If found, it exits early with sys.exit(0) before consuming any LLM tokens. To re-trigger a review, users can manually delete the previous OCR comments.
Self-hosted GitLab
The script automatically uses CI_SERVER_URL to determine the GitLab API base URL, so it works with self-hosted GitLab instances out of the box.
Use a Service Account as Review Bot
By default, review comments are posted using the user who owns the access token configured in GITLAB_API_TOKEN. You can create a dedicated service account bot to post reviews with a custom identity, making it easier to distinguish automated reviews from human comments.
For more details about GitLab service accounts, see the GitLab Service Accounts documentation.
Step 1: Create a Service Account
Create a service account in your project:
- Go to your Project → Settings → Service Accounts
- Click New service account
- Fill in the following:
- Name: e.g.,
OpenCodeReview Bot(this will be the bot name shown in MR discussions) - Username: Will be auto-generated based on the name
- Name: e.g.,
- Click Create service account
Step 2: Invite the Service Account to Your Project
After the service account is created, invite it to your project with appropriate permissions:
- Go to your Project → Settings → Members
- Click Invite member
- Search for the service account by name (e.g.,
OpenCodeReview Bot) - Select the service account and assign a role (
DeveloperorMaintainerrequired for posting discussions) - Click Invite
Step 3: Create an Access Token
Generate an access token for the service account:
- Go to your Project → Settings → Service Accounts
- Click on the service account to view its details
- Click Add new token
- Configure the token:
- Name: e.g.,
ocr-review-token - Expiration: As needed
- Scope: Select
api(required for Discussions API)
- Name: e.g.,
- Click Create token and copy the token value
Step 4: Update CI/CD Variables
Update the GITLAB_API_TOKEN variable in your project's CI/CD settings:
Go to Settings → CI/CD → Variables and update GITLAB_API_TOKEN with the service account's token.
Now review comments will be posted with your service account identity (e.g., OpenCodeReview Bot), providing a clear and professional appearance for automated code reviews.
Troubleshooting
Common Issues
- "API error 403": The
GITLAB_API_TOKENlacksapiscope or doesn't have access to the project - "Failed to parse OCR output": Check that
OCR_LLM_URLandOCR_LLM_AUTH_TOKENvariables are correctly set - "Cannot find merge-base": Ensure
GIT_DEPTH: 0is set (full clone) - Inline comments on wrong lines: GitLab requires exact SHA matching; the script fetches MR version metadata to get correct diff refs
Debugging
Add verbose output to the review step:
script:
- cat /tmp/ocr-result.json
- cat /tmp/ocr-stderr.log
Testing
The posting logic (retry, backoff, rate-limit throttling, inline→fallback→summary flow) is unit-tested with no network access and no wall-clock sleep cost. Tests use only the standard-library unittest.
# From the example directory
cd examples/gitlab_ci
python3 post_review_test.py
# From the repo root
python3 -m unittest discover -s examples/gitlab_ci -p '*_test.py'