Skip to content

fix(ci): improve AI code review workflow and CI sync step; revert bad commits #35

fix(ci): improve AI code review workflow and CI sync step; revert bad commits

fix(ci): improve AI code review workflow and CI sync step; revert bad commits #35

name: AI Code Review
on:
workflow_dispatch:
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
ai-code-review:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed files
id: changed
run: |
BASE_REF="${GITHUB_BASE_REF:-main}"
echo "base_ref=$BASE_REF" >> "$GITHUB_OUTPUT"
CHANGED=$(git diff --name-only "origin/$BASE_REF...HEAD")
echo "$CHANGED" > changed_files.txt
COUNT=$(echo "$CHANGED" | wc -l)
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
echo "Changed files ($COUNT):"
echo "$CHANGED"
- name: AI Code Review
id: review
env:
SENSENOVA_API_KEY: ${{ secrets.SENSENOVA_API_KEY }}
run: |
BASE_REF="${GITHUB_BASE_REF:-main}"
# ── 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 ──
DIFF=$(git diff "origin/$BASE_REF...HEAD" -- "$FILE" 2>/dev/null) || { echo " [ERROR] git diff failed for $FILE"; continue; }
# ── Check if binary via git diff ──
if echo "$DIFF" | grep -q "^Binary files "; then
echo " [SKIP] $FILE — binary file (detected by git diff)"
continue
fi
# ── If empty (new file / special chars), show full content ──
if [ -z "$DIFF" ]; then
# Check file type before generating virtual diff
if [ -f "$FILE" ] && file -b --mime-encoding "$FILE" 2>/dev/null | grep -q binary; then
echo " [SKIP] $FILE — binary file (detected by file command)"
continue
fi
LINE_COUNT=$(wc -l < "$FILE" 2>/dev/null) || { echo " [SKIP] $FILE — cannot read"; continue; }
if [ "${LINE_COUNT}" -gt 2000 ]; then
echo " [SKIP] $FILE — too large (${LINE_COUNT} lines, max 2000)"
continue
fi
DIFF="--- /dev/null${NL}+++ b/$FILE${NL}@@ -0,0 +1,$LINE_COUNT @@${NL}$(sed 's/^/+/' "$FILE")"
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 "deepseek-v4-flash" \
--arg system "$SYSTEM_PROMPT" \
--arg user "$USER_MSG" \
'{
model: $model,
messages: [
{role: "system", content: $system},
{role: "user", content: $user}
],
stream: false
}')
# ── Call SenseNova API ──
if [ "${FILE_COUNT:-0}" -gt 0 ]; then
sleep 1 # rate-limit: 1s gap between files
fi
RESPONSE=$(curl -s --max-time 60 --retry 3 --retry-all-errors -w "\n%{http_code}" \
"https://token.sensenova.cn/v1/chat/completions" \
-H "Authorization: Bearer $SENSENOVA_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" != "200" ]; then
FILE_SIZE=$(wc -c < "$FILE" 2>/dev/null || echo "?")
echo " [ERROR] HTTP $HTTP_CODE for $FILE (${FILE_SIZE}B)"
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;
}
// Skip comment for cross-repo PRs (fork → upstream): GITHUB_TOKEN lacks write access
const pr = context.payload.pull_request;
if (pr && pr.head.repo.full_name !== pr.base.repo.full_name) {
console.log('Cross-repo PR detected, skipping comment (GITHUB_TOKEN cannot write to upstream).');
return;
}
// 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.');
}