* feat(examples): add Gerrit CI integration for publishing review results Adds examples/gerrit_ci following the gitflic_ci CI-glue pattern (#316): a stdlib-only post_review.py that reads 'ocr review --format json' and publishes summary, inline comments, suggestion blocks, and file-level findings in ONE batched POST /a/changes/{change}/revisions/{rev}/review, tagged autogenerated:opencodereview with notify=OWNER and omit_duplicate_comments. Decisions (validated against a live Gerrit 3.14 in Docker): - Plain comments, bare line = end_line, no CommentRange: the range form {start,0,end,0} renders lines start..end-1 in the UI (end_character 0 excludes the final line), so correct ranges would need file contents in CI. Verified via UI screenshots during E2E. - Preemptive HTTP Basic auth (urllib's handler does not preempt), XSSI )]}' stripping, HTML-200 detected as config error, 400 batch fold-retry, 409 change-closed tolerated (defensive on modern Gerrit: label-free reviews post fine on closed changes), password scrubbed from all error output. - Jenkins Gerrit Trigger as the reference integration; the script is trigger-agnostic via flags/env (Zuul/hook recipes in the README). Jenkinsfile always passes the injected patchset SHA (revision race). 45 table-driven stdlib-unittest tests, red-to-green TDD; E2E against gerritcodereview/gerrit covering live post, unicode round-trip, dedup re-run, and failure paths (401 exit 2 with scrubbed password). * fix(examples/gerrit_ci): harden post_review.py per review findings - Send Authorization via add_unredirected_header so Basic credentials cannot follow a redirect to another host (urllib forwards ordinary headers cross-host). - Reject non-object JSON input cleanly instead of an AttributeError traceback; validate --timeout > 0 at argparse time. - Warn on stderr when the 400-fallback folded summary is truncated, and report the fold accurately instead of claiming N inline comments. - Pin drafts=KEEP in ReviewInput (depot_tools convention; old servers defaulted to deleting the caller's drafts). - Jenkinsfile: fetch with an explicit dest refspec so origin/$GERRIT_BRANCH materializes under narrow-refspec clones; comment out extra_body thinking (OpenAI rejects unknown fields). - Gitflic parity: optional positional input arg; single-sourced 'current' revision default; scrub() skips sub-4-char passwords. - README: document exit 1 and the defensive 409 branch; add gerrit_ci row to all five root READMEs (parity with the GitFlic example). - Tests: 44 -> 55, covering stdin input, flag-over-env precedence, fold-retry failure, fold truncation, GERRIT_CHANGE_URL wiring, non-dict JSON, timeout validation, positional input. * fix(examples/gerrit_ci): address OCR bot review on #401 - Jenkinsfile: resolve the LLM endpoint from the OCR_LLM_URL/TOKEN/MODEL env triple instead of `ocr config set`, so the auth token stays env-only and is never written to ~/.opencodereview/config.json on a shared agent (OCR_CONFIG_PATH is deliberately ignored by write paths, so it can't redirect the leak). Pin the npm install to a validated version. Document the config-file fallback (and its cleanup) for extra_body, which has no env equivalent. - post_review.py: scrub the base64(user:password) Authorization value from error output too, not just the raw password — a proxy echoing the request header would otherwise leak decodable credentials. +1 test. * feat(examples/gerrit_ci): scoped retry + robustness polish Post-review hardening from an OSS-precedent study (depot_tools, kudu, Gerrit REST docs): - Bounded retry (3 attempts, exp backoff) in make_poster, scoped to the provably-safe failures only: HTTP 5xx and pre-response connection errors (refused/reset/DNS). Read-timeouts are deliberately NOT retried — a timeout is ambiguous (the server may have applied the review) and omit_duplicate_comments dedupes only inline comments, not the summary message, so a blind retry could post a duplicate change message. 4xx (400/401/404/409) propagate unchanged so main() classifies them as before. - Document why plain comments are used, not robot_comments: the latter is deprecated since Gerrit 3.6, disabled-by-default in 3.12, and slated for removal; the tag already marks bot origin. - Fold fallback: strip the '; N posted as inline comment(s).' clause from the reused summary so the folded message doesn't claim inline comments were posted and then explain they couldn't be placed. - README: document the retry scoping and note fix_suggestions / label voting as intentional future options. Tests: 56 -> 61 (5 retry cases: 5xx-then-ok, conn-err-then-ok, 5xx-exhaust, read-timeout-not-retried, 4xx/409-not-retried). |
||
|---|---|---|
| .. | ||
| Jenkinsfile | ||
| post_review.py | ||
| post_review_test.py | ||
| README.md | ||
OpenCodeReview - Gerrit CI Demo
This demo shows how to integrate OpenCodeReview into a Gerrit code-review flow to automatically review changes and post the findings as inline comments on the patchset, plus a summary message.
Like the GitHub Actions, GitLab CI, and GitFlic examples, the posting glue lives in the CI layer rather than in the ocr binary. Here it is a small, dependency-free Python script — post_review.py — that reads ocr review --format json and posts it as one batched ReviewInput to Gerrit's set-review endpoint, so a review lands atomically: inline comments grouped per file plus a summary message, in a single request.
How It Works
Gerrit Trigger (patchset-created) → Jenkins job → ocr review --format json → post_review.py → POST /a/changes/{change}/revisions/{revision}/review
- The Gerrit Trigger plugin fires a Jenkins job on
patchset-created - The job fetches the change ref and runs
ocr review --format json --audience agent python3 post_review.pyreads the JSON and POSTs a singleReviewInputcontaining:- Inline comments on the changed lines, grouped per file
- A summary message with the totals (plus any comments that could not be placed inline)
The script handles the Gerrit-specific wrinkles for you: preemptive HTTP basic auth on /a/ endpoints, the )]}' anti-XSSI prefix on responses, a 200 that is actually an HTML login page (treated as a configuration error), and an HTTP 400 on the batch (retried once with all inline comments folded into the summary message so findings still reach the change).
Setup
1. Create a bot account and HTTP password
Create a dedicated Gerrit account for the bot (its name appears on the review comments). Log in as that account and generate a token under Settings → HTTP Credentials → Generate Password.
Important: this HTTP password is not the account/login password — using the login password is the most common cause of HTTP 401 from
/a/endpoints.post_review.pysays so explicitly when Gerrit rejects the credentials.
2. Grant minimal permissions
The bot only needs to read changes and comment on them. In the project's (or All-Projects') access settings, grant the bot's group Read on refs/* — commenting on open changes requires nothing more. Do not grant Label: Code-Review voting rights unless you enable label voting (see Notes).
3. Configure the pipeline
Copy post_review.py into your repository (or fetch it in the job) and wire it into your Jenkins job — see the Jenkinsfile in this directory. Store GERRIT_HTTP_USER / GERRIT_HTTP_PASSWORD as Jenkins credentials, plus the LLM API token for the review step itself. ocr resolves OCR_LLM_URL / OCR_LLM_TOKEN / OCR_LLM_MODEL directly from the environment — no ocr config set needed, so the token is never written to disk on a shared agent.
Configuration Reference
Every value can be passed via flag or environment variable; flags override the environment. Run python3 post_review.py -h for the same list.
| Flag | Env fallback | Default | Description |
|---|---|---|---|
--gerrit-url |
GERRIT_URL, else derived from GERRIT_CHANGE_URL |
— | Gerrit base URL (context path kept, trailing slash tolerated) |
--change |
GERRIT_CHANGE_NUMBER |
— | Change number |
--revision |
GERRIT_PATCHSET_REVISION |
current |
Revision/patchset to comment on |
--user |
GERRIT_HTTP_USER |
— | HTTP credentials username |
--password |
GERRIT_HTTP_PASSWORD |
— | Gerrit HTTP password, not the account password |
--input |
— | - (stdin) |
Review result JSON file (- = stdin); also accepted as a positional argument, which wins if both are given |
--dry-run |
— | off | Print the ReviewInput instead of posting it |
--timeout |
— | 30 |
HTTP timeout in seconds |
GERRIT_CHANGE_URL derivation: modern change URLs ({base}/c/{project}/+/{number}) split at /c/, which keeps any context path; legacy URLs ({base}/{number}) drop the trailing number.
Usage
Jenkins (Gerrit Trigger)
See the Jenkinsfile in this directory. The Gerrit Trigger plugin injects GERRIT_CHANGE_NUMBER, GERRIT_PATCHSET_REVISION, and GERRIT_CHANGE_URL into the build environment, so the script needs no positional wiring:
ocr review --format json --audience agent | python3 post_review.py
Other triggers
The script is trigger-agnostic: anything that can set the environment variables above (or pass the flags) can drive it.
-
Zuul — the same env contract can be set in a Zuul job from
zuul.change/zuul.patchsetvariables; the script does not care who exported them. -
patchset-createdhook — post directly from a server-side hook:ocr review --format json | python3 post_review.py \ --gerrit-url https://gerrit.example.com --change "$CHANGE" --revision "$REVISION"
Dry Run
Test the posting step locally without touching the change (no credentials required):
ocr review --from origin/main --to HEAD --format json > /tmp/r.json
python3 post_review.py /tmp/r.json --dry-run # or: --input /tmp/r.json
--dry-run prints the exact ReviewInput that would be POSTed:
{
"message": "OpenCodeReview found 2 issue(s) in 1 file(s) reviewed; 2 posted as inline comment(s).",
"tag": "autogenerated:opencodereview",
"notify": "OWNER",
"omit_duplicate_comments": true,
"comments": {
"internal/scan/scan.go": [
{ "message": "**Severity:** high · ...", "unresolved": true, "line": 42 },
...
Notes & Limitations
- Re-reviews and duplicates — the ReviewInput sets
omit_duplicate_comments, but Gerrit only drops byte-identical comments at the same location. A fresh LLM run usually rewords its findings, so re-triggered reviews will repeat comments. All bot comments carry the tagautogenerated:opencodereview, so UIs and scripts can filter or collapse them. - Retries — the POST is retried (up to
MAX_ATTEMPTS, with exponential backoff) only on failures that provably did not apply the review: HTTP 5xx and pre-response connection errors (refused/reset/DNS). Read timeouts are not retried — they are ambiguous (the server may have applied the review) andomit_duplicate_commentsdoes not dedupe the summarymessage, so a retry could post a duplicate change message. 4xx responses (400/401/404/409) are not retried; they are classified and handled directly. - Native fix suggestions and label voting — intentional future options.
fix_suggestions(Gerrit's native apply-a-fix) is experiment-gated in Gerrit 3.10+ and needs character-level ranges this example does not carry, so suggested code is emitted under a**Suggestion:**heading in a plain fenced code block, which renders portably on any Gerrit version. Label voting (Code-Review ±1) is likewise left out of v1 — see Label voting below. - Label voting — deliberately not in v1: the script comments, it does not vote. Teams that want gating can add one line to the ReviewInput in
build_review_input:"labels": {"Code-Review": -1}(and grant the bot the label permission). - Revision race — always pass the trigger-injected revision SHA (
GERRIT_PATCHSET_REVISION). Thecurrentdefault is for manual runs: if a new patchset lands mid-pipeline,currentwould retarget the comments onto code the review never saw. - Exit codes —
0on success (HTTP 409 change-closed is tolerated and also exits 0);1when the input JSON is unreadable or unparsable;2on configuration or HTTP errors, so the CI step fails visibly. The 409 handling is defensive: modern Gerrit (verified on 3.14) accepts label-free reviews even on abandoned changes, so it matters mainly for older/stricter servers.
Tests
post_review.py ships with post_review_test.py — standard-library unittest, no network or git required:
cd examples/gerrit_ci
python3 post_review_test.py