-
Notifications
You must be signed in to change notification settings - Fork 25
235 lines (204 loc) · 8.25 KB
/
Copy pathai-code-review.yml
File metadata and controls
235 lines (204 loc) · 8.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
name: AI Code Review
on:
workflow_dispatch:
pull_request_target:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
ai-code-review:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
# default checkout in pull_request_target = base branch
# only needed for reading .github/prompts/code-review-prompt.md
- name: Fetch PR diff data
id: fetch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
# 一次性获取所有文件的 patch 数据,存为 JSON 避免重复调用 API
gh api "/repos/${{ github.repository }}/pulls/$PR_NUMBER/files" > pr_files.json
jq -r '.[].filename' pr_files.json > changed_files.txt
COUNT=$(wc -l < changed_files.txt)
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
echo "Changed files ($COUNT):"
cat changed_files.txt
- name: AI Code Review
id: review
env:
SENSENOVA_API_KEY: ${{ secrets.SENSENOVA_API_KEY }}
run: |
# ── Inline system prompt ──
SYSTEM_PROMPT=$(cat .github/prompts/code-review-prompt.md)
REVIEW_FILE="review_output.md"
: > "$REVIEW_FILE"
HAS_REVIEW=false
FILE_COUNT=0
MAX_REVIEW_FILES=10
SKIPPED_FILES=""
CHANGED_FILES=$(cat changed_files.txt)
# ── Check API key early ──
if [ -z "$SENSENOVA_API_KEY" ] || [ "${#SENSENOVA_API_KEY}" -lt 8 ]; then
echo "::error::SENSENOVA_API_KEY is not set or invalid. Aborting."
exit 1
fi
if [ -z "$CHANGED_FILES" ]; then
echo "No files changed, skipping review." >> "$REVIEW_FILE"
else
while IFS= read -r FILE; do
NL=$'\n'
[ -z "$FILE" ] && continue
if [ "${FILE_COUNT}" -ge "${MAX_REVIEW_FILES}" ]; then
echo " [SKIP] $FILE — max ${MAX_REVIEW_FILES} files reached"
SKIPPED_FILES="${SKIPPED_FILES} - $FILE${NL}"
continue
fi
echo "Reviewing: $FILE"
# ── Get per-file diff from cached JSON ──
DIFF=$(jq -r '.[] | select(.filename == "'"$FILE"'") | .patch // empty' pr_files.json)
# ── Skip if binary or not diffable (patch is null/empty) ──
if [ -z "$DIFF" ]; then
echo " [SKIP] $FILE — binary or not diffable (no patch from API)"
continue
fi
# ── Build user message ──
USER_MSG="请 review 以下文件变更:${NL}${NL}文件: $FILE${NL}${NL}\`\`\`diff${NL}$DIFF${NL}\`\`\`"
# ── Escape for JSON ──
# Use jq to build the payload safely
PAYLOAD=$(jq -n \
--arg model "sensenova-6.8-flash-lite" \
--arg system "$SYSTEM_PROMPT" \
--arg user "$USER_MSG" \
'{
model: $model,
messages: [
{role: "system", content: $system},
{role: "user", content: $user}
],
stream: false
}') || { echo " [ERROR] Failed to build JSON payload for $FILE"; continue; }
# ── Call SenseNova API ──
if [ "${FILE_COUNT:-0}" -gt 0 ]; then
sleep 1 # rate-limit: 1s gap between files
fi
RESPONSE=$(curl -s --max-time 90 --retry 0 -w "\n%{http_code}" \
"https://token.sensenova.cn/v1/chat/completions" \
-H "Authorization: Bearer $SENSENOVA_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD") || { echo " [WARN] curl failed for $FILE (exit $?) — skipped (no result)"; continue; }
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" != "200" ]; then
echo " [ERROR] HTTP $HTTP_CODE for $FILE"
echo " Response: $(echo "$BODY" | head -3)"
continue
fi
# ── Extract review text from response ──
REVIEW_TEXT=$(echo "$BODY" | jq -r '.choices[0].message.content // empty')
if [ -z "$REVIEW_TEXT" ]; then
echo " [WARN] Empty response for $FILE"
continue
fi
# ── Append to review output ──
{
echo "## 📁 \`$FILE\`"
echo ""
echo "$REVIEW_TEXT"
echo ""
echo "---"
echo ""
} >> "$REVIEW_FILE"
HAS_REVIEW=true
FILE_COUNT=$((FILE_COUNT + 1))
echo " [OK] Review completed for $FILE"
done <<< "$CHANGED_FILES"
fi
# ── Final summary ──
{
echo "# 🤖 AI Code Review"
echo ""
echo "> 共审查 **${FILE_COUNT}** 个变更文件"
if [ -n "$SKIPPED_FILES" ]; then
SKIP_COUNT=$(echo "$SKIPPED_FILES" | grep -c ' - ' || true)
echo "> ⚠️ 另有 **${SKIP_COUNT}** 个文件超过上限(最多 ${MAX_REVIEW_FILES} 个)未审查"
fi
echo ""
if [ "$HAS_REVIEW" = true ]; then
cat "$REVIEW_FILE"
else
echo "No reviewable changes found (all files binary, unchanged, or empty)."
fi
if [ -n "$SKIPPED_FILES" ]; then
echo ""
echo "---"
echo "### ⚠️ 未审查的文件"
printf '%b' "$SKIPPED_FILES"
fi
} > "${REVIEW_FILE}.tmp"
mv "${REVIEW_FILE}.tmp" "$REVIEW_FILE"
echo "Review written to $REVIEW_FILE"
- name: Upload review artifact
uses: actions/upload-artifact@v4
with:
name: ai-code-review
path: review_output.md
if-no-files-found: ignore
- name: Post review comment
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const reviewFile = 'review_output.md';
if (!fs.existsSync(reviewFile)) {
console.log('No review output file found, skipping comment.');
return;
}
const reviewBody = fs.readFileSync(reviewFile, 'utf8').trim();
if (!reviewBody) {
console.log('Empty review output, skipping comment.');
return;
}
const pr = context.payload.pull_request;
// For pull_request events, get the PR number
let issueNumber;
if (pr) {
issueNumber = pr.number;
} else {
console.log('Not a pull request event, skipping comment.');
return;
}
// Check if we already have a review comment from this workflow
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100,
});
const botComment = comments.data.find(c =>
c.user.type === 'Bot' &&
c.body.startsWith('# 🤖 AI Code Review')
);
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: reviewBody,
});
console.log('Updated existing review comment.');
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: reviewBody,
});
console.log('Created new review comment.');
}