@alibaba-group/open-code-review 1.1.8 ā 1.1.9
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.
- package/CONTRIBUTING.md +0 -1
- package/CONTRIBUTING.zh-CN.md +0 -1
- package/README.md +301 -45
- package/README.zh-CN.md +21 -0
- package/examples/README.md +10 -0
- package/examples/github_actions/README.md +223 -0
- package/examples/github_actions/ocr-review.yml +321 -0
- package/examples/gitlab_ci/.gitlab-ci.yml +225 -0
- package/examples/gitlab_ci/README.md +268 -0
- package/package.json +2 -4
- package/NPM-README.md +0 -95
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# OpenCodeReview - GitLab CI Merge Request Auto-Review Demo
|
|
2
|
+
#
|
|
3
|
+
# This pipeline automatically reviews Merge Requests using OpenCodeReview
|
|
4
|
+
# and posts review comments (discussions) directly on the MR diff.
|
|
5
|
+
#
|
|
6
|
+
# Required CI/CD Variables (Settings ā CI/CD ā Variables):
|
|
7
|
+
# OCR_LLM_URL - LLM API endpoint (e.g., https://api.openai.com/v1/chat/completions)
|
|
8
|
+
# OCR_LLM_AUTH_TOKEN - Authentication token for the LLM API (mark as "Masked")
|
|
9
|
+
# GITLAB_API_TOKEN - GitLab Personal/Project Access Token with "api" scope
|
|
10
|
+
#
|
|
11
|
+
# Optional CI/CD Variables:
|
|
12
|
+
# OCR_LLM_MODEL - Model name (default: gpt-4o)
|
|
13
|
+
|
|
14
|
+
stages:
|
|
15
|
+
- review
|
|
16
|
+
|
|
17
|
+
code-review:
|
|
18
|
+
stage: review
|
|
19
|
+
image: node:20
|
|
20
|
+
only:
|
|
21
|
+
- merge_requests
|
|
22
|
+
variables:
|
|
23
|
+
GIT_DEPTH: 0 # Full history needed for merge-base diff
|
|
24
|
+
script:
|
|
25
|
+
# Install OpenCodeReview
|
|
26
|
+
- npm install -g @alibaba-group/open-code-review
|
|
27
|
+
|
|
28
|
+
# Configure OCR
|
|
29
|
+
- mkdir -p ~/.open-code-review
|
|
30
|
+
# Gitlab CI/CD does not support confuring variables with value length less than 8, so you can't set use_anthropic as a CI variable
|
|
31
|
+
- |
|
|
32
|
+
ocr config set llm.url $OCR_LLM_URL
|
|
33
|
+
ocr config set llm.auth_token $OCR_LLM_AUTH_TOKEN
|
|
34
|
+
ocr config set llm.model $OCR_LLM_MODEL
|
|
35
|
+
ocr config set llm.use_anthropic false
|
|
36
|
+
ocr config set llm.extra_body '{"thinking": {"type": "disabled"}}'
|
|
37
|
+
|
|
38
|
+
# Run OCR review
|
|
39
|
+
- |
|
|
40
|
+
echo "Reviewing MR: ${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME} against ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}"
|
|
41
|
+
ocr review \
|
|
42
|
+
--from "origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}" \
|
|
43
|
+
--to "origin/${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}" \
|
|
44
|
+
--format json \
|
|
45
|
+
--audience agent \
|
|
46
|
+
> /tmp/ocr-result.json 2>/tmp/ocr-stderr.log || true
|
|
47
|
+
echo "OCR review completed."
|
|
48
|
+
cat /tmp/ocr-result.json
|
|
49
|
+
|
|
50
|
+
# Post review comments to MR
|
|
51
|
+
- |
|
|
52
|
+
python3 << 'PYTHON_SCRIPT'
|
|
53
|
+
import json
|
|
54
|
+
import os
|
|
55
|
+
import sys
|
|
56
|
+
import urllib.request
|
|
57
|
+
import urllib.error
|
|
58
|
+
|
|
59
|
+
GITLAB_URL = os.environ.get("CI_SERVER_URL", "https://gitlab.com")
|
|
60
|
+
PROJECT_ID = os.environ["CI_PROJECT_ID"]
|
|
61
|
+
MR_IID = os.environ["CI_MERGE_REQUEST_IID"]
|
|
62
|
+
API_TOKEN = os.environ["GITLAB_API_TOKEN"]
|
|
63
|
+
SOURCE_BRANCH = os.environ["CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"]
|
|
64
|
+
TARGET_BRANCH = os.environ["CI_MERGE_REQUEST_TARGET_BRANCH_NAME"]
|
|
65
|
+
COMMIT_SHA = os.environ["CI_COMMIT_SHA"]
|
|
66
|
+
|
|
67
|
+
API_BASE = f"{GITLAB_URL}/api/v4/projects/{PROJECT_ID}/merge_requests/{MR_IID}"
|
|
68
|
+
|
|
69
|
+
def api_request(endpoint, data=None, method="POST"):
|
|
70
|
+
"""Make a GitLab API request."""
|
|
71
|
+
url = f"{API_BASE}{endpoint}"
|
|
72
|
+
headers = {
|
|
73
|
+
"PRIVATE-TOKEN": API_TOKEN,
|
|
74
|
+
"Content-Type": "application/json"
|
|
75
|
+
}
|
|
76
|
+
body = json.dumps(data).encode("utf-8") if data else None
|
|
77
|
+
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
78
|
+
try:
|
|
79
|
+
with urllib.request.urlopen(req) as resp:
|
|
80
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
81
|
+
except urllib.error.HTTPError as e:
|
|
82
|
+
print(f"API error {e.code}: {e.read().decode('utf-8')}", file=sys.stderr)
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
def post_note(body):
|
|
86
|
+
"""Post a general note/comment on the MR."""
|
|
87
|
+
return api_request("/notes", {"body": body})
|
|
88
|
+
|
|
89
|
+
def post_discussion(path, line, body, base_sha=None, start_sha=None, head_sha=None):
|
|
90
|
+
"""Post an inline discussion on a specific file/line in the MR diff."""
|
|
91
|
+
position = {
|
|
92
|
+
"position_type": "text",
|
|
93
|
+
"new_path": path,
|
|
94
|
+
"old_path": path,
|
|
95
|
+
"new_line": line,
|
|
96
|
+
"base_sha": base_sha or TARGET_BRANCH,
|
|
97
|
+
"start_sha": start_sha or TARGET_BRANCH,
|
|
98
|
+
"head_sha": head_sha or COMMIT_SHA,
|
|
99
|
+
}
|
|
100
|
+
data = {
|
|
101
|
+
"body": body,
|
|
102
|
+
"position": position
|
|
103
|
+
}
|
|
104
|
+
return api_request("/discussions", data)
|
|
105
|
+
|
|
106
|
+
def format_comment(comment):
|
|
107
|
+
"""Format a single review comment as markdown."""
|
|
108
|
+
body = comment.get("content", "")
|
|
109
|
+
|
|
110
|
+
existing = comment.get("existing_code", "")
|
|
111
|
+
suggestion = comment.get("suggestion_code", "")
|
|
112
|
+
if suggestion and existing:
|
|
113
|
+
body += "\n\n**Suggestion:**\n"
|
|
114
|
+
body += f"```suggestion:-0+0\n{suggestion}\n```"
|
|
115
|
+
|
|
116
|
+
return body
|
|
117
|
+
|
|
118
|
+
def format_comment_fallback(comment):
|
|
119
|
+
"""Format a comment for fallback (non-inline) display."""
|
|
120
|
+
path = comment.get("path", "unknown")
|
|
121
|
+
start_line = comment.get("start_line", 0)
|
|
122
|
+
end_line = comment.get("end_line", 0)
|
|
123
|
+
content = comment.get("content", "")
|
|
124
|
+
|
|
125
|
+
md = f"### š `{path}`"
|
|
126
|
+
if start_line and end_line:
|
|
127
|
+
md += f" (L{start_line}-L{end_line})"
|
|
128
|
+
md += f"\n\n{content}"
|
|
129
|
+
|
|
130
|
+
existing = comment.get("existing_code", "")
|
|
131
|
+
suggestion = comment.get("suggestion_code", "")
|
|
132
|
+
if suggestion and existing:
|
|
133
|
+
md += "\n\n<details><summary>š” Suggested Change</summary>\n\n"
|
|
134
|
+
md += f"**Before:**\n```\n{existing}\n```\n\n"
|
|
135
|
+
md += f"**After:**\n```\n{suggestion}\n```\n\n"
|
|
136
|
+
md += "</details>"
|
|
137
|
+
|
|
138
|
+
return md
|
|
139
|
+
|
|
140
|
+
# --- Main ---
|
|
141
|
+
|
|
142
|
+
# Read OCR result (skip first line which is summary, not JSON)
|
|
143
|
+
try:
|
|
144
|
+
with open("/tmp/ocr-result.json", "r") as f:
|
|
145
|
+
next(f) # Skip first line
|
|
146
|
+
result = json.load(f)
|
|
147
|
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
|
148
|
+
print(f"Failed to parse OCR output: {e}", file=sys.stderr)
|
|
149
|
+
stderr_content = ""
|
|
150
|
+
try:
|
|
151
|
+
with open("/tmp/ocr-stderr.log", "r") as f:
|
|
152
|
+
stderr_content = f.read().strip()
|
|
153
|
+
except FileNotFoundError:
|
|
154
|
+
pass
|
|
155
|
+
if stderr_content:
|
|
156
|
+
post_note(f"ā ļø **OpenCodeReview** encountered an error:\n```\n{stderr_content}\n```")
|
|
157
|
+
sys.exit(0)
|
|
158
|
+
|
|
159
|
+
comments = result.get("comments", [])
|
|
160
|
+
warnings = result.get("warnings", [])
|
|
161
|
+
|
|
162
|
+
# No comments - post summary
|
|
163
|
+
if not comments:
|
|
164
|
+
message = result.get("message", "No comments generated. Looks good to me.")
|
|
165
|
+
post_note(f"ā
**OpenCodeReview**: {message}")
|
|
166
|
+
print("No review comments to post.")
|
|
167
|
+
sys.exit(0)
|
|
168
|
+
|
|
169
|
+
# Get MR diff metadata for position calculation
|
|
170
|
+
diff_refs = None
|
|
171
|
+
try:
|
|
172
|
+
versions_url = f"{API_BASE}/versions"
|
|
173
|
+
req = urllib.request.Request(versions_url, headers={"PRIVATE-TOKEN": API_TOKEN})
|
|
174
|
+
with urllib.request.urlopen(req) as resp:
|
|
175
|
+
versions = json.loads(resp.read().decode("utf-8"))
|
|
176
|
+
if versions:
|
|
177
|
+
latest = versions[0]
|
|
178
|
+
diff_refs = {
|
|
179
|
+
"base_sha": latest.get("base_commit_sha", ""),
|
|
180
|
+
"start_sha": latest.get("start_commit_sha", ""),
|
|
181
|
+
"head_sha": latest.get("head_commit_sha", ""),
|
|
182
|
+
}
|
|
183
|
+
except Exception as e:
|
|
184
|
+
print(f"Warning: Could not fetch MR versions: {e}", file=sys.stderr)
|
|
185
|
+
|
|
186
|
+
# Post inline discussions for each comment
|
|
187
|
+
success_count = 0
|
|
188
|
+
failed_comments = []
|
|
189
|
+
|
|
190
|
+
for comment in comments:
|
|
191
|
+
path = comment.get("path", "")
|
|
192
|
+
end_line = comment.get("end_line", 0)
|
|
193
|
+
start_line = comment.get("start_line", end_line)
|
|
194
|
+
body = format_comment(comment)
|
|
195
|
+
|
|
196
|
+
if not path or not end_line:
|
|
197
|
+
failed_comments.append(comment)
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
kwargs = {}
|
|
201
|
+
if diff_refs:
|
|
202
|
+
kwargs = diff_refs
|
|
203
|
+
|
|
204
|
+
result_resp = post_discussion(path, end_line, body, **kwargs)
|
|
205
|
+
if result_resp:
|
|
206
|
+
success_count += 1
|
|
207
|
+
else:
|
|
208
|
+
failed_comments.append(comment)
|
|
209
|
+
|
|
210
|
+
print(f"Successfully posted {success_count}/{len(comments)} inline comments.")
|
|
211
|
+
|
|
212
|
+
# Post fallback for any failed inline comments
|
|
213
|
+
if failed_comments:
|
|
214
|
+
fallback_body = f"š **OpenCodeReview** found issues that could not be posted inline:\n\n---\n\n"
|
|
215
|
+
for comment in failed_comments:
|
|
216
|
+
fallback_body += format_comment_fallback(comment) + "\n\n---\n\n"
|
|
217
|
+
post_note(fallback_body)
|
|
218
|
+
|
|
219
|
+
# Post summary
|
|
220
|
+
summary = f"š **OpenCodeReview** found **{len(comments)}** issue(s) in this MR."
|
|
221
|
+
if warnings:
|
|
222
|
+
summary += f"\n\nā ļø {len(warnings)} warning(s) occurred during review."
|
|
223
|
+
post_note(summary)
|
|
224
|
+
|
|
225
|
+
PYTHON_SCRIPT
|