262 lines
10 KiB
YAML
262 lines
10 KiB
YAML
# ═══════════════════════════════════════════════
|
||
# 🔺 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 "霜砚的开发工单状态将由下次认知同步时更新"
|