Files
code-review/app/main.js
T

162 lines
6.8 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.
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';
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}`);
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}`);
if (headMessage.includes('[ai-review-bot]') && headOutcome === 'failure') {
console.log(' ❌ 偵測到 [ai-review-bot][failure],直接讓 workflow 失敗');
console.log('='.repeat(60));
process.exit(1);
}
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);
});