guanghulab/.github/scripts/tianyan-review.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

423 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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.

#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// .github/scripts/tianyan-review.js
// Phase D — 天眼审核修复代码 (H1-H9 Checklist)
//
// 输入: /tmp/tianyan/repair-result.json + git diff on repair branch
// 输出: /tmp/tianyan/review-result.json
//
// 天眼检查清单 v3.0:
// H1 → Secrets 安全:无硬编码令牌
// H2 → 文件完整性YAML/JSON/JS 语法正确
// H3 → 依赖安全:无新增不受信依赖
// H4 → 权限最小化Workflow permissions 未被扩大
// H5 → 天眼自保护tianyan-*.yml 文件未被修改
// H6 → 变更范围:总变更行数 < 200单文件 < 50
// H7 → Secrets引用完整性修改前后 secrets.* 引用数量一致
// H8 → 注释规范:每个修复位置都有 [AUTO-FIX] 注释
// H9 → 无残留调试代码:无 console.log、echo debug 等临时代码
'use strict';
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const ROOT = path.resolve(__dirname, '../..');
const TMP_DIR = '/tmp/tianyan';
const CONFIG_PATH = path.join(ROOT, '.github/tianyan-config.json');
// ━━━ 工具函数 ━━━
function loadJSON(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
function getTimestamp() {
return new Date().toISOString();
}
function gitExec(cmd) {
try {
return execSync(cmd, { cwd: ROOT, encoding: 'utf8', stdio: 'pipe' }).trim();
} catch (e) {
return '';
}
}
// ━━━ H1: Secrets 安全 ━━━
function checkH1_SecretsHardcoded(modifiedFiles) {
const result = { id: 'H1', name: 'Secrets安全', pass: true, details: [] };
const tokenPatterns = [
{ pattern: /ghp_[A-Za-z0-9]{36}/g, type: 'GitHub PAT' },
{ pattern: /github_pat_[A-Za-z0-9_]+/g, type: 'GitHub Fine-grained PAT' },
{ pattern: /gho_[A-Za-z0-9]+/g, type: 'GitHub OAuth token' },
{ pattern: /sk-[A-Za-z0-9]{40,}/g, type: 'API key (sk-)' },
{ pattern: /AIza[A-Za-z0-9_-]{35}/g, type: 'Google API key' }
];
for (const filePath of modifiedFiles) {
const absPath = path.join(ROOT, filePath);
if (!fs.existsSync(absPath)) continue;
const content = fs.readFileSync(absPath, 'utf8');
for (const { pattern, type } of tokenPatterns) {
const matches = content.match(pattern);
if (matches) {
result.pass = false;
result.details.push(`${filePath}: Found hardcoded ${type}`);
}
}
}
if (result.pass) result.details.push('No hardcoded tokens found');
return result;
}
// ━━━ H2: 文件完整性 ━━━
function checkH2_FileSyntax(modifiedFiles) {
const result = { id: 'H2', name: '文件完整性', pass: true, details: [] };
for (const filePath of modifiedFiles) {
const absPath = path.join(ROOT, filePath);
if (!fs.existsSync(absPath)) continue;
const content = fs.readFileSync(absPath, 'utf8');
const ext = path.extname(filePath);
try {
if (ext === '.json') {
JSON.parse(content);
} else if (ext === '.js') {
execSync(`node --check "${absPath}"`, { stdio: 'pipe' });
} else if (ext === '.yml' || ext === '.yaml') {
if (content.includes('\t')) {
throw new Error('YAML contains tab characters');
}
// Basic YAML structural check: must have at least one key-value or comment
const trimmed = content.trim();
if (trimmed.length > 0) {
const hasValidStructure = trimmed.split('\n').some(line => {
const l = line.trim();
return l.startsWith('#') || l.startsWith('---') || l.includes(':') || l.startsWith('-');
});
if (!hasValidStructure) {
throw new Error('YAML does not contain valid structure');
}
}
}
result.details.push(`${filePath}: ✅ syntax OK`);
} catch (e) {
result.pass = false;
result.details.push(`${filePath}: ❌ ${e.message}`);
}
}
return result;
}
// ━━━ H3: 依赖安全 ━━━
function checkH3_DependencySafety(modifiedFiles) {
const result = { id: 'H3', name: '依赖安全', pass: true, details: [] };
for (const filePath of modifiedFiles) {
if (!filePath.endsWith('.yml') && !filePath.endsWith('.yaml')) continue;
const absPath = path.join(ROOT, filePath);
if (!fs.existsSync(absPath)) continue;
const content = fs.readFileSync(absPath, 'utf8');
// Check for new uses: actions that aren't from trusted sources
const usesMatches = content.match(/uses:\s*([^\s#]+)/g) || [];
for (const use of usesMatches) {
const action = use.replace('uses:', '').trim();
// Allow official actions/* and known trusted actions
const trusted = ['actions/', 'github/', 'peter-evans/', 'dawidd6/', 'peaceiris/'];
const isTrusted = trusted.some(t => action.startsWith(t));
if (!isTrusted && !action.startsWith('./')) {
result.details.push(`${filePath}: ⚠️ External action: ${action}`);
// Don't fail, just warn
}
}
}
if (result.details.length === 0) result.details.push('No dependency concerns');
return result;
}
// ━━━ H4: 权限最小化 ━━━
function checkH4_PermissionsMinimal(modifiedFiles) {
const result = { id: 'H4', name: '权限最小化', pass: true, details: [] };
for (const filePath of modifiedFiles) {
if (!filePath.endsWith('.yml') && !filePath.endsWith('.yaml')) continue;
const absPath = path.join(ROOT, filePath);
if (!fs.existsSync(absPath)) continue;
const content = fs.readFileSync(absPath, 'utf8');
// Check for overly broad permissions
if (content.includes('permissions: write-all') || content.includes('permissions:\n contents: write\n actions: write\n issues: write\n pull-requests: write')) {
result.pass = false;
result.details.push(`${filePath}: ❌ Overly broad permissions detected`);
}
}
if (result.pass) result.details.push('Permissions within acceptable scope');
return result;
}
// ━━━ H5: 天眼自保护 ━━━
function checkH5_TianyanProtection(modifiedFiles) {
const result = { id: 'H5', name: '天眼自保护', pass: true, details: [] };
for (const filePath of modifiedFiles) {
const fileName = path.basename(filePath);
if (fileName.match(/^tianyan-.*\.yml$/)) {
result.pass = false;
result.details.push(`${filePath}: ❌ tianyan workflow file was modified`);
}
}
if (result.pass) result.details.push('No tianyan workflow files modified');
return result;
}
// ━━━ H6: 变更范围 ━━━
function checkH6_ChangeScope(modifiedFiles, config) {
const result = { id: 'H6', name: '变更范围', pass: true, details: [] };
let totalChanged = 0;
const maxPerFile = config.safety.max_lines_per_file || 50;
const maxTotal = config.safety.max_total_changed_lines || 200;
for (const filePath of modifiedFiles) {
const diff = gitExec(`git diff --numstat main -- "${filePath}"`) || gitExec(`git diff --numstat HEAD -- "${filePath}"`);
if (!diff) continue;
const parts = diff.split('\t');
const added = parseInt(parts[0]) || 0;
const deleted = parseInt(parts[1]) || 0;
const fileChanged = added + deleted;
totalChanged += fileChanged;
if (fileChanged > maxPerFile) {
result.pass = false;
result.details.push(`${filePath}: ❌ ${fileChanged} lines changed (max ${maxPerFile})`);
} else {
result.details.push(`${filePath}: ✅ ${fileChanged} lines changed`);
}
}
if (totalChanged > maxTotal) {
result.pass = false;
result.details.push(`Total: ❌ ${totalChanged} lines (max ${maxTotal})`);
} else {
result.details.push(`Total: ✅ ${totalChanged} lines`);
}
return result;
}
// ━━━ H7: Secrets 引用完整性 ━━━
function checkH7_SecretsIntegrity(modifiedFiles) {
const result = { id: 'H7', name: 'Secrets引用完整性', pass: true, details: [] };
for (const filePath of modifiedFiles) {
if (!filePath.endsWith('.yml') && !filePath.endsWith('.yaml')) continue;
// Get original content from git (use main branch as reference)
const originalContent = gitExec(`git show main:"${filePath}" 2>/dev/null`) || gitExec(`git show HEAD:"${filePath}" 2>/dev/null`);
const absPath = path.join(ROOT, filePath);
if (!fs.existsSync(absPath)) continue;
const newContent = fs.readFileSync(absPath, 'utf8');
const origSecrets = (originalContent.match(/secrets\.\w+/g) || []).sort();
const newSecrets = (newContent.match(/secrets\.\w+/g) || []).sort();
const removed = origSecrets.filter(s => !newSecrets.includes(s));
const added = newSecrets.filter(s => !origSecrets.includes(s));
if (removed.length > 0) {
result.pass = false;
result.details.push(`${filePath}: ❌ Secrets removed: ${removed.join(', ')}`);
}
if (added.length > 0) {
result.pass = false;
result.details.push(`${filePath}: ❌ Secrets added: ${added.join(', ')}`);
}
if (removed.length === 0 && added.length === 0) {
result.details.push(`${filePath}: ✅ Secrets references intact`);
}
}
return result;
}
// ━━━ H8: 注释规范 ━━━
function checkH8_AutoFixComments(modifiedFiles) {
const result = { id: 'H8', name: '注释规范', pass: true, details: [] };
let hasModifiedContent = false;
for (const filePath of modifiedFiles) {
const absPath = path.join(ROOT, filePath);
if (!fs.existsSync(absPath)) continue;
const content = fs.readFileSync(absPath, 'utf8');
if (content.includes('[AUTO-FIX]')) {
result.details.push(`${filePath}: ✅ [AUTO-FIX] comment found`);
hasModifiedContent = true;
}
}
if (!hasModifiedContent && modifiedFiles.length > 0) {
result.pass = false;
result.details.push('❌ No [AUTO-FIX] comments found in modified files');
}
return result;
}
// ━━━ H9: 无残留调试代码 ━━━
function checkH9_NoDebugCode(modifiedFiles) {
const result = { id: 'H9', name: '无残留调试代码', pass: true, details: [] };
const debugPatterns = [
/console\.log\s*\(\s*['"]debug/gi,
/echo\s+['"]?debug/gi,
/TODO:\s*remove/gi,
/FIXME:\s*temp/gi,
/console\.log\s*\(\s*['"]test/gi
];
for (const filePath of modifiedFiles) {
const absPath = path.join(ROOT, filePath);
if (!fs.existsSync(absPath)) continue;
const content = fs.readFileSync(absPath, 'utf8');
for (const pattern of debugPatterns) {
if (pattern.test(content)) {
result.pass = false;
result.details.push(`${filePath}: ❌ Debug code found: ${pattern.source}`);
}
}
}
if (result.pass) result.details.push('No debug code residue found');
return result;
}
// ━━━ 主审核流程 ━━━
function main() {
console.log('[天眼 v3.0] Phase D 启动 — 审核修复代码');
console.log('═══════════════════════════════════════════');
const config = loadJSON(CONFIG_PATH);
if (!config) {
console.error('❌ 无法加载 tianyan-config.json');
process.exit(1);
}
// Load repair result
const repairResult = loadJSON(path.join(TMP_DIR, 'repair-result.json'));
if (!repairResult) {
console.log('⚠️ repair-result.json 不存在');
const result = { timestamp: getTimestamp(), pass: false, reason: 'No repair result found', checks: [] };
fs.writeFileSync(path.join(TMP_DIR, 'review-result.json'), JSON.stringify(result, null, 2));
return;
}
if (repairResult.skipped) {
console.log('⚠️ 修复已跳过(周末模式)');
const result = { timestamp: getTimestamp(), pass: false, reason: 'Repair skipped', checks: [] };
fs.writeFileSync(path.join(TMP_DIR, 'review-result.json'), JSON.stringify(result, null, 2));
return;
}
// Collect all modified files
const modifiedFiles = [];
for (const repair of (repairResult.repairs || [])) {
if (repair.success && repair.files_modified) {
modifiedFiles.push(...repair.files_modified);
}
}
const uniqueFiles = [...new Set(modifiedFiles)];
console.log(`📁 审核文件: ${uniqueFiles.length}\n`);
if (uniqueFiles.length === 0) {
console.log('⚠️ 没有修改的文件需要审核');
const result = { timestamp: getTimestamp(), pass: false, reason: 'No files to review', checks: [], pr_needed: false };
fs.writeFileSync(path.join(TMP_DIR, 'review-result.json'), JSON.stringify(result, null, 2));
return;
}
// Run all checks
const checks = [
checkH1_SecretsHardcoded(uniqueFiles),
checkH2_FileSyntax(uniqueFiles),
checkH3_DependencySafety(uniqueFiles),
checkH4_PermissionsMinimal(uniqueFiles),
checkH5_TianyanProtection(uniqueFiles),
checkH6_ChangeScope(uniqueFiles, config),
checkH7_SecretsIntegrity(uniqueFiles),
checkH8_AutoFixComments(uniqueFiles),
checkH9_NoDebugCode(uniqueFiles)
];
// Print results
console.log('📋 天眼检查清单 v3.0:');
console.log('─────────────────────────────────');
let allPass = true;
for (const check of checks) {
const icon = check.pass ? '✅' : '❌';
console.log(` ${icon} ${check.id}: ${check.name}`);
for (const detail of check.details) {
console.log(` ${detail}`);
}
if (!check.pass) allPass = false;
}
console.log('─────────────────────────────────');
const reviewResult = {
timestamp: getTimestamp(),
pass: allPass,
pr_needed: allPass,
checks,
files_reviewed: uniqueFiles,
summary: {
total_checks: checks.length,
passed: checks.filter(c => c.pass).length,
failed: checks.filter(c => !c.pass).length
}
};
fs.writeFileSync(path.join(TMP_DIR, 'review-result.json'), JSON.stringify(reviewResult, null, 2));
if (allPass) {
console.log('\n✅ 天眼审核通过 — 准备创建 PR');
} else {
const failedChecks = checks.filter(c => !c.pass).map(c => c.id).join(', ');
console.log(`\n❌ 天眼审核未通过 — 失败项: ${failedChecks}`);
}
}
main();