Impl/topic17 #169
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.'); | |
| } |