Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.ja-JP.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ JSON出力ではこの2つのフィールドは`content`や`start_line`などと
- [`github_actions/`](./examples/github_actions/) — GitHub Actions統合の例
- [`gitlab_ci/`](./examples/gitlab_ci/) — GitLab CI統合の例
- [`gitflic_ci/`](./examples/gitflic_ci/) — GitFlic CI統合の例
- [`gerrit_ci/`](./examples/gerrit_ci/) — Gerrit (Jenkins / Gerrit Trigger) 統合の例

#### GitHub Action

Expand Down
1 change: 1 addition & 0 deletions README.ko-KR.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ JSON 출력에서 두 field는 `content`, `start_line` 등과 같은 수준의 s
- [`github_actions/`](./examples/github_actions/): GitHub Actions 통합 예시
- [`gitlab_ci/`](./examples/gitlab_ci/): GitLab CI 통합 예시
- [`gitflic_ci/`](./examples/gitflic_ci/): GitFlic CI 통합 예시
- [`gerrit_ci/`](./examples/gerrit_ci/): Gerrit (Jenkins / Gerrit Trigger) 통합 예시

#### GitHub Action

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ See the [`examples/`](./examples/) directory for integration examples:
- [`github_actions/`](./examples/github_actions/) — GitHub Actions integration example
- [`gitlab_ci/`](./examples/gitlab_ci/) — GitLab CI integration example
- [`gitflic_ci/`](./examples/gitflic_ci/) — GitFlic CI integration example
- [`gerrit_ci/`](./examples/gerrit_ci/) — Gerrit (Jenkins / Gerrit Trigger) integration example

#### GitHub Action

Expand Down
1 change: 1 addition & 0 deletions README.ru-RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ ocr review \
- [`github_actions/`](./examples/github_actions/) — пример интеграции с GitHub Actions
- [`gitlab_ci/`](./examples/gitlab_ci/) — пример интеграции с GitLab CI
- [`gitflic_ci/`](./examples/gitflic_ci/) — пример интеграции с GitFlic CI
- [`gerrit_ci/`](./examples/gerrit_ci/) — пример интеграции с Gerrit (Jenkins / Gerrit Trigger)

#### GitHub Action

Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ ocr review \
- [`github_actions/`](./examples/github_actions/) — GitHub Actions 集成示例
- [`gitlab_ci/`](./examples/gitlab_ci/) — GitLab CI 集成示例
- [`gitflic_ci/`](./examples/gitflic_ci/) — GitFlic CI 集成示例
- [`gerrit_ci/`](./examples/gerrit_ci/) — Gerrit (Jenkins / Gerrit Trigger) 集成示例

#### GitHub Action

Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ This directory contains examples for integrating OpenCodeReview (OCR) into vario
- **[github_actions/](./github_actions/)** - GitHub Actions integration example
- **[gitlab_ci/](./gitlab_ci/)** - GitLab CI integration example
- **[gitflic_ci/](./gitflic_ci/)** - GitFlic CI integration example
- **[gerrit_ci/](./gerrit_ci/)** - Gerrit (Jenkins / Gerrit Trigger) integration example

Each subdirectory contains its own README with detailed setup instructions.
107 changes: 107 additions & 0 deletions examples/gerrit_ci/Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// OpenCodeReview - Gerrit patchset auto-review (Jenkins + Gerrit Trigger)
//
// Reviews every new patchset with OpenCodeReview and posts the findings back
// onto the change via post_review.py (see README.md in this directory).
//
// Trigger: configure the Gerrit Trigger plugin ON THE JOB (Configure ->
// Gerrit Trigger -> trigger on "Patchset Created"). A declarative
// `triggers { gerrit(...) }` block exists, but its map syntax varies across
// plugin versions, so real jobs configure the trigger in the UI. The plugin
// injects GERRIT_BRANCH, GERRIT_REFSPEC, GERRIT_CHANGE_NUMBER,
// GERRIT_CHANGE_URL and GERRIT_PATCHSET_REVISION, used below.
//
// Jenkins credentials: ocr-llm-auth-token (Secret text, LLM API token) and
// gerrit-http (Username/password: bot account + its Gerrit HTTP password --
// Settings > HTTP Credentials, NOT the account password).
// The agent needs git, node/npm 20+ and python3 on PATH.

pipeline {
agent any

environment {
// ocr resolves OCR_LLM_URL / OCR_LLM_TOKEN / OCR_LLM_MODEL directly
// from the environment (no `ocr config set`), so the token stays
// env-only and is never written to disk on a shared agent.
OCR_LLM_URL = 'https://api.openai.com/v1/chat/completions'
OCR_LLM_MODEL = 'gpt-4o'
OCR_LLM_TOKEN = credentials('ocr-llm-auth-token')
// The env resolver defaults to the Anthropic protocol; this is an
// OpenAI chat-completions endpoint.
OCR_USE_ANTHROPIC = 'false'
// GERRIT_URL: set here or in Jenkins global env; if unset,
// post_review.py derives it from the injected GERRIT_CHANGE_URL.
}

stages {
stage('Checkout') {
steps {
// Fresh workspaces lack the target ref, so fetch the branch
// with an explicit refspec that materializes the tracking ref
// (ocr diffs against origin/$GERRIT_BRANCH; a bare fetch only
// updates FETCH_HEAD when the clone refspec is narrow), then
// fetch the patchset ref and check out its revision.
sh '''
git fetch origin "+refs/heads/$GERRIT_BRANCH:refs/remotes/origin/$GERRIT_BRANCH"
git fetch origin "$GERRIT_REFSPEC"
git checkout "$GERRIT_PATCHSET_REVISION"
'''
}
}

stage('Install OCR') {
steps {
// Pin the version you have validated; bump deliberately.
sh 'npm install -g @alibaba-group/open-code-review@1.7.12'
// extra_body has no env-var equivalent. Providers that accept
// a top-level "thinking" field (e.g. DashScope/GLM; OpenAI
// rejects unknown fields) must fall back to the config file,
// which then needs the full triple (the config-file strategy
// resolves url/token/model together, ignoring the env vars):
// ocr config set llm.url/auth_token/model/use_anthropic ...
// ocr config set llm.extra_body '{"thinking": {"type": "disabled"}}'
// That writes the token to ~/.opencodereview/config.json, so on
// a shared agent clean it up with:
// post { always { sh 'rm -f ~/.opencodereview/config.json' } }
}
}

stage('Review') {
steps {
// `|| true` is REQUIRED: `ocr review` exits non-zero only
// when EVERY sub-agent fails; partial failures still exit 0.
// The guard in the next stage handles the empty-output case.
sh '''
ocr review \
--from "origin/$GERRIT_BRANCH" \
--to "$GERRIT_PATCHSET_REVISION" \
--format json \
--audience agent \
> result.json || true
'''
}
}

stage('Post to Gerrit') {
steps {
withCredentials([usernamePassword(credentialsId: 'gerrit-http', usernameVariable: 'GERRIT_HTTP_USER', passwordVariable: 'GERRIT_HTTP_PASSWORD')]) {
// post_review.py sits in the reviewed repo only in this
// demo; real jobs vendor it or fetch it:
// curl -fsSLO https://raw.githubusercontent.com/alibaba/open-code-review/main/examples/gerrit_ci/post_review.py
// --revision pins the reviewed SHA explicitly: the
// `current` default would retarget comments if a new
// patchset landed mid-pipeline. GERRIT_CHANGE_NUMBER and
// GERRIT_URL/GERRIT_CHANGE_URL come from the environment.
sh '''
if [ ! -s result.json ]; then
echo "OCR review produced no output, skipping post."
exit 0
fi
python3 examples/gerrit_ci/post_review.py \
--input result.json \
--revision "$GERRIT_PATCHSET_REVISION"
'''
}
}
}
}
}
115 changes: 115 additions & 0 deletions examples/gerrit_ci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# OpenCodeReview - Gerrit CI Demo

This demo shows how to integrate OpenCodeReview into a [Gerrit](https://www.gerritcodereview.com) 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`](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
```

1. The Gerrit Trigger plugin fires a Jenkins job on `patchset-created`
2. The job fetches the change ref and runs `ocr review --format json --audience agent`
3. `python3 post_review.py` reads the JSON and POSTs a single `ReviewInput` containing:
- **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.py` says 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`](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`](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:

```bash
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.patchset` variables; the script does not care who exported them.
- **`patchset-created` hook** — post directly from a server-side hook:

```bash
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):

```bash
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:

```json
{
"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 tag `autogenerated: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) and `omit_duplicate_comments` does not dedupe the summary `message`, 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`). The `current` default is for manual runs: if a new patchset lands mid-pipeline, `current` would retarget the comments onto code the review never saw.
- **Exit codes** — `0` on success (HTTP 409 change-closed is tolerated and also exits 0); `1` when the input JSON is unreadable or unparsable; `2` on 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`](post_review_test.py) — standard-library `unittest`, no network or git required:

```bash
cd examples/gerrit_ci
python3 post_review_test.py
```
Loading
Loading