Skip to content

Impl/topic16

Impl/topic16 #162

Workflow file for this run

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.');
}