From f40f02cbc39a435757b3992b697c41760bb0730c Mon Sep 17 00:00:00 2001 From: stay-foolish-forever Date: Sat, 6 Jun 2026 15:53:54 +0800 Subject: [PATCH 1/4] docs(examples): add concurrency control to CI workflow examples - GitHub Actions: add concurrency group with cancel-in-progress to avoid redundant review runs on rapid pushes - GitLab CI: add interruptible and resource_group to cancel outdated review jobs when new commits are pushed to the same MR --- examples/github_actions/ocr-review.yml | 4 ++++ examples/gitlab_ci/.gitlab-ci.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/examples/github_actions/ocr-review.yml b/examples/github_actions/ocr-review.yml index ffeefb5..71d567c 100644 --- a/examples/github_actions/ocr-review.yml +++ b/examples/github_actions/ocr-review.yml @@ -18,6 +18,10 @@ name: OpenCodeReview PR Review +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + on: pull_request: types: [opened] diff --git a/examples/gitlab_ci/.gitlab-ci.yml b/examples/gitlab_ci/.gitlab-ci.yml index 9bcd372..aa8ff1d 100644 --- a/examples/gitlab_ci/.gitlab-ci.yml +++ b/examples/gitlab_ci/.gitlab-ci.yml @@ -16,6 +16,8 @@ stages: code-review: stage: review + interruptible: true + resource_group: mr-review-$CI_MERGE_REQUEST_IID image: node:20 only: - merge_requests From 0fe3aecddcb1e63824c223b53272b38fe26d2fb3 Mon Sep 17 00:00:00 2001 From: stay-foolish-forever Date: Mon, 8 Jun 2026 16:13:09 +0800 Subject: [PATCH 2/4] docs(examples): improve GitLab CI example with fork MR and concurrency support - Support forked MR pipelines by using CI_COMMIT_SHA as --to target - Fall back to CI_JOB_TOKEN when GITLAB_API_TOKEN is unavailable - Use appropriate auth header (JOB-TOKEN vs PRIVATE-TOKEN) based on token source - Add --audience agent flag for machine-consumable review output - Make diff_refs required for inline comments, simplify post_discussion signature - Improve summary with inline vs fallback comment breakdown - Add documentation comments for fork MR setup requirements --- examples/gitlab_ci/.gitlab-ci.yml | 59 ++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/examples/gitlab_ci/.gitlab-ci.yml b/examples/gitlab_ci/.gitlab-ci.yml index aa8ff1d..2cff059 100644 --- a/examples/gitlab_ci/.gitlab-ci.yml +++ b/examples/gitlab_ci/.gitlab-ci.yml @@ -2,14 +2,22 @@ # # 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 @@ -29,7 +37,7 @@ 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 @@ -37,13 +45,14 @@ code-review: 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 @@ -60,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 @@ -87,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, @@ -140,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) @@ -170,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: @@ -193,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: @@ -216,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) From ed1d7f0077e715c034b1c645ac453ba00ea962b8 Mon Sep 17 00:00:00 2001 From: stay-foolish-forever Date: Mon, 8 Jun 2026 16:19:54 +0800 Subject: [PATCH 3/4] docs(examples): use pull_request_target and SHA refs for fork PR support - Switch trigger from pull_request to pull_request_target so secrets are available for PRs from forks - Use head SHA instead of branch ref for checkout and ocr --to, since fork branches don't exist on the origin remote - Add explicit fetch step to ensure fork commits are available - Update condition checks and comments to reflect the new event name --- examples/github_actions/ocr-review.yml | 32 ++++++++++++++++---------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/examples/github_actions/ocr-review.yml b/examples/github_actions/ocr-review.yml index 71d567c..793b354 100644 --- a/examples/github_actions/ocr-review.yml +++ b/examples/github_actions/ocr-review.yml @@ -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: @@ -23,7 +23,10 @@ concurrency: 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] @@ -37,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: | @@ -62,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 @@ -83,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 @@ -154,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 From d7202da1711f3cf7629f281f492b9f4bb2b82839 Mon Sep 17 00:00:00 2001 From: stay-foolish-forever Date: Mon, 8 Jun 2026 16:37:53 +0800 Subject: [PATCH 4/4] docs: sync READMEs with CI script changes for fork PR/MR support --- README.md | 4 +++- examples/github_actions/README.md | 12 ++++++------ examples/gitlab_ci/README.md | 18 ++++++++++-------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0ff3a86..9fee69b 100644 --- a/README.md +++ b/README.md @@ -214,10 +214,12 @@ The core command for CI integration: ```bash ocr review \ --from "origin/main" \ - --to "origin/feature-branch" \ + --to "" \ --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: diff --git a/examples/github_actions/README.md b/examples/github_actions/README.md index 299fd48..272f788 100644 --- a/examples/github_actions/README.md +++ b/examples/github_actions/README.md @@ -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/ --to origin/ --format json` to analyze the diff +4. Runs `ocr review --from origin/ --to --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 @@ -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] ``` @@ -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')) ``` @@ -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')) ``` diff --git a/examples/gitlab_ci/README.md b/examples/gitlab_ci/README.md index 68874e4..6ac630b 100644 --- a/examples/gitlab_ci/README.md +++ b/examples/gitlab_ci/README.md @@ -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/ --to origin/ --format json` to analyze the diff +3. Runs `ocr review --from origin/ --to --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 @@ -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. > @@ -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`. @@ -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 @@ -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 @@ -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. @@ -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