@alibaba-group/open-code-review 1.3.14 → 1.3.19
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/README.ja-JP.md +1 -1
- package/README.ko-KR.md +1 -1
- package/README.md +1 -1
- package/README.ru-RU.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/ocr.js +10 -2
- package/package.json +16 -1
- package/scripts/install.js +8 -2
- package/scripts/platform.js +66 -0
- package/scripts/update.js +6 -0
- package/.claude-plugin/marketplace.json +0 -16
- package/CONTRIBUTING.ja-JP.md +0 -218
- package/CONTRIBUTING.ko-KR.md +0 -223
- package/CONTRIBUTING.md +0 -224
- package/CONTRIBUTING.ru-RU.md +0 -224
- package/CONTRIBUTING.zh-CN.md +0 -218
- package/examples/README.md +0 -10
- package/examples/github_actions/README.md +0 -223
- package/examples/github_actions/ocr-review.yml +0 -357
- package/examples/gitlab_ci/.gitlab-ci.yml +0 -244
- package/examples/gitlab_ci/README.md +0 -269
- package/plugins/open-code-review/.claude-plugin/plugin.json +0 -6
- package/plugins/open-code-review/.codex-plugin/plugin.json +0 -34
- package/plugins/open-code-review/CODEX.ko-KR.md +0 -108
- package/plugins/open-code-review/commands/review.md +0 -35
- package/plugins/open-code-review/skills/open-code-review/SKILL.md +0 -236
- package/scripts/github-actions/post-review-comments.test.js +0 -171
- package/skills/open-code-review/SKILL.md +0 -231
|
@@ -1,223 +0,0 @@
|
|
|
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, the workflow triggers (uses `pull_request_target` for fork secret access)
|
|
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 <head_sha> --format json` to analyze the diff (uses commit SHA to support fork PRs)
|
|
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_target.types` array in the workflow file:
|
|
50
|
-
|
|
51
|
-
```yaml
|
|
52
|
-
on:
|
|
53
|
-
pull_request_target:
|
|
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_target' ||
|
|
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_target' ||
|
|
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
|
-
```
|
|
@@ -1,357 +0,0 @@
|
|
|
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 (uses pull_request_target for fork secret access)
|
|
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
|
-
concurrency:
|
|
22
|
-
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
23
|
-
cancel-in-progress: true
|
|
24
|
-
|
|
25
|
-
on:
|
|
26
|
-
# Use pull_request_target instead of pull_request so that secrets are
|
|
27
|
-
# available even for PRs from forks. This is safe because OCR only reads
|
|
28
|
-
# the diff and does not execute any code from the PR.
|
|
29
|
-
pull_request_target:
|
|
30
|
-
types: [opened]
|
|
31
|
-
issue_comment:
|
|
32
|
-
types: [created]
|
|
33
|
-
|
|
34
|
-
permissions:
|
|
35
|
-
contents: read
|
|
36
|
-
pull-requests: write
|
|
37
|
-
|
|
38
|
-
jobs:
|
|
39
|
-
code-review:
|
|
40
|
-
runs-on: ubuntu-latest
|
|
41
|
-
# Run on PR events, or on comments starting with trigger keywords
|
|
42
|
-
if: |
|
|
43
|
-
github.event_name == 'pull_request_target' ||
|
|
44
|
-
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/open-code-review')) ||
|
|
45
|
-
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '@open-code-review'))
|
|
46
|
-
steps:
|
|
47
|
-
- name: Get PR context
|
|
48
|
-
id: pr-context
|
|
49
|
-
if: github.event_name != 'pull_request_target'
|
|
50
|
-
uses: actions/github-script@v7
|
|
51
|
-
with:
|
|
52
|
-
script: |
|
|
53
|
-
// For issue_comment events, get PR info
|
|
54
|
-
const prNumber = context.issue.number;
|
|
55
|
-
const { data: pullRequest } = await github.rest.pulls.get({
|
|
56
|
-
owner: context.repo.owner,
|
|
57
|
-
repo: context.repo.repo,
|
|
58
|
-
pull_number: prNumber
|
|
59
|
-
});
|
|
60
|
-
core.setOutput('base_ref', pullRequest.base.ref);
|
|
61
|
-
core.setOutput('head_ref', pullRequest.head.ref);
|
|
62
|
-
core.setOutput('head_sha', pullRequest.head.sha);
|
|
63
|
-
|
|
64
|
-
- name: Checkout repository
|
|
65
|
-
uses: actions/checkout@v4
|
|
66
|
-
with:
|
|
67
|
-
fetch-depth: 0 # Full history needed for merge-base diff
|
|
68
|
-
ref: ${{ github.event.pull_request.head.sha || steps.pr-context.outputs.head_sha }}
|
|
69
|
-
|
|
70
|
-
- name: Fetch PR head ref (ensures fork commits are available)
|
|
71
|
-
run: git fetch origin pull/${{ github.event.pull_request.number || github.event.issue.number }}/head
|
|
72
|
-
|
|
73
|
-
- name: Setup Node.js
|
|
74
|
-
uses: actions/setup-node@v4
|
|
75
|
-
with:
|
|
76
|
-
node-version: '20'
|
|
77
|
-
|
|
78
|
-
- name: Install OpenCodeReview
|
|
79
|
-
run: npm install -g @alibaba-group/open-code-review
|
|
80
|
-
|
|
81
|
-
- name: Configure OCR
|
|
82
|
-
run: |
|
|
83
|
-
ocr config set llm.url ${{ secrets.OCR_LLM_URL }}
|
|
84
|
-
ocr config set llm.auth_token ${{ secrets.OCR_LLM_AUTH_TOKEN }}
|
|
85
|
-
ocr config set llm.model ${{ secrets.OCR_LLM_MODEL }}
|
|
86
|
-
ocr config set llm.use_anthropic ${{ secrets.OCR_LLM_USE_ANTHROPIC }}
|
|
87
|
-
ocr config set llm.extra_body '{"thinking": {"type": "disabled"}}'
|
|
88
|
-
|
|
89
|
-
- name: Run OpenCodeReview
|
|
90
|
-
id: review
|
|
91
|
-
run: |
|
|
92
|
-
# Get base ref and head SHA from PR context (different for comment triggers)
|
|
93
|
-
# Note: We use HEAD_SHA instead of origin/${HEAD_REF} to support fork PRs,
|
|
94
|
-
# because fork branches don't exist on the origin remote.
|
|
95
|
-
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
|
|
96
|
-
BASE_REF="${{ github.event.pull_request.base.ref }}"
|
|
97
|
-
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
|
|
98
|
-
else
|
|
99
|
-
BASE_REF="${{ steps.pr-context.outputs.base_ref }}"
|
|
100
|
-
HEAD_SHA="${{ steps.pr-context.outputs.head_sha }}"
|
|
101
|
-
fi
|
|
102
|
-
|
|
103
|
-
echo "Reviewing PR: ${HEAD_SHA} against origin/${BASE_REF}"
|
|
104
|
-
|
|
105
|
-
# Run OCR in range mode with JSON output
|
|
106
|
-
ocr review \
|
|
107
|
-
--from "origin/${BASE_REF}" \
|
|
108
|
-
--to "${HEAD_SHA}" \
|
|
109
|
-
--format json \
|
|
110
|
-
> /tmp/ocr-result.json 2>/tmp/ocr-stderr.log || true
|
|
111
|
-
|
|
112
|
-
echo "OCR review completed. Output:"
|
|
113
|
-
cat /tmp/ocr-result.json
|
|
114
|
-
echo "OCR review completed. Error log:"
|
|
115
|
-
cat /tmp/ocr-stderr.log
|
|
116
|
-
|
|
117
|
-
- name: Post review comments to PR
|
|
118
|
-
uses: actions/github-script@v7
|
|
119
|
-
with:
|
|
120
|
-
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
121
|
-
script: |
|
|
122
|
-
const fs = require('fs');
|
|
123
|
-
const path = '/tmp/ocr-result.json';
|
|
124
|
-
|
|
125
|
-
// Read OCR output
|
|
126
|
-
let result;
|
|
127
|
-
try {
|
|
128
|
-
const raw = fs.readFileSync(path, 'utf8');
|
|
129
|
-
result = JSON.parse(raw);
|
|
130
|
-
} catch (e) {
|
|
131
|
-
console.log('Failed to parse OCR output:', e.message);
|
|
132
|
-
// Post a simple comment if parsing fails
|
|
133
|
-
const stderr = fs.readFileSync('/tmp/ocr-stderr.log', 'utf8').trim();
|
|
134
|
-
if (stderr) {
|
|
135
|
-
await github.rest.issues.createComment({
|
|
136
|
-
owner: context.repo.owner,
|
|
137
|
-
repo: context.repo.repo,
|
|
138
|
-
issue_number: context.issue.number,
|
|
139
|
-
body: `⚠️ **OpenCodeReview** encountered an error:\n${fencedBlock(stderr)}`
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const comments = result.comments || [];
|
|
146
|
-
const warnings = result.warnings || [];
|
|
147
|
-
|
|
148
|
-
// If no comments, post a summary
|
|
149
|
-
if (comments.length === 0) {
|
|
150
|
-
const message = result.message || 'No comments generated. Looks good to me.';
|
|
151
|
-
await github.rest.issues.createComment({
|
|
152
|
-
owner: context.repo.owner,
|
|
153
|
-
repo: context.repo.repo,
|
|
154
|
-
issue_number: context.issue.number,
|
|
155
|
-
body: `✅ **OpenCodeReview**: ${message}`
|
|
156
|
-
});
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Prepare PR review with inline comments
|
|
161
|
-
const prNumber = context.issue.number;
|
|
162
|
-
let commitSha;
|
|
163
|
-
|
|
164
|
-
// Get commit SHA from event context
|
|
165
|
-
if (context.eventName === 'pull_request_target') {
|
|
166
|
-
commitSha = context.payload.pull_request.head.sha;
|
|
167
|
-
} else {
|
|
168
|
-
// For comment events, we need to fetch the PR
|
|
169
|
-
const { data: pullRequest } = await github.rest.pulls.get({
|
|
170
|
-
owner: context.repo.owner,
|
|
171
|
-
repo: context.repo.repo,
|
|
172
|
-
pull_number: prNumber
|
|
173
|
-
});
|
|
174
|
-
commitSha = pullRequest.head.sha;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Build review comments array for the PR review API
|
|
178
|
-
// Only inline comments with line info can be posted via createReview
|
|
179
|
-
const reviewComments = [];
|
|
180
|
-
const commentsWithoutLine = [];
|
|
181
|
-
|
|
182
|
-
for (const comment of comments) {
|
|
183
|
-
const body = formatComment(comment);
|
|
184
|
-
|
|
185
|
-
// Check if comment has valid line information for inline comment (line >= 1)
|
|
186
|
-
const hasValidLine = (comment.start_line >= 1) || (comment.end_line >= 1);
|
|
187
|
-
if (!hasValidLine) {
|
|
188
|
-
commentsWithoutLine.push({ comment, body });
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const reviewComment = {
|
|
193
|
-
path: comment.path,
|
|
194
|
-
body: body
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
// Use line range if available
|
|
198
|
-
if (comment.start_line >= 1 && comment.end_line >= 1 && comment.start_line !== comment.end_line) {
|
|
199
|
-
reviewComment.start_line = comment.start_line;
|
|
200
|
-
reviewComment.line = comment.end_line;
|
|
201
|
-
reviewComment.start_side = 'RIGHT';
|
|
202
|
-
reviewComment.side = 'RIGHT';
|
|
203
|
-
} else if (comment.end_line >= 1) {
|
|
204
|
-
reviewComment.line = comment.end_line;
|
|
205
|
-
reviewComment.side = 'RIGHT';
|
|
206
|
-
} else if (comment.start_line >= 1) {
|
|
207
|
-
reviewComment.line = comment.start_line;
|
|
208
|
-
reviewComment.side = 'RIGHT';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
reviewComments.push({ comment, reviewComment });
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Submit as a single PR review with all comments
|
|
215
|
-
const totalCount = comments.length;
|
|
216
|
-
const inlineCount = reviewComments.length;
|
|
217
|
-
const summaryCount = commentsWithoutLine.length;
|
|
218
|
-
let summaryBody = buildSummaryBody(totalCount, inlineCount, summaryCount, warnings);
|
|
219
|
-
|
|
220
|
-
// Add comments without line info to summary body
|
|
221
|
-
summaryBody += formatSummaryComments(commentsWithoutLine);
|
|
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.map(({ reviewComment }) => reviewComment)
|
|
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 { comment, 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, 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 = buildSummaryBody(totalCount, successCount, commentsWithoutLine.length + failedComments.length, warnings);
|
|
267
|
-
finalBody += formatSummaryComments(commentsWithoutLine);
|
|
268
|
-
finalBody += `\n\n---\n\n📊 **Posting Statistics:**`;
|
|
269
|
-
finalBody += `\n- ✅ Successfully posted: ${successCount} comment(s)`;
|
|
270
|
-
if (failedCount > 0) {
|
|
271
|
-
finalBody += `\n- ❌ Failed to post: ${failedCount} comment(s)`;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Add failed comments as summary content so review feedback is not lost.
|
|
275
|
-
if (failedComments.length > 0) {
|
|
276
|
-
finalBody += '\n\n---\n\n### ⚠️ Inline comments shown in summary';
|
|
277
|
-
for (const { comment, error } of failedComments) {
|
|
278
|
-
finalBody += '\n\n---\n\n';
|
|
279
|
-
finalBody += formatCommentMarkdown(comment, error);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
await github.rest.issues.createComment({
|
|
284
|
-
owner: context.repo.owner,
|
|
285
|
-
repo: context.repo.repo,
|
|
286
|
-
issue_number: prNumber,
|
|
287
|
-
body: finalBody
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function formatComment(comment) {
|
|
292
|
-
let body = comment.content || '';
|
|
293
|
-
|
|
294
|
-
// Add code suggestion if available
|
|
295
|
-
if (comment.suggestion_code && comment.existing_code) {
|
|
296
|
-
body += '\n\n**Suggestion:**\n';
|
|
297
|
-
body += fencedBlock(comment.suggestion_code, 'suggestion');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return body;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function formatCommentMarkdown(comment, error) {
|
|
304
|
-
let md = `### 📄 \`${comment.path}\``;
|
|
305
|
-
if (comment.start_line && comment.end_line) {
|
|
306
|
-
md += ` (L${comment.start_line}-L${comment.end_line})`;
|
|
307
|
-
}
|
|
308
|
-
md += '\n\n';
|
|
309
|
-
if (error) {
|
|
310
|
-
md += `⚠️ GitHub could not post this as an inline comment: ${error}\n\n`;
|
|
311
|
-
}
|
|
312
|
-
md += comment.content || '';
|
|
313
|
-
|
|
314
|
-
if (comment.suggestion_code && comment.existing_code) {
|
|
315
|
-
md += '\n\n<details><summary>💡 Suggested Change</summary>\n\n';
|
|
316
|
-
md += '**Before:**\n' + fencedBlock(comment.existing_code) + '\n\n';
|
|
317
|
-
md += '**After:**\n' + fencedBlock(comment.suggestion_code) + '\n\n';
|
|
318
|
-
md += '</details>';
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return md;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function buildSummaryBody(totalCount, inlineCount, summaryCount, warnings) {
|
|
325
|
-
let body = `🔍 **OpenCodeReview** found **${totalCount}** issue(s) in this PR.`;
|
|
326
|
-
if (totalCount > 0) {
|
|
327
|
-
body += `\n- ✅ ${inlineCount} posted as inline comment(s)`;
|
|
328
|
-
body += `\n- 📝 ${summaryCount} posted as summary`;
|
|
329
|
-
}
|
|
330
|
-
if (warnings.length > 0) {
|
|
331
|
-
body += `\n\n⚠️ ${warnings.length} warning(s) occurred during review.`;
|
|
332
|
-
}
|
|
333
|
-
return body;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function formatSummaryComments(summaryComments) {
|
|
337
|
-
let body = '';
|
|
338
|
-
for (const { comment } of summaryComments) {
|
|
339
|
-
body += '\n\n---\n\n';
|
|
340
|
-
body += formatCommentMarkdown(comment);
|
|
341
|
-
}
|
|
342
|
-
return body;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function fencedBlock(content, language = '') {
|
|
346
|
-
const text = String(content || '');
|
|
347
|
-
const fence = safeFence(text);
|
|
348
|
-
let block = fence + language + '\n' + text;
|
|
349
|
-
if (!text.endsWith('\n')) block += '\n';
|
|
350
|
-
return block + fence;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function safeFence(content) {
|
|
354
|
-
const matches = String(content || '').match(/`+/g) || [];
|
|
355
|
-
const maxTicks = matches.reduce((max, ticks) => Math.max(max, ticks.length), 0);
|
|
356
|
-
return '`'.repeat(Math.max(3, maxTicks + 1));
|
|
357
|
-
}
|