open-code-review/examples/github_actions
Lei Zhang 0dac8ac376
fix(ci): prevent duplicate review posts on retry in ocr-review workflow (#250)
* fix(ci): add idempotency check to prevent duplicate review posts on retry

When the batch createReview fails with a 5xx/408/network error, the
request may still have landed on the server. Before retrying per-comment,
the workflow now:

- Tags each review/comment/summary with a per-run HTML comment ID derived
  from runId + runAttempt + content hash.
- Queries existing reviews and review comments to detect whether the batch
  actually landed, and only retries the comments that are missing.
- Before retrying an individual comment whose request may have reached
  GitHub, cools down (honoring rate-limit headers) then checks whether the
  comment already exists, treating it as success instead of posting a
  duplicate.
- Skips posting the summary comment when one with the same run tag already
  exists.
- Adds read-API retry/pacing helpers (withRetry/readWithPacing/readAllPages)
  with shorter spacing than writes (OCR_READ_SUCCESS_DELAY /
  OCR_READ_LOW_REMAINING_SPACING) since reads are cheaper but still consume
  the primary rate limit.

Degrades gracefully to the original fallback (accepting duplicate risk)
when the idempotency read calls themselves fail.

* fix(ci): harden idempotency checks in ocr-review workflow

Address code review findings on the GitHub Actions PR auto-review
workflow (applied to both .github/workflows and examples copies):

- readAllPages: cap pagination at maxPages=50 (default) to prevent
  unbounded loops, and validate the argument is a positive integer.
- getPostedCommentIds: anchor the ID regex to the HTML comment wrapper
  (<!-- ocr-... -->) with a capture group to avoid false positives from
  user-generated content.
- isCommentAlreadyPosted: return null (unknown) instead of false when
  the read API fails, so callers do not silently risk duplicates; accept
  a postedIdsCache to reuse a single paginated walk across retries.
- hasIssueCommentWithId: return null (unknown) on read API failure, and
  match the summary tag with an anchored regex for consistency.
- Call sites: handle null by skipping retry/posting to avoid duplicates
  while surfacing the failure in the summary.

* fix(ci): validate env config and document intentional behaviors

Address code review findings on the ocr-review workflow (applied to
both .github/workflows and examples copies):

- parseNonNegInt: add a validation helper for env-var parsing so
  negative or non-numeric values (e.g. OCR_MAX_RETRIES=-5) fall back to
  defaults instead of bypassing the `|| default` guard (a negative
  parseInt result is truthy). All seven retry/pacing config values now
  use it.
- readAllPages: document that the 50-page cap is an intentional safety
  valve against unbounded loops, not a normal mode; callers that depend
  on completeness already degrade safely to null (unknown), so a
  truncated walk does not silently produce duplicates.
- commentId: document that the 12-hex-char (48-bit) hash collision
  scope is a single PR (listReviewComments is PR-scoped) and a single
  run produces only tens to hundreds of comments, making the
  birthday-bound collision probability negligible (~1e-7 at 10k).

* docs(github_actions): sync README with retry/idempotency features in ocr-review.yml

- Add OCR_READ_SUCCESS_DELAY and OCR_READ_LOW_REMAINING_SPACING variables
  for read API pacing used by the idempotency check
- Document the three GitHub rate-limit retry strategies (primary reset,
  retry-after header, secondary no-header backoff)
- Add 'Idempotency: avoiding duplicate review comments' section describing
  how the workflow detects already-landed comments via per-run HTML tags
  and skips retrying when the read API is unavailable

* fix(ci): use full sha256 hash for review comment idempotency IDs

Drop the .slice(0, 12) truncation in commentId() and use the full 64-char
(256-bit) sha256 hex digest. The truncated 12-char hash carried a tiny but
nonzero collision risk whose failure mode was a silently dropped inline
comment (the idempotency check would mistake two distinct comments for
duplicates). The full hash makes the collision probability effectively
zero with no meaningful downside; the ID regex already used [a-f0-9]+ so
it accepts the longer IDs unchanged.

* fix(ci): use random per-comment IDs and defer body assembly in review workflow

Replace the content-derived commentId() (sha256 of path/line/content) with
a random per-comment ID (crypto.randomBytes) and restructure the inline-
comment flow around an item struct that carries { comment, id, lines }.

This fixes two issues in the idempotency check:

1. ID was recomputed on every failure check. Each inline comment is now
   assigned one random ID up front and carried on the item struct, so the
   retry/idempotency logic reads item.id directly. The comment body (which
   embeds the ID) is assembled only at API-call time in toReviewPayload(),
   eliminating repeated hash computation.

2. Content-derived IDs collided for distinct comments sharing the same
   path/line/content. A random ID guarantees two such comments get
   different IDs, so the idempotency check no longer mistakes the second
   for a duplicate of the first and silently drops it on retry.

formatComment/commentId are removed (no callers remain) and replaced with
newCommentId/resolveLines/toReviewPayload/buildBody. The matching regex
already used [a-f0-9]+ so it accepts the new random tokens unchanged.
README ID-format placeholder updated from <hash> to <token>.

* docs(ci): correct misleading readAllPages truncation comment

The comment claimed 'a truncated walk does not silently produce
duplicates' because callers 'degrade safely by returning null on read
failures.' That reasoning only holds when the read API THROWS (rate
limit, 5xx): isCommentAlreadyPosted/hasIssueCommentWithId then return
null (unknown) and the caller skips retrying. A truncated walk does not
throw — it returns a partial set silently, so isCommentAlreadyPosted
returns false (definitively 'not posted') for comments beyond the cap,
and the retry loop reposts them, producing duplicates.

Rewrite the comment to state the cap is an intentional safety valve and
to explicitly distinguish truncation (partial data, can duplicate) from
thrown read failures (null/unknown, safe). No behavior change.

* fix(ci): drop stale postedIdsCache to prevent duplicate inline comments

isCommentAlreadyPosted reused a single listReviewComments snapshot
(postedIdsCache) across all per-comment retries. As comments landed
during the loop, the snapshot went stale; a 5xx-landed comment checked
against the stale snapshot would be reported as 'not posted' and
retried, posting a duplicate.

Remove the cache and walk fresh on every check. The extra reads are
paced via readAllPages/readWithPacing (with retry honoring retry-after
and x-ratelimit-reset) and degrade to null — skip retry — if the read
API ultimately fails, so they cannot produce duplicates. The cache
provided no real benefit in this path: checked comments are either
genuine misses (correctly false) or just-landed (a fresh walk catches
them), so hits essentially never occurred.
2026-07-01 19:38:10 +08:00
..
ocr-review.yml fix(ci): prevent duplicate review posts on retry in ocr-review workflow (#250) 2026-07-01 19:38:10 +08:00
README.md fix(ci): prevent duplicate review posts on retry in ocr-review workflow (#250) 2026-07-01 19:38:10 +08:00

OpenCodeReview - GitHub Actions Demo

This demo shows how to integrate OpenCodeReview into your GitHub Actions workflow to automatically review Pull Requests and post review comments.

How It Works

PR Created/Updated → GitHub Actions Triggered → OCR Reviews Diff → Comments Posted on PR
     OR
Comment with trigger keyword ↗
  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 <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

1. Copy the workflow file

Copy ocr-review.yml to your repository's .github/workflows/ directory:

mkdir -p .github/workflows
cp ocr-review.yml .github/workflows/ocr-review.yml

2. Configure secrets

Go to your repository's Settings → Secrets and variables → Actions and add:

Secret Required Description
OCR_LLM_URL Yes LLM API endpoint URL (e.g., https://api.openai.com/v1/chat/completions)
OCR_LLM_AUTH_TOKEN Yes API authentication token
OCR_LLM_MODEL No Model name (defaults to gpt-4o)
OCR_LLM_USE_ANTHROPIC No Set to true if using Anthropic Claude models

Note: GITHUB_TOKEN is automatically provided by GitHub Actions with the required pull-requests: write permission.

The workflow also configures llm.extra_body to disable thinking mode for compatibility with various LLM providers.

Customization

Change the trigger events

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

on:
  pull_request_target:
    types: [opened, synchronize, reopened, ready_for_review]

Customize comment trigger keywords

By default, the workflow triggers when a PR comment starts with /open-code-review or @open-code-review. You can customize these keywords by modifying the if condition in the workflow:

if: |
  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'))

Or use a more flexible pattern with contains to trigger on any comment containing the keyword:

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

Note: The condition github.event.issue.pull_request ensures the comment is on a PR, not a regular issue.

Use a specific OCR version

- name: Install OpenCodeReview
  run: 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:

- name: Run OCR review
  run: ocr review --rule ./my-rules.json --from origin/${{ github.base_ref }} --to origin/${{ github.head_ref }}

Adjust retry and delay settings

When posting review comments individually (fallback mode), the workflow includes rate-limit handling with exponential backoff. The retry strategy follows GitHub's documented guidance for REST API rate limits — see Rate limits for the REST API for details on primary/secondary rate limits and recommended retry behavior:

  • Primary rate limit exhausted (x-ratelimit-remaining=0): wait until x-ratelimit-reset.
  • Secondary rate limit with a retry-after header: wait exactly that long.
  • Secondary rate limit with no header: wait at least one minute, then use exponential backoff on continued failures.

You can configure the retry and delay behavior via repository variables (Settings → Secrets and variables → Actions → Variables):

Variable Default Description
OCR_RETRY_BASE_DELAY 60000 Base delay (ms) for exponential backoff when no retry header is present (per GitHub's "at least one minute" recommendation for secondary limits)
OCR_RETRY_MAX_DELAY 300000 Maximum delay (ms) cap applied to every computed wait, including retry-after and x-ratelimit-reset, so a far-future reset cannot stall the job past its timeout
OCR_MAX_RETRIES 3 Maximum retry attempts per comment when rate-limited
OCR_SUCCESS_DELAY 2000 Delay (ms) after a successful comment post to pace subsequent requests
OCR_FAILURE_DELAY 1000 Delay (ms) after a non-rate-limit failure to pace subsequent requests
OCR_LOW_REMAINING_THRESHOLD 3 When x-ratelimit-remaining is at or below this value, proactively increase request spacing to avoid hitting the limit
OCR_LOW_REMAINING_SPACING 10000 Request spacing (ms) used when remaining quota is low
OCR_READ_SUCCESS_DELAY 500 Delay (ms) after a successful read API call (listReviews / listReviewComments / listIssueComments) used for the idempotency check. Reads are cheaper than writes, so the default is shorter
OCR_READ_LOW_REMAINING_SPACING 5000 Request spacing (ms) for read calls when remaining quota is low

These variables are optional — if not configured, sensible defaults are used. Consider increasing delays for repositories with many concurrent workflows or large PRs that generate numerous review comments.

Idempotency: avoiding duplicate review comments

When the batch createReview call fails with a 5xx error, the request may still have landed on the GitHub server (the response was simply lost). Before retrying per-comment, the workflow queries existing reviews and review comments — each tagged with a per-run HTML comment (e.g. <!-- ocr-<runId>-<attempt>-<token> -->) — and only retries the comments that are actually missing. This prevents duplicate review posts.

The same idempotency check is applied to the summary comment: before posting, the workflow verifies whether a summary with the same run tag already exists, and skips posting if so.

If the read API itself is unavailable (rate-limited or 5xx), the check returns unknown rather than assuming the comment was not posted. In that case the workflow skips retrying to avoid risking a duplicate, and surfaces the uncertainty in the summary instead of silently producing duplicates.

Limit concurrency

Adjust the --concurrency flag for large PRs to control the number of concurrent LLM requests:

- name: Run OCR review
  run: ocr review --concurrency 5 --from origin/${{ github.base_ref }} --to origin/${{ github.head_ref }}

Provide background context

Use the --background flag to pass additional context that helps OCR better understand the purpose of the changes:

- name: Run OCR review
  run: ocr review --background "${{ github.event.pull_request.title }}" --from origin/${{ github.base_ref }} --to origin/${{ github.head_ref }}

This is particularly useful when your PR titles follow semantic conventions (e.g., feat(auth): add OAuth2 support) that clearly summarize what the PR implements. The background information helps OCR provide more relevant and context-aware review comments.

Customize the review comment author with GitHub App

By default, review comments are posted using the built-in GITHUB_TOKEN, which appears as github-actions[bot]. You can customize this by creating a GitHub App and using its credentials instead.

For more details about GitHub Apps, see the GitHub Apps documentation.

Step 1: Create a GitHub App

  1. Go to your organization or personal account Settings → Developer settings → GitHub Apps → New GitHub App
  2. Fill in the following:
    • GitHub App name: e.g., OpenCodeReview Bot
    • Homepage URL: Your repository or documentation URL
    • Webhook: Uncheck "Active" (not needed for this use case)
  3. Under Repository permissions, set:
    • Pull requests: Read and write
    • Contents: Read-only (for fetching diffs)
    • Metadata: Read-only (required)
  4. Click Create GitHub App

Step 2: Generate a Private Key

  1. After creating the app, scroll down to Private keys
  2. Click Generate a private key
  3. Download and save the .pem file securely

Note your App ID from the app settings page.

Step 3: Install the App

  1. In the left sidebar, click Install App
  2. Select the repositories where you want to use OCR
  3. After installation, note the Installation ID from the URL (e.g., https://github.com/settings/installations/12345 → Installation ID is 12345)

Step 4: Configure Repository Secrets

Add the following secrets to your repository (Settings → Secrets and variables → Actions):

Secret Description
GITHUB_APP_ID Your GitHub App's ID
GITHUB_APP_PRIVATE_KEY Contents of the .pem file (including -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY-----)
GITHUB_APP_INSTALLATION_ID The Installation ID from Step 3

Step 5: Update the Workflow

Add a step to obtain a token from the GitHub App, then use it in the "Post review comments to PR" step:

- name: Get GitHub App Token
  id: app-token
  uses: actions/create-github-app-token@v1
  with:
    app-id: ${{ secrets.GITHUB_APP_ID }}
    private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}

- name: Post review comments to PR
  uses: actions/github-script@v7
  with:
    github-token: ${{ steps.app-token.outputs.token }}
    script: |
      # ... existing script

Now review comments will be posted with your custom GitHub App identity (e.g., OpenCodeReview Bot), providing a more professional and distinguishable appearance in your PRs.

Example Output

When a PR is reviewed, comments appear directly in the PR's "Files changed" tab:

  • If no issues found: A comment saying "No comments generated. Looks good to me."
  • 🔍 If issues found: Inline review comments with suggestions using GitHub's native suggestion syntax

Inline Comment Example

The workflow uses GitHub's suggestion code block syntax, so reviewers can apply fixes with one click:

**Suggestion:**
```suggestion
// Fixed code here
```

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 (set OCR_LLM_USE_ANTHROPIC: true):
    • Anthropic Claude models

Troubleshooting

Common Issues

  1. "Failed to parse OCR output": Check that OCR_LLM_URL and OCR_LLM_AUTH_TOKEN secrets are correctly set
  2. "Cannot find merge-base": Ensure fetch-depth: 0 is set in the checkout step
  3. Review comments not appearing on correct lines: This can happen when the diff has changed since the review started; the workflow handles this gracefully with a fallback to issue comments

Debugging

Enable debug logging by adding to the OCR review step:

env:
  OCR_DEBUG: "1"