Repository navigation
feat: add AI code review workflow #2
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: | |
| 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 | |
| CHANGED_FILES=$(cat changed_files.txt) | |
| if [ -z "$CHANGED_FILES" ]; then | |
| echo "No files changed, skipping review." >> "$REVIEW_FILE" | |
| else | |
| while IFS= read -r FILE; do | |
| [ -z "$FILE" ] && continue | |
| echo "Reviewing: $FILE" | |
| # ── Get per-file diff ── | |
| DIFF=$(git diff "origin/$BASE_REF...HEAD" -- "$FILE" 2>/dev/null || true) | |
| if [ -z "$DIFF" ]; then | |
| echo " [SKIP] $FILE — no diff (binary or unchanged)" | |
| continue | |
| fi | |
| # ── Checkif binary ── | |
| if echo "$DIFF" | head -1 | grep -q "^Binary files"; then | |
| echo " [SKIP] $FILE — binary file" | |
| continue | |
| fi | |
| # ── Build user message ── | |
| USER_MSG="请 review 以下文件变更:\n\n文件: $FILE\n\n\`\`\`diff\n$DIFF\n\`\`\`" | |
| # ── 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 ── | |
| RESPONSE=$(curl -s -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 | |
| echo " [ERROR] HTTP $HTTP_CODE for $FILE" | |
| echo " Response: $(echo "$BODY" | head -5)" | |
| 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 ── | |
| if [ "$HAS_REVIEW" = false ]; then | |
| printf '%s\n' '## 🤖 AI Code Review' '' 'No reviewable changes found (all files binary, unchanged, or empty).' > "$REVIEW_FILE" | |
| else | |
| SUMMARY=$(printf '# 🤖 AI Code Review\n\n> 共审查 **%s** 个变更文件\n\n' "${FILE_COUNT}") | |
| { | |
| echo "$SUMMARY" | |
| echo "" | |
| cat "$REVIEW_FILE" | |
| } > "${REVIEW_FILE}.tmp" && mv "${REVIEW_FILE}.tmp" "$REVIEW_FILE" | |
| fi | |
| echo "Review written to $REVIEW_FILE" | |
| - 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; | |
| } | |
| // For pull_request events, get the PR number | |
| let issueNumber; | |
| if (context.payload.pull_request) { | |
| issueNumber = context.payload.pull_request.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.'); | |
| } |