chore: unify log formatting

This commit is contained in:
2026-05-15 15:25:26 +00:00
parent bd4c3bce9e
commit 3fcbf788fc
8 changed files with 127 additions and 106 deletions
+42 -50
View File
@@ -6,121 +6,115 @@ import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplica
import { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js';
import { cloneRepo, commitAndPush, getRepoState } from './git.js';
import { validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js';
import { section, step, line, ok, warn, error } from './log.js';
const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace';
async function main() {
console.log('='.repeat(60));
console.log('🚀 Step1: Pipeline 啟動');
console.log(` repo=${GITEA_REPOSITORY} PR=#${PR_NUMBER}`);
console.log(` ${PR_HEAD_BRANCH} -> ${PR_BASE_BRANCH}`);
section('AI Code Review Pipeline');
step('Step1', 'Pipeline 啟動');
line(`repo=${GITEA_REPOSITORY} PR=#${PR_NUMBER}`);
line(`${PR_HEAD_BRANCH} -> ${PR_BASE_BRANCH}`);
const headSha = process.env.PR_HEAD_SHA || process.env.GITHUB_SHA || '';
const headMessage = await getCommitMessageBySha(headSha);
const headOutcome = getBotReviewOutcome(headMessage);
console.log(` 🔎 head check: sha=${headSha || 'empty'} outcome=${headOutcome}`);
line(`head check: sha=${headSha || 'empty'} outcome=${headOutcome}`);
if (headMessage.includes('[ai-review-bot]') && headOutcome === 'failure') {
console.log('偵測到 [ai-review-bot][failure],直接讓 workflow 失敗');
console.log('='.repeat(60));
error('偵測到 [ai-review-bot][failure],直接讓 workflow 失敗');
section('Pipeline 結束');
process.exit(1);
}
if (await shouldSkipBotCommit()) {
console.log(' 🤖 偵測到 [ai-review-bot] 自動提交,直接完成 action');
console.log('='.repeat(60));
ok('偵測到 [ai-review-bot] 自動提交,直接完成 action');
section('Pipeline 結束');
process.exit(0);
}
const { provider, baseURL, model } = getLLMConfig();
if (!provider) {
console.error('未設定任何 LLM API Key,請檢查 action inputs');
error('未設定任何 LLM API Key,請檢查 action inputs');
process.exit(1);
}
console.log(` LLM: provider=${provider} model=${model} base_url=${baseURL}`);
line(`LLM: provider=${provider} model=${model} base_url=${baseURL}`);
const roles = loadRoles();
console.log(` 已載入 ${roles.length} 個角色: [${roles.map(r => r.name).join(', ')}]`);
line(`已載入 ${roles.length} 個角色: [${roles.map(r => r.name).join(', ')}]`);
let diff;
try {
diff = await getPRDiff();
console.log(` diff 長度: ${diff.length} 字元`);
line(`diff 長度: ${diff.length} 字元`);
} catch (e) {
console.error(`取得 diff 失敗: ${e.message}`);
error(`取得 diff 失敗: ${e.message}`);
process.exit(1);
}
if (!diff.trim()) {
console.log(' ⚠️ diff 為空,無需審查');
warn('diff 為空,無需審查');
process.exit(0);
}
try {
const intro = getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`;
await postComment(intro);
console.log('角色介紹 comment 發布成功');
ok('角色介紹 comment 發布成功');
} catch (e) {
console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`);
warn(`comment 發布失敗(繼續執行): ${e.message}`);
}
// Step2: 各角色分析 diff 產生新 findings
console.log('\n📊 Step2: Findings 產生');
step('Step2', 'Findings 產生');
const results = await Promise.allSettled(roles.map(role => analyzeWithRole(role, diff)));
const newFindings = [];
for (let i = 0; i < results.length; i++) {
if (results[i].status === 'fulfilled') {
newFindings.push(...results[i].value);
} else {
console.log(` ⚠️ [${roles[i].name}] 分析失敗(跳過): ${results[i].reason?.message}`);
warn(`[${roles[i].name}] 分析失敗(跳過): ${results[i].reason?.message}`);
}
}
console.log(` Step2 完成: 新 findings 總計 ${newFindings.length}`);
ok(`Step2 完成: 新 findings 總計 ${newFindings.length}`);
// Step4: 讀取舊 findings合併去重(含 AI 語意去重)
console.log('\n🔀 Step3: Findings 合併');
// Clone repo 以讀取舊 findings 與排除清單
step('Step3', 'Findings 合併');
let repoDir;
try {
repoDir = cloneRepo(WORKSPACE);
} catch (e) {
console.log(` ⚠️ clone repo 失敗(繼續執行): ${e.message}`);
warn(`clone repo 失敗(繼續執行): ${e.message}`);
}
const repoState = repoDir ? getRepoState(repoDir) : null;
if (repoState) {
console.log(` repo 狀態: branch=${repoState.branch || 'detached'} commit=${repoState.shortSha || 'unknown'} commit_time=${repoState.commitTime || 'unknown'} path=${repoState.repoDir}`);
line(`repo 狀態: branch=${repoState.branch || 'detached'} commit=${repoState.shortSha || 'unknown'} commit_time=${repoState.commitTime || 'unknown'} path=${repoState.repoDir}`);
}
const oldFindings = loadOldFindings(repoDir || WORKSPACE);
const mergedFindings = mergeFindings(oldFindings, newFindings);
console.log(` Step3 merged findings total=${mergedFindings.length}`);
ok(`Step3 merged findings total=${mergedFindings.length}`);
console.log('\n🤖 Step3b: AI 語意去重');
step('Step3b', 'AI 語意去重');
const deduped = await deduplicateWithAI(mergedFindings);
const sorted = sortByLevel(deduped);
console.log(` Step3b dedup findings total=${sorted.length} (critical=${sorted.filter(f=>f.level==='critical').length} warning=${sorted.filter(f=>f.level==='warning').length} info=${sorted.filter(f=>f.level==='info').length})`);
ok(`Step3b dedup findings total=${sorted.length} (critical=${sorted.filter(f=>f.level==='critical').length} warning=${sorted.filter(f=>f.level==='warning').length} info=${sorted.filter(f=>f.level==='info').length})`);
// Step5: 讀取排除問題檔案,過濾 PR 問題表格,並請 AI 判斷誤報
console.log('\n🚫 Step4: AI 排除問題過濾');
// 輸入至 findings 用於 AI 誤報過濾,exclusions 同時作為已知誤報參考
step('Step4', 'AI 排除問題過濾');
const exclusions = loadExclusions(repoDir || WORKSPACE, repoState);
const ruleFiltered = applyExclusions(sorted, exclusions);
const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions);
console.log(` Step4 完成: findings total=${filtered.length}`);
ok(`Step4 完成: findings total=${filtered.length}`);
// Step6: 寫入 findings.json,依序發布 comment
console.log('\n📝 Step5: Findings 寫入與 Comment 發布');
step('Step5', 'Findings 寫入與 Comment 發布');
const reviewDir = repoDir || WORKSPACE;
saveFindings(WORKSPACE, filtered, reviewDir);
try {
await postOldFindingsComment(filtered);
await postNewNonCriticalComment(filtered);
await postNewCriticalComments(filtered);
console.log(' Step5 完成');
ok('Step5 完成');
} catch (e) {
console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`);
warn(`comment 發布失敗(繼續執行): ${e.message}`);
}
// Step7: 驗證 findings.json 與 exclusions.json 為合法 JSON
console.log('\n🔎 Step6: JSON 格式驗證');
step('Step6', 'JSON 格式驗證');
const missingPaths = [];
for (const relPath of [FINDINGS_PATH, EXCLUSIONS_PATH]) {
const fullPath = path.join(reviewDir, relPath);
@@ -136,26 +130,24 @@ async function main() {
ensureJSONArrayFileExists(fullPath, relPath);
}
// Step7: commit/push findings.json 到來源分支
console.log('\n💾 Step7: 記憶區 Commit/Push');
step('Step7', '記憶區 Commit/Push');
const reviewOutcome = filtered.some(f => f.level === 'critical') ? 'failure' : 'success';
console.log(` 🔎 review outcome=${reviewOutcome}`);
line(`review outcome=${reviewOutcome}`);
await commitAndPush(WORKSPACE, repoDir || WORKSPACE, undefined, undefined, reviewOutcome);
// Step9: 有 critical 問題則 exit 1
console.log('\n🚦 Step8: 嚴重問題檢查');
step('Step8', '嚴重問題檢查');
const criticalCount = filtered.filter(f => f.level === 'critical').length;
if (criticalCount > 0) {
console.log(`發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1`);
console.log('='.repeat(60));
error(`發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1`);
section('Pipeline 結束');
process.exit(1);
}
console.log('無嚴重問題');
console.log('\n✅ Pipeline 完成');
console.log('='.repeat(60));
ok('無嚴重問題');
ok('Pipeline 完成');
section('Pipeline 結束');
}
main().catch(e => {
console.error('❌ Runner failed:', e.message);
error(`Runner failed: ${e.message}`);
process.exit(1);
});