* feat(action): extract reusable OpenCodeReview PR review GitHub Action
Consolidate the reusable-action work into one commit:
- Add composite action (action.yml at repo root for GitHub Marketplace;
helper at scripts/github-actions/post-review-comments.js) porting the
sticky summary, incremental posting, and retry idempotency logic.
- Add unit tests covering the ported idempotency behavior.
- Switch the in-repo CI workflow to use the reusable action.
- Add and refine example reusable workflows for consumers.
* ci(workflow): point ocr-review at root action.yml and quote boolean inputs
- Fix uses: to ./ now that action.yml lives at the repo root.
- Quote sticky_summary/incremental/upload_artifacts as strings to
match action.yml's input declarations (composite-action inputs are
always strings) and silence actionlint.
- Enable upload_artifacts for this workflow.
* docs(examples): point reusable demo at root action.yml
The example workflow referenced alibaba/open-code-review/action@v1,
but action.yml now lives at the repo root, so the /action subpath no
longer resolves. Use alibaba/open-code-review@v1 and update the stale
action/README.md comment to point at the root action.yml.
* docs(examples): sync README to root action.yml references
The example README still pointed at the relocated/deleted locations:
action.yml is now at the repo root, so update all 11
alibaba/open-code-review/action@v1 references to
alibaba/open-code-review@v1, and repoint the action/ directory and
action/README.md links to the root action.yml.
* fix(examples): prevent unrelated PR comments from canceling ocr-review
GitHub Actions evaluates concurrency before the job-level if-condition.
The flat group mapped every issue_comment event on a PR into the review's
group, so any comment (even a skipped conversation reply) canceled any
in-progress review.
Match the reusable demo's conditional group: PR events and human-authored
/open-code-review/@open-code-review comments share a per-PR group, while
non-matching comments fall back to a unique noop-<run_id> group that can
never collide with a real review.
* fix(action): address code-review findings across reusable PR review
- post-review-comments: parse retry delays via parseNonNegInt (0/negative fix);
paginate findExistingSummaryComment through readAllPages; remove dead
rangeOf and hasIssueCommentWithId (plus duplicated comment block)
- action.yml: move ${{ }} interpolations into env: (resolve refs, PR_NUM,
ocr_version); fail fast on PR head fetch instead of swallowing errors
- workflows: add timeout-minutes: 30; gate issue_comment on
author_association; tighten pr-context if to == 'issue_comment'
* fix(action): harden review posting after code review
- pass incremental_overlap_threshold via env to avoid github-script injection
- capture ocr review exit code directly instead of &&/|| chain
- drop redundant SUMMARY_MARKER prepend in postSummary (callers already add it)
- align example job if-condition bot check with its concurrency group
* fix(action): always upload review artifacts and capture ocr exit code
* fix(action): merge posting statistics into the summary header
The PR summary issue comment used to present two overlapping breakdowns:
a leading "posted as inline / posted as summary" header and a trailing
"📊 Posting Statistics" block. Their definitions overlapped (the header's
"summary" count included failures the trailer also listed as failed), and
when incremental filtering skipped comments the header counts no longer
summed to the total, making the summary hard to interpret.
Merge them into a single header whose four counts (inline / summary /
skipped / failed) are mutually exclusive and sum to the total, and drop
the trailing Posting Statistics section. buildSummaryBody now takes an
options object.
* fix(action): support local action resolution in container/self-hosted setups
- Checkout trusted base + mark workspace safe for pull_request_target so
the local `uses: ./` action can be resolved and loaded
- Check for git/Node.js and install git when missing, making the
composite action resilient across runner images
- Move Setup Node.js earlier and make it conditional on availability
- Resolve post-review-comments helper at runtime via
GITHUB_ACTION_PATH falling back to GITHUB_WORKSPACE, fixing helper
lookup for local actions where the action path is a host path
invisible inside containers
* refactor(examples): consolidate github_actions demo to reusable action
Drop the inline-script full-control demo; the renamed ocr-review.yml
(from ocr-review-reusable.yml) is now the single demo, invoking
alibaba/open-code-review@main.
Sync the README to the current implementation:
- normalize action refs to @main; point self-hosted-runner users to the
repo's own workflow (noting uses: ./ is internal-only)
- document config via action inputs (posting modes: sticky/incremental)
- update the comment-trigger if with defensive bot/author_association
guards and the concurrency mirror
- fix Example Output to cover the summary comment + inline comments
- replace the non-existent OCR_DEBUG debugging with
artifacts/outputs/ACTIONS_STEP_DEBUG
- use --replace-all for safe.directory
* fix(action): harden withRetry against silent undefined return
withRetry's for loop had no terminal return/throw after the loop body.
Although the current loop invariant (last attempt always throws, and
parseNonNegInt guards against negative MAX_RETRIES) makes fall-through
unreachable, an async function that falls through resolves to undefined,
which would surface as a confusing downstream TypeError for the read-API
callers that rely on it.
Capture lastErr in the loop and add an explicit terminal throw so any
future break of the invariant fails loudly instead of silently returning
undefined.
* docs(readme): document the reusable GitHub Action in CI/CD section
* fix(action): restore language config via a language input
The old inline workflow ran `ocr config set language English`, but the
composite action's Configure OCR step only set llm.extra_body, with no
language input. Add a language input (default English) and write it via
`ocr config set language` so review output language is no longer left
to the tool's default.
Addresses #337 (discussion_r3550069843).
* fix(action): warn when incremental comment listing hits page cap
listExistingReviewComments silently dropped comments beyond its 10-page
cap, unlike readAllPages which logs when truncation occurs. Add the
same max-page-limit warning after the loop so a partial walk during
incremental dedup is visible in the logs.
Addresses #337 (discussion_r3550069871).
* docs(readme): sync GitHub Action section to localized READMEs
govulncheck flags GO-2026-5856 (Encrypted Client Hello privacy leak in
crypto/tls), present in the Go standard library through go1.26.4 and
fixed in go1.26.5. The CI and release workflows pin the golang:1.26.4
container image, so govulncheck fails with exit code 3 on every run.
Bump both workflow images to golang:1.26.5.
* 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.
On self-hosted runners, _github_home/.gitconfig persists across jobs.
The ocr-review workflow used --add which accumulated multiple safe.directory
values over time. Once multiple values existed, other workflows using plain
git config (without --add/--replace-all) failed with "cannot overwrite
multiple values with a single value".
Unify all workflows to use --replace-all, which clears previous values and
writes exactly one entry regardless of prior state.
Add build provenance attestation to the release workflow using
actions/attest-build-provenance with OIDC keyless signing.
Document release signature verification in SECURITY.md.
- Add govulncheck step to CI pipeline for vulnerability scanning
- Set 15-minute timeout on CI test job
- Add dependabot config for weekly Go module and GitHub Actions updates
- Split LD_FLAGS so dev builds retain debug symbols while release
builds remain stripped with -s -w
* ci(workflow): add rate-limit-aware retry for PR review comment posting
Add GitHub REST API rate-limit handling to the ocr-review workflow:
- Implement computeRetryDelayMs() following GitHub's documented strategy:
* Honor retry-after header (seconds or HTTP-date)
* Wait until x-ratelimit-reset when primary limit is exhausted (remaining=0)
* Exponential backoff (>=60s base) for secondary limits without a header
* Backoff for transient 5xx/408 errors
- Add per-comment retry loop with configurable attempts and delays
- Add logRateLimitQuota() to log and proactively throttle on low remaining quota
- Honor batch createReview rate-limit headers before per-comment retry
- Cap all waits (including header-derived) to avoid stalling the CI job
- Expose tuning via OCR_* env vars (retries, delays, thresholds)
- Update example workflow docs with retry/delay configuration reference
* fix(workflow): use shorter transient backoff base and dedupe header lookup
- Transient server errors (5xx/408) now use a 2s base delay instead of
the 60s rate-limit base, matching the comment's stated intent and
avoiding unnecessary CI stalls on short-lived server hiccups.
- Extract a shared getHeader() helper for case-insensitive header
lookup, reused by both computeRetryDelayMs and logRateLimitQuota to
eliminate duplicated logic and ensure consistent robustness.
* doc: revert example doc
The release job uses ubuntu:24.04 which lacks git. After adding
commit-based release notes generation, git commands (config/describe/log)
fail with "git: not found". Install git before checkout so the full
history is available for release notes.
Add continuous integration pipeline that runs go vet, tests with
race detection, and a build check on every push to main and PR.
Satisfies the OpenSSF Best Practices test_continuous_integration criterion.
Satisfy the OpenSSF Best Practices "release_notes" criterion by parsing
commits between tags and categorizing them into Features, Bug Fixes,
Refactoring, and Documentation sections.
Ship Go binaries inside per-platform npm packages (@alibaba-group/ocr-{os}-{arch})
so npm install resolves the correct binary via optionalDependencies + os/cpu fields.
This removes the need for a postinstall download from GitHub Releases, which is
extremely slow for users behind restricted networks (e.g. China mainland).
The postinstall download is retained as a fallback for --no-optional installs.
* ci: use self-hosted runners with container images for build and deploy workflows
- deploy-pages: switch to self-hosted runner with node:20 container, remove redundant setup-node
- release: use self-hosted runner with golang:1.26.4 container for build job, remove redundant setup-go
- release: use self-hosted runner with ubuntu:24.04 container for release job
- release: use self-hosted runner with node:20 container for npm-publish job, remove redundant setup-node
- release: add jq installation step for version injection in npm-publish
* ci: fix dubious ownership in self-hosted container jobs
- Add git safe.directory trust step after checkout in release.yml build and npm-publish jobs
- Add git safe.directory trust step after checkout in deploy-pages.yml build job
- Switch deploy-pages deploy job from ubuntu-latest to self-hosted with node:20 container
Add safe.directory '*' to git config before running git operations.
Self-hosted runners may run jobs under a different OS user than the
one that owns the workspace, causing git to refuse operations with a
'dubious ownership' error.
* ci: add GitHub Actions workflow for automated PR review
- Trigger on PR opened and issue_comment (/open-code-review)
- Install OCR via npm, configure LLM secrets, run review
- Post inline review comments via GitHub PR review API
- Fallback to individual comments if batch posting fails
- Use commit SHA instead of branch ref to support fork PRs
* ci: use pull_request_target to support fork PR secret access
- Switch from pull_request to pull_request_target trigger
- Add synchronize and reopened event types
- Explicitly checkout PR head SHA (pull_request_target defaults to base)
- This ensures secrets are available for PRs from forked repos
* ci: fix pull_request_target handling for fork PRs
- Narrow trigger types to 'opened' only to reduce redundant runs
- Add explicit fetch of PR head ref to ensure fork commits are available
- Fix event name check from 'pull_request' to 'pull_request_target'
* feat(build): add Windows platform support
Add windows/amd64 and windows/arm64 to CI build matrix, Makefile
cross-platform targets, and handle .exe suffix in install/update
scripts and binary wrapper. Skip chmod on Windows where unsupported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(README): add Windows download instructions
Add Windows x86_64 and ARM64 binary download commands to both
English and Chinese README install sections.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Allow users to configure include/exclude glob patterns in
.open-code-review/rule.json (global or project-level) to control
which files are reviewed. Exclude has highest priority, include
penetrates default path exclusions but not the extension allowlist.
Only one layer takes effect following --rule > project > global priority.