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, getCommitMessageBySha, getBotReviewOutcome, 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'; import { section, step, line, ok, warn, error } from './log.js'; const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace'; async function main() { 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); line(`head check: sha=${headSha || 'empty'} outcome=${headOutcome}`); if (headMessage.includes('[ai-review-bot]') && headOutcome === 'failure') { error('偵測到 [ai-review-bot][failure],直接讓 workflow 失敗'); section('Pipeline 結束'); process.exit(1); } if (await shouldSkipBotCommit()) { ok('偵測到 [ai-review-bot] 自動提交,直接完成 action'); section('Pipeline 結束'); process.exit(0); } const { provider, baseURL, model } = getLLMConfig(); if (!provider) { error('未設定任何 LLM API Key,請檢查 action inputs'); process.exit(1); } line(`LLM: provider=${provider} model=${model} base_url=${baseURL}`); const roles = loadRoles(); line(`已載入 ${roles.length} 個角色: [${roles.map(r => r.name).join(', ')}]`); let diff; try { diff = await getPRDiff(); line(`diff 長度: ${diff.length} 字元`); } catch (e) { error(`取得 diff 失敗: ${e.message}`); process.exit(1); } if (!diff.trim()) { warn('diff 為空,無需審查'); process.exit(0); } try { const intro = getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`; await postComment(intro); ok('角色介紹 comment 發布成功'); } catch (e) { warn(`comment 發布失敗(繼續執行): ${e.message}`); } 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 { warn(`[${roles[i].name}] 分析失敗(跳過): ${results[i].reason?.message}`); } } ok(`Step2 完成: 新 findings 總計 ${newFindings.length} 筆`); step('Step3', 'Findings 合併與語意去重'); let repoDir; try { repoDir = cloneRepo(WORKSPACE); } catch (e) { warn(`clone repo 失敗(繼續執行): ${e.message}`); } const repoState = repoDir ? getRepoState(repoDir) : null; if (repoState) { 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); ok(`Step3 merged findings total=${mergedFindings.length}`); const deduped = await deduplicateWithAI(mergedFindings); const sorted = sortByLevel(deduped); ok(`Step3 去重完成: ${mergedFindings.length} -> ${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})`); step('Step4', 'AI 排除問題過濾'); const exclusions = loadExclusions(repoDir || WORKSPACE, repoState, WORKSPACE); const ruleFiltered = applyExclusions(sorted, exclusions); const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions); ok(`Step4 完成: findings total=${filtered.length}`); step('Step5', 'Findings 寫入與 Comment 發布'); const reviewDir = repoDir || WORKSPACE; saveFindings(WORKSPACE, filtered, reviewDir); try { await postOldFindingsComment(filtered); await postNewNonCriticalComment(filtered); await postNewCriticalComments(filtered); ok('Step5 完成'); } catch (e) { warn(`comment 發布失敗(繼續執行): ${e.message}`); } step('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); } step('Step7', '記憶區 Commit/Push'); const reviewOutcome = filtered.some(f => f.level === 'critical') ? 'failure' : 'success'; line(`review outcome=${reviewOutcome}`); await commitAndPush(WORKSPACE, repoDir || WORKSPACE, undefined, undefined, reviewOutcome); step('Step8', '嚴重問題檢查'); const criticalCount = filtered.filter(f => f.level === 'critical').length; if (criticalCount > 0) { error(`發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1)`); section('Pipeline 結束'); process.exit(1); } ok('無嚴重問題'); ok('Pipeline 完成'); section('Pipeline 結束'); } main().catch(e => { error(`Runner failed: ${e.message}`); process.exit(1); });