mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-09 17:28:58 +00:00
305 lines
11 KiB
YAML
305 lines
11 KiB
YAML
name: OpenCodeReview PR Review
|
|
description: >-
|
|
AI-powered GitHub PR review with inline comments, sticky summary, and
|
|
incremental non-destructive posting.
|
|
author: alibaba
|
|
branding:
|
|
icon: eye
|
|
color: green
|
|
|
|
inputs:
|
|
llm_url:
|
|
description: LLM API endpoint URL (mapped to env OCR_LLM_URL).
|
|
required: true
|
|
llm_auth_token:
|
|
description: LLM auth token (mapped to env OCR_LLM_TOKEN).
|
|
required: true
|
|
llm_model:
|
|
description: Model name (mapped to env OCR_LLM_MODEL).
|
|
required: true
|
|
llm_use_anthropic:
|
|
description: "'true' for Anthropic Claude, 'false' for OpenAI-compatible APIs
|
|
(mapped to env OCR_USE_ANTHROPIC). Required to force an explicit choice."
|
|
required: true
|
|
llm_auth_header:
|
|
description: Custom auth header name (mapped to env OCR_LLM_AUTH_HEADER).
|
|
required: false
|
|
llm_extra_headers:
|
|
description: Extra headers "K=V,K=V" (mapped to env OCR_LLM_EXTRA_HEADERS).
|
|
required: false
|
|
llm_extra_body:
|
|
description: >-
|
|
extra_body JSON for LLM requests. No env var exists for this, so it is
|
|
written via `ocr config set llm.extra_body`.
|
|
required: false
|
|
default: '{"thinking": {"type": "disabled"}}'
|
|
language:
|
|
description: >-
|
|
Review output language, written via `ocr config set language`
|
|
(e.g. English, 中文). No env var exists for this.
|
|
required: false
|
|
default: 'English'
|
|
llm_timeout:
|
|
description: LLM request timeout in seconds (mapped to env OCR_LLM_TIMEOUT).
|
|
required: false
|
|
github_token:
|
|
description: GitHub token used to post review comments.
|
|
required: false
|
|
default: ${{ github.token }}
|
|
ocr_version:
|
|
description: npm version spec for @alibaba-group/open-code-review.
|
|
required: false
|
|
default: latest
|
|
review_concurrency:
|
|
description: Value passed to `ocr review --concurrency`.
|
|
required: false
|
|
background:
|
|
description: Value passed to `ocr review --background`.
|
|
required: false
|
|
rule:
|
|
description: Path to a custom rules JSON file passed to `ocr review --rule`.
|
|
required: false
|
|
upload_artifacts:
|
|
description: >-
|
|
Upload raw JSON result and stderr as workflow artifacts. Must be the
|
|
literal string 'true' or 'false' (quoted); the step gates on a string
|
|
comparison, so an unquoted YAML boolean will not match.
|
|
required: false
|
|
default: 'true'
|
|
sticky_summary:
|
|
description: >-
|
|
Summary dimension. true = update an existing summary comment in place
|
|
(sticky) instead of posting a new one each run.
|
|
required: false
|
|
default: 'true'
|
|
incremental:
|
|
description: >-
|
|
Incremental dimension. true = only append inline comments whose (path,
|
|
line range) does not overlap an existing bot review comment. History is
|
|
never deleted (non-destructive).
|
|
required: false
|
|
default: 'false'
|
|
incremental_overlap_threshold:
|
|
description: >-
|
|
IoU (intersection-over-union) threshold used by incremental mode to decide
|
|
whether a new multi-line comment overlaps an existing one. Two single-line
|
|
comments match when on the same line; single- vs multi-line never match.
|
|
Value in (0, 1]; ignored unless incremental is true.
|
|
required: false
|
|
default: '0.6'
|
|
base_ref:
|
|
description: >-
|
|
Override the base ref. Provide this (and head_sha) when invoking from a
|
|
non-PR event such as issue_comment.
|
|
required: false
|
|
head_sha:
|
|
description: Override the head commit SHA (use with base_ref for comment triggers).
|
|
required: false
|
|
node_version:
|
|
description: Node.js version for actions/setup-node.
|
|
required: false
|
|
default: '24'
|
|
|
|
outputs:
|
|
comments_total:
|
|
description: Total number of review comments generated by OCR.
|
|
value: ${{ steps.post.outputs.comments_total }}
|
|
comments_inline:
|
|
description: Number of inline comments successfully posted.
|
|
value: ${{ steps.post.outputs.comments_inline }}
|
|
comments_skipped:
|
|
description: Number of inline comments skipped by incremental mode (overlap with history).
|
|
value: ${{ steps.post.outputs.comments_skipped }}
|
|
comments_failed:
|
|
description: Number of inline comments that failed to post.
|
|
value: ${{ steps.post.outputs.comments_failed }}
|
|
summary_comment_url:
|
|
description: URL of the posted/updated summary comment, if any.
|
|
value: ${{ steps.post.outputs.summary_comment_url }}
|
|
|
|
runs:
|
|
using: composite
|
|
steps:
|
|
- name: Check git and Node.js
|
|
id: check_deps
|
|
shell: bash
|
|
run: |
|
|
if command -v git >/dev/null 2>&1; then
|
|
echo "git_installed=true" >> "$GITHUB_OUTPUT"
|
|
echo "git is already installed: $(git --version)"
|
|
else
|
|
echo "git_installed=false" >> "$GITHUB_OUTPUT"
|
|
echo "git is not installed"
|
|
fi
|
|
if command -v node >/dev/null 2>&1; then
|
|
echo "node_installed=true" >> "$GITHUB_OUTPUT"
|
|
echo "node is already installed: $(node --version)"
|
|
else
|
|
echo "node_installed=false" >> "$GITHUB_OUTPUT"
|
|
echo "node is not installed"
|
|
fi
|
|
|
|
- name: Install git
|
|
if: steps.check_deps.outputs.git_installed != 'true'
|
|
shell: bash
|
|
run: |
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
sudo apt-get update
|
|
sudo apt-get install -y git
|
|
elif command -v brew >/dev/null 2>&1; then
|
|
brew install git
|
|
elif command -v yum >/dev/null 2>&1; then
|
|
sudo yum install -y git
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
sudo apk add --no-cache git
|
|
else
|
|
echo "::error::Unable to install git: no supported package manager found"
|
|
exit 1
|
|
fi
|
|
git --version
|
|
|
|
- name: Setup Node.js
|
|
if: steps.check_deps.outputs.node_installed != 'true'
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ inputs.node_version }}
|
|
|
|
- name: Resolve PR refs
|
|
shell: bash
|
|
env:
|
|
INPUT_BASE_REF: ${{ inputs.base_ref }}
|
|
INPUT_HEAD_SHA: ${{ inputs.head_sha }}
|
|
EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
BASE_REF="${INPUT_BASE_REF:-$EVENT_BASE_REF}"
|
|
HEAD_SHA="${INPUT_HEAD_SHA:-$EVENT_HEAD_SHA}"
|
|
echo "BASE_REF=$BASE_REF" >> "$GITHUB_ENV"
|
|
echo "HEAD_SHA=$HEAD_SHA" >> "$GITHUB_ENV"
|
|
echo "PR base ref: $BASE_REF"
|
|
echo "PR head sha: $HEAD_SHA"
|
|
|
|
- name: Checkout base
|
|
uses: actions/checkout@v4
|
|
with:
|
|
# Checkout the trusted base, not the PR head. OCR reviews the
|
|
# base-to-head diff from git objects; the head commit's blobs are
|
|
# fetched separately so they are resolvable without materializing
|
|
# untrusted PR files into the working tree.
|
|
fetch-depth: 0
|
|
|
|
- name: Fetch PR head (fork-safe)
|
|
if: env.HEAD_SHA != ''
|
|
shell: bash
|
|
env:
|
|
PR_NUM: ${{ github.event.pull_request.number || github.event.issue.number }}
|
|
run: |
|
|
if [ -n "$PR_NUM" ]; then
|
|
git fetch origin "pull/${PR_NUM}/head"
|
|
fi
|
|
|
|
- name: Compute merge-base
|
|
shell: bash
|
|
run: |
|
|
git fetch origin "${BASE_REF}" 2>/dev/null || true
|
|
MERGE_BASE=$(git merge-base "origin/${BASE_REF}" "${HEAD_SHA}" 2>/dev/null || echo "${HEAD_SHA}")
|
|
echo "MERGE_BASE=$MERGE_BASE" >> "$GITHUB_ENV"
|
|
echo "Reviewing ${HEAD_SHA} from merge-base ${MERGE_BASE} (base origin/${BASE_REF})"
|
|
|
|
- name: Install OpenCodeReview
|
|
shell: bash
|
|
env:
|
|
OCR_VERSION: ${{ inputs.ocr_version }}
|
|
run: |
|
|
npm install -g "@alibaba-group/open-code-review@${OCR_VERSION}"
|
|
echo "OpenCodeReview installed:"
|
|
ocr version || true
|
|
|
|
- name: Configure OCR
|
|
env:
|
|
OCR_EXTRA_BODY: ${{ inputs.llm_extra_body }}
|
|
OCR_LANGUAGE: ${{ inputs.language }}
|
|
shell: bash
|
|
run: |
|
|
ocr config set llm.extra_body "$OCR_EXTRA_BODY"
|
|
ocr config set language "$OCR_LANGUAGE"
|
|
|
|
- name: Run OpenCodeReview
|
|
env:
|
|
OCR_LLM_URL: ${{ inputs.llm_url }}
|
|
OCR_LLM_TOKEN: ${{ inputs.llm_auth_token }}
|
|
OCR_LLM_MODEL: ${{ inputs.llm_model }}
|
|
OCR_USE_ANTHROPIC: ${{ inputs.llm_use_anthropic }}
|
|
OCR_LLM_AUTH_HEADER: ${{ inputs.llm_auth_header }}
|
|
OCR_LLM_EXTRA_HEADERS: ${{ inputs.llm_extra_headers }}
|
|
OCR_LLM_TIMEOUT: ${{ inputs.llm_timeout }}
|
|
OCR_REVIEW_CONCURRENCY: ${{ inputs.review_concurrency }}
|
|
OCR_BACKGROUND: ${{ inputs.background }}
|
|
OCR_RULE: ${{ inputs.rule }}
|
|
shell: bash
|
|
run: |
|
|
ARGS=(--from "${MERGE_BASE}" --to "${HEAD_SHA}" --format json)
|
|
[ -n "$OCR_REVIEW_CONCURRENCY" ] && ARGS+=(--concurrency "$OCR_REVIEW_CONCURRENCY")
|
|
[ -n "$OCR_BACKGROUND" ] && ARGS+=(--background "$OCR_BACKGROUND")
|
|
[ -n "$OCR_RULE" ] && ARGS+=(--rule "$OCR_RULE")
|
|
set +e
|
|
ocr review "${ARGS[@]}" > /tmp/ocr-result.json 2>/tmp/ocr-stderr.log
|
|
OCR_EXIT_CODE=$?
|
|
set -e
|
|
echo "OCR_EXIT_CODE=$OCR_EXIT_CODE" >> "$GITHUB_ENV"
|
|
echo "=== OCR result ==="
|
|
cat /tmp/ocr-result.json
|
|
echo "=== OCR stderr ==="
|
|
cat /tmp/ocr-stderr.log
|
|
|
|
- name: Upload review artifacts
|
|
if: ${{ always() && inputs.upload_artifacts == 'true' }}
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ocr-review-result-${{ github.run_id }}-${{ github.run_attempt }}
|
|
path: |
|
|
/tmp/ocr-result.json
|
|
/tmp/ocr-stderr.log
|
|
if-no-files-found: warn
|
|
|
|
- name: Fail job on OCR error
|
|
if: env.OCR_EXIT_CODE != '0'
|
|
shell: bash
|
|
run: |
|
|
echo "ocr review exited with code ${OCR_EXIT_CODE}; see uploaded artifacts for details."
|
|
exit "${OCR_EXIT_CODE}"
|
|
|
|
- name: Post review comments
|
|
if: env.OCR_EXIT_CODE == '0'
|
|
id: post
|
|
uses: actions/github-script@v7
|
|
env:
|
|
OCR_INCREMENTAL_OVERLAP_THRESHOLD: ${{ inputs.incremental_overlap_threshold }}
|
|
with:
|
|
github-token: ${{ inputs.github_token }}
|
|
script: |
|
|
// Locate the helper shipped alongside action.yml at runtime.
|
|
// GITHUB_ACTION_PATH: correct for published (remote) actions and for
|
|
// local actions when not running in a container.
|
|
// GITHUB_WORKSPACE: correct for local `uses: ./` actions — under
|
|
// self-hosted + container setups, GITHUB_ACTION_PATH points to the
|
|
// host path (invisible inside the container), whereas GITHUB_WORKSPACE
|
|
// is correctly mapped to /__w.
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const REL = 'scripts/github-actions/post-review-comments.js';
|
|
const roots = [process.env.GITHUB_ACTION_PATH, process.env.GITHUB_WORKSPACE].filter(Boolean);
|
|
const helper = roots.map(r => path.resolve(r, REL)).find(p => fs.existsSync(p));
|
|
if (!helper) throw new Error(`Could not locate ${REL}; searched roots: ${roots.join(', ')}`);
|
|
const { runPostReviewComments } = require(helper);
|
|
await runPostReviewComments({
|
|
github,
|
|
context,
|
|
core,
|
|
fs,
|
|
resultPath: '/tmp/ocr-result.json',
|
|
stderrPath: '/tmp/ocr-stderr.log',
|
|
stickySummary: ${{ inputs.sticky_summary == 'true' }},
|
|
incremental: ${{ inputs.incremental == 'true' }},
|
|
incrementalOverlapThreshold: parseFloat(process.env.OCR_INCREMENTAL_OVERLAP_THRESHOLD),
|
|
});
|