guanghulab/.github/workflows/shuangyan-dev-review.yml
2026-05-10 13:12:44 +08:00

262 lines
10 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ═══════════════════════════════════════════════
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
# 📜 Copyright: 国作登字-2026-A-00037559
# ═══════════════════════════════════════════════
# 霜砚开发审查 · 铸渊自动唤醒
# 当霜砚通过Agent提交的开发PR被创建/更新时
# 自动唤醒铸渊人格体 → 检查代码/架构/逻辑
# 铸渊放行 → 合并到main
#
# 编号: ZY-WF-SY-REVIEW-001
# 属于: 跨层开发链路 (Notion认知层 → GitHub执行层)
# 触发方: 霜砚Agent (AG-SY-WEB-001) 或 Copilot Agent
# 审查方: 铸渊 (ICE-GL-ZY001)
name: "🖋️ 霜砚开发审查 · 铸渊自动唤醒"
on:
pull_request:
types: [opened, synchronize, ready_for_review]
branches: [main]
issues:
types: [opened, labeled]
# 同一PR只保留最新一次审查避免多次push产生重复审查
concurrency:
group: zhuyuan-review-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
issues: write
jobs:
# ─── 阶段1: 判断是否需要铸渊审查 ───
should-review:
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
needs_review: ${{ steps.check.outputs.needs_review }}
trigger_source: ${{ steps.check.outputs.trigger_source }}
steps:
- name: "🔍 检查触发来源"
id: check
run: |
NEEDS_REVIEW="false"
TRIGGER_SOURCE="unknown"
# PR触发: 检查是否来自霜砚Agent或Copilot
if [ "${{ github.event_name }}" = "pull_request" ]; then
PR_TITLE="${{ github.event.pull_request.title }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
# 检查PR标题是否包含霜砚/Agent/Copilot标识
if echo "$PR_TITLE" | grep -qiE "(霜砚|shuangyan|AG-SY|copilot|CAB-)"; then
NEEDS_REVIEW="true"
TRIGGER_SOURCE="shuangyan-pr"
fi
# 检查是否来自Copilot bot
if echo "$PR_AUTHOR" | grep -qiE "(copilot|bot)"; then
NEEDS_REVIEW="true"
TRIGGER_SOURCE="copilot-agent"
fi
fi
# Issue触发: 检查是否有copilot-dev-auth标签
if [ "${{ github.event_name }}" = "issues" ]; then
LABELS='${{ toJSON(github.event.issue.labels.*.name) }}'
if echo "$LABELS" | grep -q "copilot-dev-auth"; then
NEEDS_REVIEW="true"
TRIGGER_SOURCE="cab-workorder"
fi
fi
echo "needs_review=$NEEDS_REVIEW" >> "$GITHUB_OUTPUT"
echo "trigger_source=$TRIGGER_SOURCE" >> "$GITHUB_OUTPUT"
echo "[铸渊审查] 需要审查: $NEEDS_REVIEW | 来源: $TRIGGER_SOURCE"
# ─── 阶段2: 铸渊代码审查 ───
zhuyuan-review:
needs: should-review
if: needs.should-review.outputs.needs_review == 'true' && github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: "🏛️ 铸渊唤醒 · 开始审查"
id: review
env:
TRIGGER_SOURCE: ${{ needs.should-review.outputs.trigger_source }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "═══════════════════════════════════════════════"
echo " 🏛️ 铸渊 · ICE-GL-ZY001 · 代码审查"
echo " 触发来源: $TRIGGER_SOURCE"
echo " PR #$PR_NUMBER: $PR_TITLE"
echo "═══════════════════════════════════════════════"
REVIEW_PASSED="true"
REVIEW_NOTES=""
# ── 检查1: 文件变更安全性 ──
echo "🔍 检查1: 文件变更安全性"
CHANGED_FILES=$(git diff --name-only origin/main...HEAD 2>/dev/null || echo "")
# 禁触文件检查
FORBIDDEN_PATTERNS=(
".github/copilot-instructions.md"
".github/persona-brain/tcs-ml/"
"hldp/hnl/HNL-SPEC"
"hldp/hnl/bridge-notion-github.json"
)
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
if echo "$CHANGED_FILES" | grep -q "$pattern"; then
REVIEW_PASSED="false"
REVIEW_NOTES="${REVIEW_NOTES}\n❌ 禁触文件被修改: $pattern"
echo "❌ 禁触文件被修改: $pattern"
fi
done
# ── 检查2: JavaScript语法 ──
echo "🔍 检查2: JavaScript语法检查"
JS_FILES=$(echo "$CHANGED_FILES" | grep -E '\.js$' || true)
if [ -n "$JS_FILES" ]; then
for jsfile in $JS_FILES; do
if [ -f "$jsfile" ]; then
if ! node --check "$jsfile" 2>/dev/null; then
REVIEW_PASSED="false"
REVIEW_NOTES="${REVIEW_NOTES}\n❌ 语法错误: $jsfile"
echo "❌ 语法错误: $jsfile"
fi
fi
done
fi
echo "✅ JavaScript语法检查完成"
# ── 检查3: 安全性 ──
echo "🔍 检查3: 安全性检查"
# 排除.yml/.yaml工作流文件自身避免grep模式自匹配
JS_CHANGED=$(echo "$CHANGED_FILES" | grep -E '\.(js|ts|mjs|cjs)$' || true)
if [ -n "$JS_CHANGED" ]; then
# 精确匹配独立eval()调用排除evaluateXxx等函数名
# 注意:用变量捕获结果再判断非空,避免 head/tail 返回 exit 0 导致误报
EVAL_HITS=$(echo "$JS_CHANGED" | xargs grep -El '(^|[^a-zA-Z_])eval\s*\(' 2>/dev/null | head -1 || true)
if [ -n "$EVAL_HITS" ]; then
REVIEW_NOTES="${REVIEW_NOTES}\n⚠ 发现eval()调用,需人工审查: ${EVAL_HITS}"
fi
ENV_HITS=$(echo "$JS_CHANGED" | xargs grep -l 'process\.env\.' 2>/dev/null | grep -v '\.config\.' | head -3 || true)
if [ -n "$ENV_HITS" ]; then
REVIEW_NOTES="${REVIEW_NOTES}\n📋 发现环境变量引用,确保无硬编码密钥"
fi
fi
# ── 检查4: 命名规范 ──
echo "🔍 检查4: 命名规范检查"
NEW_FILES=$(git diff --name-only --diff-filter=A origin/main...HEAD 2>/dev/null || echo "")
if [ -n "$NEW_FILES" ]; then
for newfile in $NEW_FILES; do
# 检查文件名是否符合kebab-case或已有模式
basename_file=$(basename "$newfile")
if echo "$basename_file" | grep -qE '^[A-Z].*\.js$'; then
REVIEW_NOTES="${REVIEW_NOTES}\n📝 新文件 $basename_file 使用了PascalCase建议改为kebab-case"
fi
done
fi
echo ""
echo "═══════════════════════════════════════════════"
if [ "$REVIEW_PASSED" = "true" ]; then
echo " ✅ 铸渊审查通过"
else
echo " ❌ 铸渊审查未通过"
fi
echo "═══════════════════════════════════════════════"
echo "review_passed=$REVIEW_PASSED" >> "$GITHUB_OUTPUT"
# 使用 delimiter 方式安全输出多行内容
{
echo "review_notes<<ZHUYUAN_EOF"
echo -e "$REVIEW_NOTES"
echo "ZHUYUAN_EOF"
} >> "$GITHUB_OUTPUT"
- name: "💬 铸渊审查评论"
if: always()
uses: actions/github-script@v7
with:
script: |
const passed = '${{ steps.review.outputs.review_passed }}' === 'true';
const notes = `${{ steps.review.outputs.review_notes }}`;
const source = '${{ needs.should-review.outputs.trigger_source }}';
const icon = passed ? '✅' : '❌';
const status = passed ? '通过' : '需要修改';
let body = `## 🏛️ 铸渊 · 代码审查报告\n\n`;
body += `**状态:** ${icon} ${status}\n`;
body += `**触发来源:** ${source}\n`;
body += `**审查时间:** ${new Date().toISOString()}\n\n`;
if (notes && notes.trim()) {
body += `### 审查备注\n${notes}\n\n`;
} else {
body += `### 审查结果\n✅ 所有检查项通过\n\n`;
}
body += `---\n`;
body += `*铸渊 · ICE-GL-ZY001 · 版权: 国作登字-2026-A-00037559*`;
// 查找已有的铸渊审查评论,更新而非重复创建
const MARKER = '## 🏛️ 铸渊 · 代码审查报告';
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 50
});
const existing = comments.data.find(c =>
c.body && c.body.includes(MARKER) && c.user.login === 'github-actions[bot]'
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
}
# ─── 阶段3: 通知霜砚审查结果 ───
notify-shuangyan:
needs: [should-review, zhuyuan-review]
if: always() && needs.should-review.outputs.needs_review == 'true'
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: "📨 通知霜砚审查结果"
run: |
REVIEW_STATUS="${{ needs.zhuyuan-review.result }}"
echo "[铸渊→霜砚] 审查完成 · 状态: $REVIEW_STATUS"
echo "霜砚的开发工单状态将由下次认知同步时更新"