import path from 'path'; import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig, FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js'; import { loadRoles, getRoleIntro } from './roles.js'; import { getPRDiff, postComment, shouldSkipBotCommit } from './gitea.js'; import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions, filterFalsePositivesWithAI } from './findings.js'; import { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js'; import { cloneRepo, commitAndPush, getRepoState } from './git.js'; import { validateJSONArrayFile, ensureJSONArrayFileExists } from './json.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}`); if (await shouldSkipBotCommit()) { console.log(' 🤖 偵測到 [ai-review-bot] 自動提交,直接完成 action'); console.log('='.repeat(60)); process.exit(0); } const { provider, baseURL, model } = getLLMConfig(); if (!provider) { console.error('❌ 未設定任何 LLM API Key,請檢查 action inputs'); process.exit(1); } console.log(` LLM: provider=${provider} model=${model} base_url=${baseURL}`); const roles = loadRoles(); console.log(` 已載入 ${roles.length} 個角色: [${roles.map(r => r.name).join(', ')}]`); let diff; try { diff = await getPRDiff(); console.log(` diff 長度: ${diff.length} 字元`); } catch (e) { console.error(` ❌ 取得 diff 失敗: ${e.message}`); process.exit(1); } if (!diff.trim()) { console.log(' ⚠️ diff 為空,無需審查'); process.exit(0); } try { const intro = getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`; await postComment(intro); console.log(' ✅ 角色介紹 comment 發布成功'); } catch (e) { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } // Step2: 各角色分析 diff 產生新 findings console.log('\n📊 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}`); } } console.log(` Step2 完成: 新 findings 總計 ${newFindings.length} 筆`); // Step4: 讀取舊 findings,合併去重(含 AI 語意去重) console.log('\n🔀 Step3: Findings 合併'); // Clone repo 以讀取舊 findings 與排除清單 let repoDir; try { repoDir = cloneRepo(WORKSPACE); } catch (e) { console.log(` ⚠️ 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}`); } const oldFindings = loadOldFindings(repoDir || WORKSPACE); const mergedFindings = mergeFindings(oldFindings, newFindings); console.log(` Step3 merged findings total=${mergedFindings.length}`); console.log('\n🤖 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})`); // Step5: 讀取排除問題檔案,過濾 PR 問題表格,並請 AI 判斷誤報 console.log('\n🚫 Step4: AI 排除問題過濾'); // 輸入至 findings 用於 AI 誤報過濾,exclusions 同時作為已知誤報參考 const exclusions = loadExclusions(repoDir || WORKSPACE, repoState); const ruleFiltered = applyExclusions(sorted, exclusions); const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions); console.log(` Step4 完成: findings total=${filtered.length}`); // Step6: 寫入 findings.json,依序發布 comment console.log('\n📝 Step5: Findings 寫入與 Comment 發布'); const reviewDir = repoDir || WORKSPACE; saveFindings(WORKSPACE, filtered, reviewDir); try { await postOldFindingsComment(filtered); await postNewNonCriticalComment(filtered); await postNewCriticalComments(filtered); console.log(' Step5 完成'); } catch (e) { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } // Step7: 驗證 findings.json 與 exclusions.json 為合法 JSON console.log('\n🔎 Step6: JSON 格式驗證'); const missingPaths = []; for (const relPath of [FINDINGS_PATH, EXCLUSIONS_PATH]) { const fullPath = path.join(reviewDir, relPath); try { const result = await validateJSONArrayFile(fullPath, relPath); if (!result.exists) missingPaths.push({ fullPath, relPath }); } catch { process.exit(1); } } for (const { fullPath, relPath } of missingPaths) { ensureJSONArrayFileExists(fullPath, relPath); } // Step7: commit/push findings.json 到來源分支 console.log('\n💾 Step7: 記憶區 Commit/Push'); const reviewOutcome = filtered.some(f => f.level === 'critical') ? 'failure' : 'success'; console.log(` 🔎 review outcome=${reviewOutcome}`); await commitAndPush(WORKSPACE, repoDir || WORKSPACE, undefined, undefined, reviewOutcome); // Step9: 有 critical 問題則 exit 1 console.log('\n🚦 Step8: 嚴重問題檢查'); const criticalCount = filtered.filter(f => f.level === 'critical').length; if (criticalCount > 0) { console.log(` ❌ 發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1)`); console.log('='.repeat(60)); process.exit(1); } console.log(' ✅ 無嚴重問題'); console.log('\n✅ Pipeline 完成'); console.log('='.repeat(60)); } main().catch(e => { console.error('❌ Runner failed:', e.message); process.exit(1); });