diff --git a/TODO.md b/TODO.md index 6978c02..01b8e73 100644 --- a/TODO.md +++ b/TODO.md @@ -3,53 +3,49 @@ ## 階段一:基本流程串接 - 目標:確保 action 可以被觸發,pipeline 各步驟依序執行,log 出每個主要階段的進入與完成。 - 驗收:log 中能看到每個階段(如「Step1: pipeline start」、「Step2: findings merge」等)明確訊息,且流程能走完(即使還沒產生 findings)。 -- 完成 +- 未驗收 ## 階段二:Git Diff 排除 .gitea/ 資料夾 - 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼。 - 驗收:PR 中有 `.gitea/` 路徑的變更時,diff 內容不包含該路徑的區塊,AI 分析結果不含 `.gitea/` 相關問題。 -- 完成 +- 未驗收 ## 階段三:Findings 產生與合併 - 目標:各角色(style/security/performance/maintainability/testing)能產生 findings,並正確合併新舊 findings。 - 驗收:log 中能看到每個角色 findings 數量、合併後 findings 統計,並有「Step3: merged findings total=...」等訊息。 -- 完成 +- 未驗收 ## 階段四:AI 去重與角色確認 - 目標:嘗試呼叫 LLM 進行 findings 去重與角色確認,API 額度不足時要有降級處理 log。 - 驗收:log 中能看到 deduplication/resolution confirmation 成功或失敗(如 402),降級時有「保留所有問題」等明確訊息。 -- 完成 +- 未驗收 ## 階段五:AI 排除問題過濾 - 目標:讀取排除問題檔案(`.gitea/ai-review/exclusions.json`)進行規則過濾,並呼叫 AI 判斷剩餘問題是否為誤報或不適用,兩層過濾後產生最終問題清單。 - 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息、規則過濾數量變化,以及「AI 誤報過濾: N -> M 筆」或降級訊息。 -- 完成 +- 未驗收 ## 階段六:findings 寫入與 comment 發布 - 目標:`.gitea/ai-review/findings.json` 正確寫入,comment 發布順序正確(舊問題→非嚴重→嚴重),每步有 log。 - 驗收:log 中能看到 `.gitea/ai-review/findings.json` 寫入、comment sync 的詳細訊息與順序。 -- 完成 +- 未驗收 ## 階段七:階段六後驗證 JSON 格式 - 目標:階段六完成後驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試重置為空陣列並備份原檔,修正失敗才 exit 1。 - 驗收:log 中能看到兩個檔案的驗證結果(成功或失敗),格式錯誤時有「嘗試修正」訊息與備份路徑,修正失敗時 workflow 狀態為失敗。 -- 完成 +- 未驗收 ## 階段八:記憶區 commit/push 與錯誤處理 - 目標:記憶區能成功 commit/push,錯誤時有明確 log,流程結束有總結訊息。 - 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,錯誤時有「Runner failed: ...」等明確錯誤說明。 -- 完成 +- 未驗收 ## 階段九:阻擋嚴重問題 PR(第 8 點) - 目標:如果 PR 問題表格中有嚴重(critical)問題,workflow 需直接 exit 1,不讓流程成功。 - 驗收:log 中能看到「critical 問題存在,workflow 結束(exit 1)」等明確訊息,且 workflow 狀態為失敗。 -- 完成 +- 未驗收 ## 階段十:API Key 輪替 - 目標:所有平台的 API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,單一 Key 失敗時自動換下一個,全部失敗則 exit 1。 - 驗收:log 中能看到「key[N/M] 失敗」等訊息,換 key 後繼續執行;傳入單一 Key 時行為與原本相同;全部 Key 失敗時 log「所有 API Key 均失敗,終止流程」且 workflow 狀態為失敗。 -- 完成 - ---- - -所有階段驗收通過。 +- 未驗收 diff --git a/app/gitea.js b/app/gitea.js index b7a048b..263baf1 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -8,14 +8,14 @@ const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`; export async function getPRDiff() { const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent }); - return filterDiff(resp.data, ['.gitea/']); + return resp.data; } /** * 過濾 diff 內容,移除路徑符合 excludePrefixes 的區塊。 * 每個區塊以 "diff --git a/" 開頭判斷,使用 startsWith 精確比對前綴。 */ -function filterDiff(diff, excludePrefixes) { +export function filterDiff(diff, excludePrefixes) { return diff.split(/(?=^diff --git )/m) .filter(block => !excludePrefixes.some(p => block.startsWith(`diff --git a/${p}`))) .join(''); diff --git a/app/main.js b/app/main.js index a699ee6..02f5d15 100644 --- a/app/main.js +++ b/app/main.js @@ -2,7 +2,7 @@ import fs from 'fs'; 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 } from './gitea.js'; +import { getPRDiff, filterDiff, postComment } 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 } from './git.js'; @@ -47,8 +47,18 @@ async function main() { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } - // Step2: 各角色分析 diff 產生新 findings - console.log('\n📊 Step2: Findings 產生'); + // Step2: 排除 .gitea/ 資料夾內的所有檔案 + console.log('\n🗂️ Step2: Git Diff 過濾'); + diff = filterDiff(diff, ['.gitea/']); + console.log(` 排除 .gitea/ 後 diff 長度: ${diff.length} 字元`); + + if (!diff.trim()) { + console.log(' ⚠️ 過濾後 diff 為空,無需審查'); + process.exit(0); + } + + // Step3: 各角色分析 diff 產生新 findings + console.log('\n📊 Step3: Findings 產生'); const results = await Promise.allSettled(roles.map(role => analyzeWithRole(role, diff))); const newFindings = []; for (let i = 0; i < results.length; i++) { @@ -58,10 +68,10 @@ async function main() { console.log(` ⚠️ [${roles[i].name}] 分析失敗(跳過): ${results[i].reason?.message}`); } } - console.log(` Step2 完成: 新 findings 總計 ${newFindings.length} 筆`); + console.log(` Step3 完成: 新 findings 總計 ${newFindings.length} 筆`); - // Step3: 讀取舊 findings,合併去重(含 AI 語意去重) - console.log('\n🔀 Step3: Findings 合併'); + // Step4: 讀取舊 findings,合併去重(含 AI 語意去重) + console.log('\n🔀 Step4: Findings 合併'); // Clone repo 以讀取舊 findings 與排除清單 let repoDir; try { @@ -71,35 +81,35 @@ async function main() { } const oldFindings = loadOldFindings(repoDir || WORKSPACE); const mergedFindings = mergeFindings(oldFindings, newFindings); - console.log(` Step3 merged findings total=${mergedFindings.length}`); + console.log(` Step4 merged findings total=${mergedFindings.length}`); - console.log('\n🤖 Step3b: AI 語意去重'); + console.log('\n🤖 Step4b: 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})`); + console.log(` Step4b 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})`); - // Step4: 讀取排除問題檔案,過濾 PR 問題表格,並請 AI 判斷誤報 - console.log('\n🚫 Step4: AI 排除問題過濾'); + // Step5: 讀取排除問題檔案,過濾 PR 問題表格,並請 AI 判斷誤報 + console.log('\n🚫 Step5: AI 排除問題過濾'); // 輸入至 findings 用於 AI 誤報過濾,exclusions 同時作為已知誤報參考 const exclusions = loadExclusions(repoDir || WORKSPACE); const ruleFiltered = applyExclusions(sorted, exclusions); const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions); - console.log(` Step4 完成: findings total=${filtered.length}`); + console.log(` Step5 完成: findings total=${filtered.length}`); - // Step5: 寫入 findings.json,依序發布 comment - console.log('\n📝 Step5: Findings 寫入與 Comment 發布'); + // Step6: 寫入 findings.json,依序發布 comment + console.log('\n📝 Step6: Findings 寫入與 Comment 發布'); saveFindings(WORKSPACE, filtered); try { await postOldFindingsComment(filtered); await postNewNonCriticalComment(filtered); await postNewCriticalComments(filtered); - console.log(' Step5 完成'); + console.log(' Step6 完成'); } catch (e) { console.log(` ⚠️ comment 發布失敗(繼續執行): ${e.message}`); } - // Step5b: 驗證 findings.json 與 exclusions.json 為合法 JSON - console.log('\n🔎 Step5b: JSON 格式驗證'); + // Step7: 驗證 findings.json 與 exclusions.json 為合法 JSON + console.log('\n🔎 Step7: JSON 格式驗證'); for (const relPath of [FINDINGS_PATH, EXCLUSIONS_PATH]) { const fullPath = path.join(repoDir || WORKSPACE, relPath); if (!fs.existsSync(fullPath)) { @@ -123,12 +133,12 @@ async function main() { } } - // Step6: commit/push findings.json 到來源分支 - console.log('\n💾 Step6: 記憶區 Commit/Push'); + // Step8: commit/push findings.json 到來源分支 + console.log('\n💾 Step8: 記憶區 Commit/Push'); await commitAndPush(WORKSPACE); - // Step7: 有 critical 問題則 exit 1 - console.log('\n🚦 Step7: 嚴重問題檢查'); + // Step9: 有 critical 問題則 exit 1 + console.log('\n🚦 Step9: 嚴重問題檢查'); const criticalCount = filtered.filter(f => f.level === 'critical').length; if (criticalCount > 0) { console.log(` ❌ 發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1)`);