mirror of
https://github.com/adminlove520/AI-Account-Toolkit.git
synced 2026-05-16 09:26:46 +08:00
feat: 添加 PR 自动审查 workflow
This commit is contained in:
436
.github/workflows/pr-review.yml
vendored
436
.github/workflows/pr-review.yml
vendored
@@ -0,0 +1,436 @@
|
||||
# PR 自动审查 - 语法检查 + AI 语义审查
|
||||
# 当有 PR 创建或更新时自动触发
|
||||
|
||||
name: PR Review
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- '**.py'
|
||||
- '**.md'
|
||||
- 'README.md'
|
||||
- 'requirements.txt'
|
||||
- 'pyproject.toml'
|
||||
- 'setup.cfg'
|
||||
- '.github/workflows/**'
|
||||
- '.github/scripts/**'
|
||||
# 支持手动触发(用于重新审查)
|
||||
workflow_dispatch:
|
||||
|
||||
# 限制并发,避免同一 PR 多次触发时重复评论
|
||||
concurrency:
|
||||
group: pr-review-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
# ==================== 安全检查(检测敏感文件修改)====================
|
||||
security-check:
|
||||
name: 🔒 安全检查
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
safe_to_run: ${{ steps.check_sensitive.outputs.safe_to_run }}
|
||||
sensitive_files_changed: ${{ steps.check_sensitive.outputs.sensitive_files_changed }}
|
||||
|
||||
steps:
|
||||
- name: 📥 检出代码
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🔄 获取 base 分支
|
||||
run: git fetch origin ${{ github.base_ref || 'main' }}:refs/remotes/origin/${{ github.base_ref || 'main' }}
|
||||
|
||||
- name: 🔒 检查敏感文件修改
|
||||
id: check_sensitive
|
||||
run: |
|
||||
BASE_REF="${{ github.base_ref || 'main' }}"
|
||||
SENSITIVE_FILES=$(git diff --name-only origin/$BASE_REF...HEAD | grep -E '^(\.github/workflows/.*\.yml|\.github/scripts/.*\.py)$' || echo "")
|
||||
|
||||
if [ -n "$SENSITIVE_FILES" ]; then
|
||||
echo "⚠️ **检测到敏感文件修改,需要人工审核!**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "修改的敏感文件:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$SENSITIVE_FILES" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "已标记为敏感变更,请重点人工复核;自动流程继续执行。" >> $GITHUB_STEP_SUMMARY
|
||||
echo "sensitive_files_changed=true" >> $GITHUB_OUTPUT
|
||||
echo "safe_to_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "✅ 未检测到敏感文件修改" >> $GITHUB_STEP_SUMMARY
|
||||
echo "sensitive_files_changed=false" >> $GITHUB_OUTPUT
|
||||
echo "safe_to_run=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# ==================== 静态检查(先于 AI 审查)====================
|
||||
auto-check:
|
||||
name: 🔍 静态检查
|
||||
runs-on: ubuntu-latest
|
||||
needs: [security-check]
|
||||
if: needs.security-check.outputs.safe_to_run == 'true'
|
||||
outputs:
|
||||
syntax_ok: ${{ steps.syntax.outputs.syntax_ok }}
|
||||
has_py_changes: ${{ steps.check_files.outputs.has_py_changes }}
|
||||
has_reviewable_changes: ${{ steps.check_files.outputs.has_reviewable_changes }}
|
||||
|
||||
steps:
|
||||
- name: 📥 检出代码
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🔄 获取 base 分支
|
||||
run: git fetch origin ${{ github.base_ref || 'main' }}:refs/remotes/origin/${{ github.base_ref || 'main' }}
|
||||
|
||||
- name: 🐍 设置 Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: 📦 安装依赖
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install flake8
|
||||
|
||||
- name: 📋 检查变更文件
|
||||
id: check_files
|
||||
run: |
|
||||
BASE_REF="${{ github.base_ref || 'main' }}"
|
||||
CHANGED_FILES=$(git diff --name-only origin/$BASE_REF...HEAD -- '*.py' 2>/dev/null || echo "")
|
||||
REVIEWABLE_FILES=$(git diff --name-only origin/$BASE_REF...HEAD -- '*.py' '*.md' 'README.md' 'requirements.txt' 'pyproject.toml' 'setup.cfg' '.github/workflows/**' '.github/scripts/**' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$REVIEWABLE_FILES" ]; then
|
||||
echo "has_reviewable_changes=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_reviewable_changes=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ -z "$CHANGED_FILES" ]; then
|
||||
echo "has_py_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "✅ 没有修改 Python 文件" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "has_py_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "CHANGED_FILES<<EOF" >> $GITHUB_ENV
|
||||
echo "$CHANGED_FILES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: 🐍 Python 语法检查
|
||||
id: syntax
|
||||
if: steps.check_files.outputs.has_py_changes == 'true'
|
||||
run: |
|
||||
echo "## 🐍 语法检查" >> $GITHUB_STEP_SUMMARY
|
||||
echo "检查文件: $CHANGED_FILES"
|
||||
|
||||
ERRORS=""
|
||||
SYNTAX_OK="true"
|
||||
for file in $CHANGED_FILES; do
|
||||
if [ -f "$file" ]; then
|
||||
if ! python -m py_compile "$file" 2>&1; then
|
||||
ERRORS="$ERRORS\n❌ $file 语法错误"
|
||||
SYNTAX_OK="false"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "syntax_ok=$SYNTAX_OK" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "$SYNTAX_OK" = "false" ]; then
|
||||
echo -e "$ERRORS" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "✅ 所有文件语法正确" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: 🔎 Flake8 检查(严重错误)
|
||||
if: steps.check_files.outputs.has_py_changes == 'true'
|
||||
run: |
|
||||
echo "## 🔎 代码质量检查" >> $GITHUB_STEP_SUMMARY
|
||||
RESULT=$(flake8 $CHANGED_FILES --select=E9,F63,F7,F82 --format='%(path)s:%(row)d: %(code)s %(text)s' 2>/dev/null || true)
|
||||
if [ -n "$RESULT" ]; then
|
||||
echo "⚠️ 发现以下问题:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$RESULT" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "✅ 未发现严重代码问题" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: 📊 变更统计
|
||||
run: |
|
||||
BASE_REF="${{ github.base_ref || 'main' }}"
|
||||
echo "## 📊 变更统计" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
STATS=$(git diff --stat origin/$BASE_REF...HEAD 2>/dev/null | tail -1)
|
||||
echo "$STATS" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ==================== AI 代码审查(依赖静态检查通过)====================
|
||||
ai-review:
|
||||
name: 🤖 AI 代码审查
|
||||
runs-on: ubuntu-latest
|
||||
needs: [security-check, auto-check]
|
||||
if: |
|
||||
needs.security-check.outputs.safe_to_run == 'true' &&
|
||||
needs.auto-check.result == 'success' &&
|
||||
needs.auto-check.outputs.has_reviewable_changes == 'true' &&
|
||||
vars.ENABLE_AI_REVIEW != 'false'
|
||||
|
||||
steps:
|
||||
# 先检出主分支(获取最新的 .github/scripts)
|
||||
- name: 📥 检出主分支脚本
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
path: main-scripts
|
||||
|
||||
# 再检出 PR 代码(用于 diff 分析)
|
||||
- name: 📥 检出 PR 代码
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
path: pr-code
|
||||
|
||||
- name: 🔄 获取 base 分支
|
||||
working-directory: pr-code
|
||||
run: git fetch origin ${{ github.base_ref || 'main' }}:refs/remotes/origin/${{ github.base_ref || 'main' }}
|
||||
|
||||
- name: 🐍 设置 Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: 📦 安装依赖
|
||||
run: pip install openai google-genai httpx
|
||||
|
||||
- name: 🤖 AI 审查代码变更
|
||||
working-directory: pr-code
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_BASE_REF: ${{ github.base_ref || 'main' }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }}
|
||||
OPENAI_MODEL: ${{ vars.OPENAI_MODEL }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
|
||||
AI_REVIEW_STRICT: ${{ vars.AI_REVIEW_STRICT || 'false' }}
|
||||
CI_SYNTAX_OK: ${{ needs.auto-check.outputs.syntax_ok || '' }}
|
||||
CI_HAS_PY_CHANGES: ${{ needs.auto-check.outputs.has_py_changes || 'false' }}
|
||||
CI_AUTO_CHECK_RESULT: ${{ needs.auto-check.result || '' }}
|
||||
run: python ../main-scripts/.github/scripts/ai_review.py
|
||||
|
||||
- name: 📤 上传审查结果
|
||||
uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: ai-review-result
|
||||
path: pr-code/ai_review_result.txt
|
||||
if-no-files-found: ignore
|
||||
|
||||
# ==================== PR 标签自动分类 ====================
|
||||
labeler:
|
||||
name: 🏷️ 自动标签
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 📥 检出代码
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: 🏷️ 添加标签
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number
|
||||
});
|
||||
|
||||
const labels = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const filename = file.filename.toLowerCase();
|
||||
|
||||
if (filename.includes('pool') || filename.includes('orchestrator')) {
|
||||
labels.add('pool-orchestrator');
|
||||
}
|
||||
if (filename.includes('mail') || filename.includes('email')) {
|
||||
labels.add('mail-provider');
|
||||
}
|
||||
if (filename.includes('proxy')) {
|
||||
labels.add('proxy');
|
||||
}
|
||||
if (filename.includes('openai') || filename.includes('account')) {
|
||||
labels.add('openai');
|
||||
}
|
||||
if (filename.endsWith('.md') || filename.includes('readme')) {
|
||||
labels.add('documentation');
|
||||
}
|
||||
if (filename.includes('workflow') || filename.includes('.github')) {
|
||||
labels.add('ci-cd');
|
||||
}
|
||||
if (filename.includes('test')) {
|
||||
labels.add('testing');
|
||||
}
|
||||
}
|
||||
|
||||
const additions = files.reduce((sum, f) => sum + f.additions, 0);
|
||||
const deletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
||||
const totalChanges = additions + deletions;
|
||||
|
||||
if (totalChanges < 50) {
|
||||
labels.add('size/S');
|
||||
} else if (totalChanges < 200) {
|
||||
labels.add('size/M');
|
||||
} else if (totalChanges < 500) {
|
||||
labels.add('size/L');
|
||||
} else {
|
||||
labels.add('size/XL');
|
||||
}
|
||||
|
||||
if (labels.size > 0) {
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: Array.from(labels)
|
||||
});
|
||||
console.log(`Added labels: ${Array.from(labels).join(', ')}`);
|
||||
} catch (e) {
|
||||
console.log(`Failed to add labels: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
# ==================== 自动评论检查结果 ====================
|
||||
comment:
|
||||
name: 💬 审查报告
|
||||
runs-on: ubuntu-latest
|
||||
needs: [security-check, auto-check, ai-review]
|
||||
if: always() && github.event_name == 'pull_request_target' && needs.security-check.outputs.safe_to_run == 'true'
|
||||
|
||||
steps:
|
||||
- name: 📥 检出代码
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 📥 下载 AI 审查结果
|
||||
uses: actions/download-artifact@v7
|
||||
if: needs.ai-review.result == 'success'
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: ai-review-result
|
||||
path: .
|
||||
|
||||
- name: 💬 生成审查报告
|
||||
env:
|
||||
AUTO_CHECK_RESULT: ${{ needs.auto-check.result }}
|
||||
AI_REVIEW_RESULT: ${{ needs.ai-review.result }}
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number
|
||||
});
|
||||
|
||||
const additions = files.reduce((sum, f) => sum + f.additions, 0);
|
||||
const deletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
||||
const changedFiles = files.length;
|
||||
|
||||
let aiReview = '';
|
||||
try {
|
||||
aiReview = fs.readFileSync('ai_review_result.txt', 'utf8');
|
||||
} catch (e) {
|
||||
console.log('No AI review result found');
|
||||
}
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
|
||||
const botComment = comments.find(c =>
|
||||
c.user.type === 'Bot' &&
|
||||
c.body.includes('## 🤖 自动审查报告')
|
||||
);
|
||||
|
||||
const checkStatus = process.env.AUTO_CHECK_RESULT === 'success' ? '✅ 通过' : '⚠️ 有问题';
|
||||
const aiStatus = process.env.AI_REVIEW_RESULT === 'success' ? '✅ 已完成' :
|
||||
process.env.AI_REVIEW_RESULT === 'skipped' ? '⏭️ 跳过' : '⚠️ 失败';
|
||||
|
||||
let report = `## 🤖 自动审查报告
|
||||
|
||||
| 项目 | 结果 |
|
||||
|------|------|
|
||||
| 📊 变更文件 | ${changedFiles} 个 |
|
||||
| ➕ 新增行数 | ${additions} 行 |
|
||||
| ➖ 删除行数 | ${deletions} 行 |
|
||||
| 🔍 静态检查 | ${checkStatus} |
|
||||
| 🧠 AI 审查 | ${aiStatus} |
|
||||
|
||||
### 📁 修改的文件
|
||||
|
||||
`;
|
||||
|
||||
for (const file of files.slice(0, 20)) {
|
||||
const status = file.status === 'added' ? '🆕' :
|
||||
file.status === 'removed' ? '🗑️' : '📝';
|
||||
report += `- ${status} \`${file.filename}\` (+${file.additions}/-${file.deletions})\n`;
|
||||
}
|
||||
|
||||
if (files.length > 20) {
|
||||
report += `\n... 还有 ${files.length - 20} 个文件\n`;
|
||||
}
|
||||
|
||||
if (aiReview) {
|
||||
report += `
|
||||
---
|
||||
|
||||
### 🧠 AI 代码审查意见
|
||||
|
||||
${aiReview}
|
||||
`;
|
||||
}
|
||||
|
||||
report += `
|
||||
---
|
||||
|
||||
> 💡 **提示**: 请确保代码已通过本地测试,并遵循项目代码规范。
|
||||
`;
|
||||
|
||||
if (botComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: report
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: report
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user