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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,12 @@ The core command for CI integration:
```bash
ocr review \
--from "origin/main" \
--to "origin/feature-branch" \
--to "<commit_sha>" \
--format json
```

The `--from` flag accepts a branch ref (e.g., `origin/main`) or commit SHA as the base, while `--to` accepts a commit SHA or branch ref as the head. In CI environments, using commit SHA for `--to` is recommended to correctly handle fork PRs/MRs where the source branch doesn't exist on the origin remote.

The `--format json` flag outputs machine-readable results suitable for parsing in CI scripts.

See the [`examples/`](./examples/) directory for integration examples:
Expand Down
12 changes: 6 additions & 6 deletions examples/github_actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ PR Created/Updated → GitHub Actions Triggered → OCR Reviews Diff → Comment
Comment with trigger keyword ↗
```

1. When a PR is opened, synchronized, or reopened, the workflow triggers
1. When a PR is opened, the workflow triggers (uses `pull_request_target` for fork secret access)
2. Alternatively, when a comment containing `/open-code-review` or `@open-code-review` is posted on a PR, the workflow triggers
3. It installs OCR via `npm install -g @alibaba-group/open-code-review`
4. Runs `ocr review --from origin/<base> --to origin/<head> --format json` to analyze the diff
4. Runs `ocr review --from origin/<base> --to <head_sha> --format json` to analyze the diff (uses commit SHA to support fork PRs)
5. Parses the JSON output and posts inline review comments on the PR using GitHub's Pull Request Review API

## Setup
Expand Down Expand Up @@ -46,11 +46,11 @@ Go to your repository's **Settings → Secrets and variables → Actions** and a

### Change the trigger events

Modify the `on.pull_request.types` array in the workflow file:
Modify the `on.pull_request_target.types` array in the workflow file:

```yaml
on:
pull_request:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
```

Expand All @@ -60,7 +60,7 @@ By default, the workflow triggers when a PR comment starts with `/open-code-revi

```yaml
if: |
github.event_name == 'pull_request' ||
github.event_name == 'pull_request_target' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/review')) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '@mybot'))
```
Expand All @@ -69,7 +69,7 @@ Or use a more flexible pattern with `contains` to trigger on any comment contain

```yaml
if: |
github.event_name == 'pull_request' ||
github.event_name == 'pull_request_target' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '/review'))
```

Expand Down
36 changes: 24 additions & 12 deletions examples/github_actions/ocr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# and posts review comments directly on the PR.
#
# Triggers:
# - PR opened, synchronized, or reopened
# - PR opened (uses pull_request_target for fork secret access)
# - Comment on PR containing '/open-code-review' or '@open-code-review'
#
# Required secrets:
Expand All @@ -18,8 +18,15 @@

name: OpenCodeReview PR Review

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

on:
pull_request:
# Use pull_request_target instead of pull_request so that secrets are
# available even for PRs from forks. This is safe because OCR only reads
# the diff and does not execute any code from the PR.
pull_request_target:
types: [opened]
issue_comment:
types: [created]
Expand All @@ -33,13 +40,13 @@ jobs:
runs-on: ubuntu-latest
# Run on PR events, or on comments starting with trigger keywords
if: |
github.event_name == 'pull_request' ||
github.event_name == 'pull_request_target' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/open-code-review')) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '@open-code-review'))
steps:
- name: Get PR context
id: pr-context
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request_target'
uses: actions/github-script@v7
with:
script: |
Expand All @@ -58,7 +65,10 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history needed for merge-base diff
ref: ${{ github.event_name != 'pull_request' && steps.pr-context.outputs.head_sha || '' }}
ref: ${{ github.event.pull_request.head.sha || steps.pr-context.outputs.head_sha }}

- name: Fetch PR head ref (ensures fork commits are available)
run: git fetch origin pull/${{ github.event.pull_request.number || github.event.issue.number }}/head

- name: Setup Node.js
uses: actions/setup-node@v4
Expand All @@ -79,21 +89,23 @@ jobs:
- name: Run OpenCodeReview
id: review
run: |
# Get base and head refs from PR context (different for comment triggers)
if [ "${{ github.event_name }}" = "pull_request" ]; then
# Get base ref and head SHA from PR context (different for comment triggers)
# Note: We use HEAD_SHA instead of origin/${HEAD_REF} to support fork PRs,
# because fork branches don't exist on the origin remote.
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
BASE_REF="${{ github.event.pull_request.base.ref }}"
HEAD_REF="${{ github.event.pull_request.head.ref }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
else
BASE_REF="${{ steps.pr-context.outputs.base_ref }}"
HEAD_REF="${{ steps.pr-context.outputs.head_ref }}"
HEAD_SHA="${{ steps.pr-context.outputs.head_sha }}"
fi

echo "Reviewing PR: ${HEAD_REF} against ${BASE_REF}"
echo "Reviewing PR: ${HEAD_SHA} against origin/${BASE_REF}"

# Run OCR in range mode with JSON output
ocr review \
--from "origin/${BASE_REF}" \
--to "origin/${HEAD_REF}" \
--to "${HEAD_SHA}" \
--format json \
> /tmp/ocr-result.json 2>/tmp/ocr-stderr.log || true

Expand Down Expand Up @@ -150,7 +162,7 @@ jobs:
let commitSha;

// Get commit SHA from event context
if (context.eventName === 'pull_request') {
if (context.eventName === 'pull_request_target') {
commitSha = context.payload.pull_request.head.sha;
} else {
// For comment events, we need to fetch the PR
Expand Down
61 changes: 41 additions & 20 deletions examples/gitlab_ci/.gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,30 @@
#
# This pipeline automatically reviews Merge Requests using OpenCodeReview
# and posts review comments (discussions) directly on the MR diff.
# Supports both same-repo and forked MR scenarios.
#
# Required CI/CD Variables (Settings → CI/CD → Variables):
# OCR_LLM_URL - LLM API endpoint (e.g., https://api.openai.com/v1/chat/completions)
# OCR_LLM_AUTH_TOKEN - Authentication token for the LLM API (mark as "Masked")
# GITLAB_API_TOKEN - GitLab Personal/Project Access Token with "api" scope
#
# Optional CI/CD Variables:
# OCR_LLM_MODEL - Model name (default: gpt-4o)
# GITLAB_API_TOKEN - GitLab Personal/Project Access Token with "api" scope
# (falls back to CI_JOB_TOKEN if not set)
#
# Fork MR Support:
# The script uses CI_COMMIT_SHA as the diff target to correctly resolve the
# source commit from forked repos. For some GitLab versions, you may need to enable:
# Project Settings → CI/CD → General pipelines →
# "Run pipelines in the parent project for merge requests from forked projects"

stages:
- review

code-review:
stage: review
interruptible: true
resource_group: mr-review-$CI_MERGE_REQUEST_IID
image: node:20
only:
- merge_requests
Expand All @@ -27,21 +37,22 @@ code-review:

# Configure OCR
- mkdir -p ~/.open-code-review
# Gitlab CI/CD does not support setting variables with value length less than 8, so you can't set use_anthropic as a CI variable
# Gitlab CI/CD does not support configuring variables with value length less than 8, so you can't set use_anthropic as a CI variable
- |
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"}}'

# Run OCR review
# Run OCR review (use CI_COMMIT_SHA for --to to support both same-repo and forked MRs)
- |
echo "Reviewing MR: ${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME} against ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}"
ocr review \
--from "origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}" \
--to "origin/${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}" \
--to "${CI_COMMIT_SHA}" \
--format json \
--audience agent \
> /tmp/ocr-result.json 2>/tmp/ocr-stderr.log || true
echo "OCR review completed."
cat /tmp/ocr-result.json
Expand All @@ -58,18 +69,27 @@ code-review:
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"]
# Fall back to CI_JOB_TOKEN for fork MR pipelines where GITLAB_API_TOKEN is unavailable
API_TOKEN = os.environ.get("GITLAB_API_TOKEN") or os.environ.get("CI_JOB_TOKEN", "")
SOURCE_BRANCH = os.environ["CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"]
TARGET_BRANCH = os.environ["CI_MERGE_REQUEST_TARGET_BRANCH_NAME"]
COMMIT_SHA = os.environ["CI_COMMIT_SHA"]

if not API_TOKEN:
print("ERROR: No API token available (GITLAB_API_TOKEN or CI_JOB_TOKEN). Cannot post comments.", file=sys.stderr)
sys.exit(1)

API_BASE = f"{GITLAB_URL}/api/v4/projects/{PROJECT_ID}/merge_requests/{MR_IID}"

# Determine auth header: PRIVATE-TOKEN for personal/project tokens, JOB-TOKEN for CI_JOB_TOKEN
USE_JOB_TOKEN = not os.environ.get("GITLAB_API_TOKEN")
AUTH_HEADER = "JOB-TOKEN" if USE_JOB_TOKEN else "PRIVATE-TOKEN"

def api_request(endpoint, data=None, method="POST"):
"""Make a GitLab API request."""
url = f"{API_BASE}{endpoint}"
headers = {
"PRIVATE-TOKEN": API_TOKEN,
AUTH_HEADER: API_TOKEN,
"Content-Type": "application/json"
}
body = json.dumps(data).encode("utf-8") if data else None
Expand All @@ -85,16 +105,16 @@ code-review:
"""Post a general note/comment on the MR."""
return api_request("/notes", {"body": body})

def post_discussion(path, line, body, base_sha=None, start_sha=None, head_sha=None):
def post_discussion(path, line, body, base_sha, start_sha, head_sha):
"""Post an inline discussion on a specific file/line in the MR diff."""
position = {
"position_type": "text",
"new_path": path,
"old_path": path,
"new_line": line,
"base_sha": base_sha or TARGET_BRANCH,
"start_sha": start_sha or TARGET_BRANCH,
"head_sha": head_sha or COMMIT_SHA,
"base_sha": base_sha,
"start_sha": start_sha,
"head_sha": head_sha,
}
data = {
"body": body,
Expand Down Expand Up @@ -138,7 +158,7 @@ code-review:

# --- Main ---

# Read OCR result
# Read OCR result (skip first line which is summary, not JSON)
try:
with open("/tmp/ocr-result.json", "r") as f:
result = json.load(f)
Expand Down Expand Up @@ -168,7 +188,7 @@ code-review:
diff_refs = None
try:
versions_url = f"{API_BASE}/versions"
req = urllib.request.Request(versions_url, headers={"PRIVATE-TOKEN": API_TOKEN})
req = urllib.request.Request(versions_url, headers={AUTH_HEADER: API_TOKEN})
with urllib.request.urlopen(req) as resp:
versions = json.loads(resp.read().decode("utf-8"))
if versions:
Expand All @@ -191,15 +211,11 @@ code-review:
start_line = comment.get("start_line", end_line)
body = format_comment(comment)

if not path or not end_line:
if not path or not end_line or not diff_refs:
failed_comments.append(comment)
continue

kwargs = {}
if diff_refs:
kwargs = diff_refs

result_resp = post_discussion(path, end_line, body, **kwargs)
result_resp = post_discussion(path, end_line, body, **diff_refs)
if result_resp:
success_count += 1
else:
Expand All @@ -214,8 +230,13 @@ code-review:
fallback_body += format_comment_fallback(comment) + "\n\n---\n\n"
post_note(fallback_body)

# Post summary
summary = f"🔍 **OpenCodeReview** found **{len(comments)}** issue(s) in this MR."
# Post summary last
total_count = len(comments)
failed_count = len(failed_comments)
summary = f"🔍 **OpenCodeReview** found **{total_count}** issue(s) in this MR."
if total_count > 0:
summary += f"\n- ✅ {success_count} posted as inline comment(s)"
summary += f"\n- 📝 {failed_count} posted as summary (missing line info)"
if warnings:
summary += f"\n\n⚠️ {len(warnings)} warning(s) occurred during review."
post_note(summary)
Expand Down
18 changes: 10 additions & 8 deletions examples/gitlab_ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ MR Created/Updated → GitLab Pipeline Triggered → OCR Reviews Diff → Discus

1. When a Merge Request is opened or updated, the pipeline triggers
2. It installs OCR via npm in a `node:20` Docker image
3. Runs `ocr review --from origin/<target> --to origin/<source> --format json` to analyze the diff
3. Runs `ocr review --from origin/<target> --to <commit_sha> --format json --audience agent` to analyze the diff (uses commit SHA to support fork MRs)
4. Parses the JSON output and posts inline discussions on the MR using GitLab's Discussions API

## Setup
Expand Down Expand Up @@ -39,7 +39,7 @@ Go to your project's **Settings → CI/CD → Variables** and add:
| `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` | No | No | Model name (defaults to `gpt-4o`) |
| `GITLAB_API_TOKEN` | Yes | Yes | GitLab access token with `api` scope |
| `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_anthropic` cannot be set as a CI variable. The pipeline sets it to `false` by default. If you need to use Anthropic Claude models, you'll need to modify the `.gitlab-ci.yml` script directly.
>
Expand All @@ -53,7 +53,7 @@ You need a token with `api` scope to post discussions on MRs. Options:
- **Personal Access Token**: User Settings → Access Tokens → Create with `api` scope
- **Group Access Token**: For organization-wide usage

> **Note:** The built-in `CI_JOB_TOKEN` does NOT have sufficient permissions to create MR discussions, which is why a separate token is needed.
> **Note:** The built-in `CI_JOB_TOKEN` has limited API scope and may not support all discussion features (e.g., creating new threads on older GitLab versions). If `GITLAB_API_TOKEN` is not set, the pipeline falls back to `CI_JOB_TOKEN` automatically — but for best results, a dedicated token with `api` scope 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 Bot` will make review comments appear as posted by `OpenCodeReview Bot`.

Expand Down Expand Up @@ -95,7 +95,7 @@ Use the `--rule` flag to pass a custom rules JSON file:

```yaml
script:
- ocr review --rule ./my-rules.json --from origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to origin/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
- ocr review --rule ./my-rules.json --from origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to $CI_COMMIT_SHA
```

### Limit concurrency
Expand All @@ -104,7 +104,7 @@ Adjust the `--concurrency` flag for large MRs to control the number of concurren

```yaml
script:
- ocr review --concurrency 5 --from origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to origin/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
- ocr review --concurrency 5 --from origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to $CI_COMMIT_SHA
```

### Provide background context
Expand All @@ -113,7 +113,7 @@ Use the `--background` flag to pass additional context that helps OCR better und

```yaml
script:
- ocr review --background "$CI_MERGE_REQUEST_TITLE" --from origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --to origin/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
- 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.
Expand Down Expand Up @@ -168,11 +168,13 @@ script:

# 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", f"origin/{SOURCE_BRANCH}",
"--format", "json"
"--to", COMMIT_SHA,
"--format", "json",
"--audience", "agent"
], capture_output=True, text=True)

# Save output for the posting script
Expand Down