@alibaba-group/open-code-review 1.1.8 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,223 @@
1
+ # OpenCodeReview - GitHub Actions Demo
2
+
3
+ This demo shows how to integrate OpenCodeReview into your GitHub Actions workflow to automatically review Pull Requests and post review comments.
4
+
5
+ ## How It Works
6
+
7
+ ```
8
+ PR Created/Updated → GitHub Actions Triggered → OCR Reviews Diff → Comments Posted on PR
9
+ OR
10
+ Comment with trigger keyword ↗
11
+ ```
12
+
13
+ 1. When a PR is opened, synchronized, or reopened, the workflow triggers
14
+ 2. Alternatively, when a comment containing `/open-code-review` or `@open-code-review` is posted on a PR, the workflow triggers
15
+ 3. It installs OCR via `npm install -g @alibaba-group/open-code-review`
16
+ 4. Runs `ocr review --from origin/<base> --to origin/<head> --format json` to analyze the diff
17
+ 5. Parses the JSON output and posts inline review comments on the PR using GitHub's Pull Request Review API
18
+
19
+ ## Setup
20
+
21
+ ### 1. Copy the workflow file
22
+
23
+ Copy `ocr-review.yml` to your repository's `.github/workflows/` directory:
24
+
25
+ ```bash
26
+ mkdir -p .github/workflows
27
+ cp ocr-review.yml .github/workflows/ocr-review.yml
28
+ ```
29
+
30
+ ### 2. Configure secrets
31
+
32
+ Go to your repository's **Settings → Secrets and variables → Actions** and add:
33
+
34
+ | Secret | Required | Description |
35
+ |--------|----------|-------------|
36
+ | `OCR_LLM_URL` | Yes | LLM API endpoint URL (e.g., `https://api.openai.com/v1/chat/completions`) |
37
+ | `OCR_LLM_AUTH_TOKEN` | Yes | API authentication token |
38
+ | `OCR_LLM_MODEL` | No | Model name (defaults to `gpt-4o`) |
39
+ | `OCR_LLM_USE_ANTHROPIC` | No | Set to `true` if using Anthropic Claude models |
40
+
41
+ > **Note:** `GITHUB_TOKEN` is automatically provided by GitHub Actions with the required `pull-requests: write` permission.
42
+ >
43
+ > The workflow also configures `llm.extra_body` to disable thinking mode for compatibility with various LLM providers.
44
+
45
+ ## Customization
46
+
47
+ ### Change the trigger events
48
+
49
+ Modify the `on.pull_request.types` array in the workflow file:
50
+
51
+ ```yaml
52
+ on:
53
+ pull_request:
54
+ types: [opened, synchronize, reopened, ready_for_review]
55
+ ```
56
+
57
+ ### Customize comment trigger keywords
58
+
59
+ 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:
60
+
61
+ ```yaml
62
+ if: |
63
+ github.event_name == 'pull_request' ||
64
+ (github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/review')) ||
65
+ (github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '@mybot'))
66
+ ```
67
+
68
+ Or use a more flexible pattern with `contains` to trigger on any comment containing the keyword:
69
+
70
+ ```yaml
71
+ if: |
72
+ github.event_name == 'pull_request' ||
73
+ (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '/review'))
74
+ ```
75
+
76
+ > **Note:** The condition `github.event.issue.pull_request` ensures the comment is on a PR, not a regular issue.
77
+
78
+ ### Use a specific OCR version
79
+
80
+ ```yaml
81
+ - name: Install OpenCodeReview
82
+ run: npm install -g @alibaba-group/open-code-review@1.0.0
83
+ ```
84
+
85
+ ### Add custom review rules
86
+
87
+ Use the `--rule` flag to pass a custom rules JSON file:
88
+
89
+ ```yaml
90
+ - name: Run OCR review
91
+ run: ocr review --rule ./my-rules.json --from origin/${{ github.base_ref }} --to origin/${{ github.head_ref }}
92
+ ```
93
+
94
+ ### Limit concurrency
95
+
96
+ Adjust the `--concurrency` flag for large PRs to control the number of concurrent LLM requests:
97
+
98
+ ```yaml
99
+ - name: Run OCR review
100
+ run: ocr review --concurrency 5 --from origin/${{ github.base_ref }} --to origin/${{ github.head_ref }}
101
+ ```
102
+
103
+ ### Provide background context
104
+
105
+ Use the `--background` flag to pass additional context that helps OCR better understand the purpose of the changes:
106
+
107
+ ```yaml
108
+ - name: Run OCR review
109
+ run: ocr review --background "${{ github.event.pull_request.title }}" --from origin/${{ github.base_ref }} --to origin/${{ github.head_ref }}
110
+ ```
111
+
112
+ 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.
113
+
114
+ ### Customize the review comment author with GitHub App
115
+
116
+ 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.
117
+
118
+ For more details about GitHub Apps, see the [GitHub Apps documentation](https://docs.github.com/en/apps).
119
+
120
+ #### Step 1: Create a GitHub App
121
+
122
+ 1. Go to your organization or personal account **Settings → Developer settings → GitHub Apps → New GitHub App**
123
+ 2. Fill in the following:
124
+ - **GitHub App name**: e.g., `OpenCodeReview Bot`
125
+ - **Homepage URL**: Your repository or documentation URL
126
+ - **Webhook**: Uncheck "Active" (not needed for this use case)
127
+ 3. Under **Repository permissions**, set:
128
+ - **Pull requests**: Read and write
129
+ - **Contents**: Read-only (for fetching diffs)
130
+ - **Metadata**: Read-only (required)
131
+ 4. Click **Create GitHub App**
132
+
133
+ #### Step 2: Generate a Private Key
134
+
135
+ 1. After creating the app, scroll down to **Private keys**
136
+ 2. Click **Generate a private key**
137
+ 3. Download and save the `.pem` file securely
138
+
139
+ Note your App ID from the app settings page.
140
+
141
+ #### Step 3: Install the App
142
+
143
+ 1. In the left sidebar, click **Install App**
144
+ 2. Select the repositories where you want to use OCR
145
+ 3. After installation, note the **Installation ID** from the URL (e.g., `https://github.com/settings/installations/12345` → Installation ID is `12345`)
146
+
147
+ #### Step 4: Configure Repository Secrets
148
+
149
+ Add the following secrets to your repository (**Settings → Secrets and variables → Actions**):
150
+
151
+ | Secret | Description |
152
+ |--------|-------------|
153
+ | `GITHUB_APP_ID` | Your GitHub App's ID |
154
+ | `GITHUB_APP_PRIVATE_KEY` | Contents of the `.pem` file (including `-----BEGIN RSA PRIVATE KEY-----` and `-----END RSA PRIVATE KEY-----`) |
155
+ | `GITHUB_APP_INSTALLATION_ID` | The Installation ID from Step 3 |
156
+
157
+ #### Step 5: Update the Workflow
158
+
159
+ Add a step to obtain a token from the GitHub App, then use it in the "Post review comments to PR" step:
160
+
161
+ ```yaml
162
+ - name: Get GitHub App Token
163
+ id: app-token
164
+ uses: actions/create-github-app-token@v1
165
+ with:
166
+ app-id: ${{ secrets.GITHUB_APP_ID }}
167
+ private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
168
+
169
+ - name: Post review comments to PR
170
+ uses: actions/github-script@v7
171
+ with:
172
+ github-token: ${{ steps.app-token.outputs.token }}
173
+ script: |
174
+ # ... existing script
175
+ ```
176
+
177
+ 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.
178
+
179
+ ## Example Output
180
+
181
+ When a PR is reviewed, comments appear directly in the PR's "Files changed" tab:
182
+
183
+ - ✅ If no issues found: A comment saying "No comments generated. Looks good to me."
184
+ - 🔍 If issues found: Inline review comments with suggestions using GitHub's native suggestion syntax
185
+
186
+ ### Inline Comment Example
187
+
188
+ The workflow uses GitHub's `suggestion` code block syntax, so reviewers can apply fixes with one click:
189
+
190
+ ````markdown
191
+ **Suggestion:**
192
+ ```suggestion
193
+ // Fixed code here
194
+ ```
195
+ ````
196
+
197
+ ## Supported LLM Providers
198
+
199
+ OCR supports both OpenAI and Anthropic API formats:
200
+
201
+ - **OpenAI-compatible APIs** (default):
202
+ - OpenAI (GPT-4o, GPT-4, etc.)
203
+ - Azure OpenAI
204
+ - Self-hosted models (vLLM, Ollama, etc.)
205
+ - **Anthropic APIs** (set `OCR_LLM_USE_ANTHROPIC: true`):
206
+ - Anthropic Claude models
207
+
208
+ ## Troubleshooting
209
+
210
+ ### Common Issues
211
+
212
+ 1. **"Failed to parse OCR output"**: Check that `OCR_LLM_URL` and `OCR_LLM_AUTH_TOKEN` secrets are correctly set
213
+ 2. **"Cannot find merge-base"**: Ensure `fetch-depth: 0` is set in the checkout step
214
+ 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
215
+
216
+ ### Debugging
217
+
218
+ Enable debug logging by adding to the OCR review step:
219
+
220
+ ```yaml
221
+ env:
222
+ OCR_DEBUG: "1"
223
+ ```
@@ -0,0 +1,321 @@
1
+ # OpenCodeReview - GitHub Actions PR Auto-Review Demo
2
+ #
3
+ # This workflow automatically reviews pull requests using OpenCodeReview
4
+ # and posts review comments directly on the PR.
5
+ #
6
+ # Triggers:
7
+ # - PR opened, synchronized, or reopened
8
+ # - Comment on PR containing '/open-code-review' or '@open-code-review'
9
+ #
10
+ # Required secrets:
11
+ # OCR_LLM_URL - LLM API endpoint (e.g., https://api.openai.com/v1/chat/completions)
12
+ # OCR_LLM_AUTH_TOKEN - Authentication token for the LLM API
13
+ #
14
+ # Optional secrets:
15
+ # OCR_LLM_MODEL - Model name (default: gpt-4o)
16
+ #
17
+ # Note: GITHUB_TOKEN is automatically provided by GitHub Actions.
18
+
19
+ name: OpenCodeReview PR Review
20
+
21
+ on:
22
+ pull_request:
23
+ types: [opened]
24
+ issue_comment:
25
+ types: [created]
26
+
27
+ permissions:
28
+ contents: read
29
+ pull-requests: write
30
+
31
+ jobs:
32
+ code-review:
33
+ runs-on: ubuntu-latest
34
+ # Run on PR events, or on comments starting with trigger keywords
35
+ if: |
36
+ github.event_name == 'pull_request' ||
37
+ (github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/open-code-review')) ||
38
+ (github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '@open-code-review'))
39
+ steps:
40
+ - name: Get PR context
41
+ id: pr-context
42
+ if: github.event_name != 'pull_request'
43
+ uses: actions/github-script@v7
44
+ with:
45
+ script: |
46
+ // For issue_comment events, get PR info
47
+ const prNumber = context.issue.number;
48
+ const { data: pullRequest } = await github.rest.pulls.get({
49
+ owner: context.repo.owner,
50
+ repo: context.repo.repo,
51
+ pull_number: prNumber
52
+ });
53
+ core.setOutput('base_ref', pullRequest.base.ref);
54
+ core.setOutput('head_ref', pullRequest.head.ref);
55
+ core.setOutput('head_sha', pullRequest.head.sha);
56
+
57
+ - name: Checkout repository
58
+ uses: actions/checkout@v4
59
+ with:
60
+ fetch-depth: 0 # Full history needed for merge-base diff
61
+ ref: ${{ github.event_name != 'pull_request' && steps.pr-context.outputs.head_sha || '' }}
62
+
63
+ - name: Setup Node.js
64
+ uses: actions/setup-node@v4
65
+ with:
66
+ node-version: '20'
67
+
68
+ - name: Install OpenCodeReview
69
+ run: npm install -g @alibaba-group/open-code-review
70
+
71
+ - name: Configure OCR
72
+ run: |
73
+ ocr config set llm.url ${{ secrets.OCR_LLM_URL }}
74
+ ocr config set llm.auth_token ${{ secrets.OCR_LLM_AUTH_TOKEN }}
75
+ ocr config set llm.model ${{ secrets.OCR_LLM_MODEL }}
76
+ ocr config set llm.use_anthropic ${{ secrets.OCR_LLM_USE_ANTHROPIC }}
77
+ ocr config set llm.extra_body '{"thinking": {"type": "disabled"}}'
78
+
79
+ - name: Run OpenCodeReview
80
+ id: review
81
+ run: |
82
+ # Get base and head refs from PR context (different for comment triggers)
83
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
84
+ BASE_REF="${{ github.event.pull_request.base.ref }}"
85
+ HEAD_REF="${{ github.event.pull_request.head.ref }}"
86
+ else
87
+ BASE_REF="${{ steps.pr-context.outputs.base_ref }}"
88
+ HEAD_REF="${{ steps.pr-context.outputs.head_ref }}"
89
+ fi
90
+
91
+ echo "Reviewing PR: ${HEAD_REF} against ${BASE_REF}"
92
+
93
+ # Run OCR in range mode with JSON output
94
+ ocr review \
95
+ --from "origin/${BASE_REF}" \
96
+ --to "origin/${HEAD_REF}" \
97
+ --format json \
98
+ --audience agent \
99
+ > /tmp/ocr-result.json 2>/tmp/ocr-stderr.log || true
100
+
101
+ echo "OCR review completed. Output:"
102
+ cat /tmp/ocr-result.json
103
+ echo "OCR review completed. Error log:"
104
+ cat /tmp/ocr-stderr.log
105
+
106
+ - name: Post review comments to PR
107
+ uses: actions/github-script@v7
108
+ with:
109
+ github-token: ${{ secrets.GITHUB_TOKEN }}
110
+ script: |
111
+ const fs = require('fs');
112
+ const path = '/tmp/ocr-result.json';
113
+
114
+ // Read OCR output (skip first line which is not valid JSON)
115
+ let result;
116
+ try {
117
+ const raw = fs.readFileSync(path, 'utf8');
118
+ const jsonContent = raw.substring(raw.indexOf('\n') + 1);
119
+ result = JSON.parse(jsonContent);
120
+ } catch (e) {
121
+ console.log('Failed to parse OCR output:', e.message);
122
+ // Post a simple comment if parsing fails
123
+ const stderr = fs.readFileSync('/tmp/ocr-stderr.log', 'utf8').trim();
124
+ if (stderr) {
125
+ await github.rest.issues.createComment({
126
+ owner: context.repo.owner,
127
+ repo: context.repo.repo,
128
+ issue_number: context.issue.number,
129
+ body: `⚠️ **OpenCodeReview** encountered an error:\n\`\`\`\n${stderr}\n\`\`\``
130
+ });
131
+ }
132
+ return;
133
+ }
134
+
135
+ const comments = result.comments || [];
136
+ const warnings = result.warnings || [];
137
+
138
+ // If no comments, post a summary
139
+ if (comments.length === 0) {
140
+ const message = result.message || 'No comments generated. Looks good to me.';
141
+ await github.rest.issues.createComment({
142
+ owner: context.repo.owner,
143
+ repo: context.repo.repo,
144
+ issue_number: context.issue.number,
145
+ body: `✅ **OpenCodeReview**: ${message}`
146
+ });
147
+ return;
148
+ }
149
+
150
+ // Prepare PR review with inline comments
151
+ const prNumber = context.issue.number;
152
+ let commitSha;
153
+
154
+ // Get commit SHA from event context
155
+ if (context.eventName === 'pull_request') {
156
+ commitSha = context.payload.pull_request.head.sha;
157
+ } else {
158
+ // For comment events, we need to fetch the PR
159
+ const { data: pullRequest } = await github.rest.pulls.get({
160
+ owner: context.repo.owner,
161
+ repo: context.repo.repo,
162
+ pull_number: prNumber
163
+ });
164
+ commitSha = pullRequest.head.sha;
165
+ }
166
+
167
+ // Build review comments array for the PR review API
168
+ // Only inline comments with line info can be posted via createReview
169
+ const reviewComments = [];
170
+ const commentsWithoutLine = [];
171
+
172
+ for (const comment of comments) {
173
+ const body = formatComment(comment);
174
+
175
+ // Check if comment has valid line information for inline comment (line >= 1)
176
+ const hasValidLine = (comment.start_line >= 1) || (comment.end_line >= 1);
177
+ if (!hasValidLine) {
178
+ commentsWithoutLine.push({ comment, body });
179
+ continue;
180
+ }
181
+
182
+ const reviewComment = {
183
+ path: comment.path,
184
+ body: body
185
+ };
186
+
187
+ // Use line range if available
188
+ if (comment.start_line >= 1 && comment.end_line >= 1 && comment.start_line !== comment.end_line) {
189
+ reviewComment.start_line = comment.start_line;
190
+ reviewComment.line = comment.end_line;
191
+ reviewComment.start_side = 'RIGHT';
192
+ reviewComment.side = 'RIGHT';
193
+ } else if (comment.end_line >= 1) {
194
+ reviewComment.line = comment.end_line;
195
+ reviewComment.side = 'RIGHT';
196
+ } else if (comment.start_line >= 1) {
197
+ reviewComment.line = comment.start_line;
198
+ reviewComment.side = 'RIGHT';
199
+ }
200
+
201
+ reviewComments.push(reviewComment);
202
+ }
203
+
204
+ // Submit as a single PR review with all comments
205
+ const totalCount = comments.length;
206
+ const inlineCount = reviewComments.length;
207
+ const summaryCount = commentsWithoutLine.length;
208
+ let summaryBody = `🔍 **OpenCodeReview** found **${totalCount}** issue(s) in this PR.`;
209
+ if (totalCount > 0) {
210
+ summaryBody += `\n- ✅ ${inlineCount} posted as inline comment(s)`;
211
+ summaryBody += `\n- 📝 ${summaryCount} posted as summary (missing line info)`;
212
+ }
213
+ if (warnings.length > 0) {
214
+ summaryBody += `\n\n⚠️ ${warnings.length} warning(s) occurred during review.`;
215
+ }
216
+
217
+ // Add comments without line info to summary body
218
+ for (const { comment, body } of commentsWithoutLine) {
219
+ summaryBody += '\n\n---\n\n';
220
+ summaryBody += formatCommentMarkdown(comment);
221
+ }
222
+
223
+ // Statistics tracking
224
+ let successCount = 0;
225
+ let failedCount = 0;
226
+ const failedComments = [];
227
+
228
+ try {
229
+ await github.rest.pulls.createReview({
230
+ owner: context.repo.owner,
231
+ repo: context.repo.repo,
232
+ pull_number: prNumber,
233
+ commit_id: commitSha,
234
+ body: summaryBody,
235
+ event: 'COMMENT',
236
+ comments: reviewComments
237
+ });
238
+ successCount = reviewComments.length;
239
+ console.log(`Successfully posted review with ${successCount} inline comments (${commentsWithoutLine.length} in summary)`);
240
+ } catch (e) {
241
+ console.log('Failed to post review with inline comments:', e.message);
242
+ console.log('Falling back to posting comments individually...');
243
+
244
+ // Fallback: post comments one by one
245
+ for (const reviewComment of reviewComments) {
246
+ try {
247
+ await github.rest.pulls.createReview({
248
+ owner: context.repo.owner,
249
+ repo: context.repo.repo,
250
+ pull_number: prNumber,
251
+ commit_id: commitSha,
252
+ body: '',
253
+ event: 'COMMENT',
254
+ comments: [reviewComment]
255
+ });
256
+ successCount++;
257
+ console.log(`Successfully posted comment for ${reviewComment.path}`);
258
+ } catch (innerE) {
259
+ failedCount++;
260
+ failedComments.push({ comment: reviewComment, error: innerE.message });
261
+ console.log(`Failed to post comment for ${reviewComment.path}: ${innerE.message}`);
262
+ }
263
+ }
264
+
265
+ // Post summary comment with statistics
266
+ let finalBody = summaryBody;
267
+ finalBody += `\n\n---\n\n📊 **Posting Statistics:**`;
268
+ finalBody += `\n- ✅ Successfully posted: ${successCount} comment(s)`;
269
+ if (failedCount > 0) {
270
+ finalBody += `\n- ❌ Failed to post: ${failedCount} comment(s)`;
271
+ }
272
+
273
+ // Add failed comments details
274
+ if (failedComments.length > 0) {
275
+ finalBody += '\n\n<details><summary>❌ Failed Comments Details</summary>\n\n';
276
+ for (const { comment, error } of failedComments) {
277
+ finalBody += `- \`${comment.path}\`: ${error}\n`;
278
+ }
279
+ finalBody += '\n</details>';
280
+ }
281
+
282
+ await github.rest.issues.createComment({
283
+ owner: context.repo.owner,
284
+ repo: context.repo.repo,
285
+ issue_number: prNumber,
286
+ body: finalBody
287
+ });
288
+ }
289
+
290
+ function formatComment(comment) {
291
+ let body = comment.content || '';
292
+
293
+ // Add code suggestion if available
294
+ if (comment.suggestion_code && comment.existing_code) {
295
+ body += '\n\n**Suggestion:**\n';
296
+ body += '```suggestion\n';
297
+ body += comment.suggestion_code;
298
+ if (!comment.suggestion_code.endsWith('\n')) body += '\n';
299
+ body += '```';
300
+ }
301
+
302
+ return body;
303
+ }
304
+
305
+ function formatCommentMarkdown(comment) {
306
+ let md = `### 📄 \`${comment.path}\``;
307
+ if (comment.start_line && comment.end_line) {
308
+ md += ` (L${comment.start_line}-L${comment.end_line})`;
309
+ }
310
+ md += '\n\n';
311
+ md += comment.content || '';
312
+
313
+ if (comment.suggestion_code && comment.existing_code) {
314
+ md += '\n\n<details><summary>💡 Suggested Change</summary>\n\n';
315
+ md += '**Before:**\n```\n' + comment.existing_code + '\n```\n\n';
316
+ md += '**After:**\n```\n' + comment.suggestion_code + '\n```\n\n';
317
+ md += '</details>';
318
+ }
319
+
320
+ return md;
321
+ }