423 lines
14 KiB
JavaScript
423 lines
14 KiB
JavaScript
#!/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();
|