Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
268cd05211 | ||
|
|
c2f41b16eb | ||
|
|
5c2c37f9d1 | ||
|
|
5fbfec3aaf | ||
|
|
d1293bb950 | ||
|
|
69122dca3e | ||
|
|
e54c3f5fbe | ||
|
|
2f294845c8 | ||
|
|
a7824cff29 | ||
|
|
fda3331dd4 | ||
|
|
11205d5b82 |
@@ -40,5 +40,359 @@
|
||||
"role": "Bard",
|
||||
"original_finding": "main() 前的 JSDoc 幾乎把整條 pipeline 逐步重寫一次,和函式內 Step 註解重複,維護時很容易變成兩份會走調的文件。",
|
||||
"reason": "main() 的 pipeline 概述 JSDoc 為刻意文件化風格,與 Step 註解並存屬設計選擇。"
|
||||
},
|
||||
{
|
||||
"location": "src/resolve.js:253",
|
||||
"role": "Assassin",
|
||||
"original_finding": "這裡會把 PR 上所有未解決的 review comment ID 全部送去 resolve,而不是只處理 AI Review bot 自己建立的 thread。攻擊者只要開 PR 觸發這個 action,就可能讓 bot 關閉人類審查者留下的安全疑慮或阻擋性對話,繞過人工審查流程。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/gitea.js:66",
|
||||
"role": "Assassin",
|
||||
"original_finding": "這裡直接從 PR head 讀取 `.reviewignore`,再拿它當成排除規則。攻擊者可以在自己的分支塞入排除條目,讓 bot 故意跳過包含惡意變更的檔案或整個目錄,等於自己決定哪些地方不被審查。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:352",
|
||||
"role": "Assassin",
|
||||
"original_finding": "這裡會直接讀取 PR 工作樹中的 `.gitea/ai-review/exclusions.json` 當成可信排除來源。攻擊者可以先在分支裡放一份藏在 `.gitea/` 下的 exclusions 檔,利用被忽略的路徑把自己的問題先排除掉,讓後續的 findings 被靜默吃掉。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/main.js:158",
|
||||
"role": "Assassin",
|
||||
"original_finding": "這裡直接載入 PR 工作樹中的 `.gitea/ai-review/exclusions.json` 當成既有排除規則。攻擊者可以先在 PR 內預埋一份排除清單,因為 `.gitea/` 又被預設排除於 diff 之外,這些惡意排除不會被審查到,卻會被流程直接拿來吞掉真正的 findings,形成靜默的審查繞過。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/gitea.js:118",
|
||||
"role": "Assassin",
|
||||
"original_finding": "這裡只靠 commit 訊息是否包含 `[ai-review-bot]` 來判斷要不要跳過審查,等於把信任建立在可由任何提交者自行偽造的字串上。攻擊者只要把自己的 PR head commit 訊息改成這個標記,整個審查流程就會被直接略過。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/llm.js:21",
|
||||
"role": "Assassin",
|
||||
"original_finding": "這裡把未清洗的 `userContent` 直接塞進模型提示詞,等於讓 PR 內容、留言內容或其他外部文字能反過來操控 LLM。攻擊者可以在 diff 裡埋入『忽略前述規則、回傳空陣列』這類指令,讓審查模型漏報真正的風險或把嚴重問題降級成誤報。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/main.js:69",
|
||||
"role": "Mage",
|
||||
"original_finding": "這裡先檢查 head SHA 對應的訊息是否為 failure,但如果 SHA 查詢失敗或是空值,後面的 `shouldSkipBotCommit()` 仍可能只看到分支 head 上的 `[ai-review-bot]` 標記就直接跳過。最小重現:`getCommitMessageBySha()` 因 Gitea API 暫時失敗回空字串,而分支 head 正好是 `[ai-review-bot][failure]`,流程就會 exit 0,等於把本來應該失敗的 bot commit 當成可跳過的自動提交。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/main.js:132",
|
||||
"role": "Rogue",
|
||||
"original_finding": "這裡把每個角色的 LLM 分析逐一 await,6 個角色就把總耗時堆成約 6 倍單次模型延遲;這些分析彼此獨立,CPU 沒偷到時間,反而把整條 pipeline 卡在序列網路/CLI 呼叫上。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:381",
|
||||
"role": "Leo",
|
||||
"original_finding": "`loadExclusions()` 同時負責讀檔、解析多種格式、正規化、去重、記錄 repo 狀態、改寫原檔、鏡像寫入與建立 AI prompt 摘要。這個函式的職責過多,之後只要調整 exclusions 格式或同步策略,就很容易牽動不相關行為。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:466",
|
||||
"role": "Mage",
|
||||
"original_finding": "這裡優先使用 `ex.textKey`,但 `textKey` 是由 `toKeyText()` 產生的無分隔且未轉小寫文字,而 findingText 是 `normalizeText()` 產生的小寫、以空白分隔文字。最小重現:排除文字 `Update tests` 會變成 `Updatetests`,finding suggestion 會變成 `update tests`,兩邊互相 `includes` 都不成立,導致純文字排除規則失效。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:416",
|
||||
"role": "Maya",
|
||||
"original_finding": "applyExclusions 的核心比對支援「只有文字、沒有路徑/角色」的排除規則,但現有測試多半靠相同檔案路徑命中,沒有驗證純文字排除、空文字排除、大小寫/標點差異等邊界。這條排除規則的最脆弱分支還沒被測到。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/gitea.js:95",
|
||||
"role": "Maya",
|
||||
"original_finding": "shouldSkipBotCommit 目前只看到命中 bot marker 的測試,缺少「commit API 失敗、分支查詢失敗、sha/branch 都沒有 marker」時應回 false 的失敗與保守路徑驗證。這是避免 workflow 誤跳過審查的關鍵判斷,不能只測快樂路徑。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/llm.js:66",
|
||||
"role": "Maya",
|
||||
"original_finding": "`runAssistantCLI()` 目前只有成功與一般失敗的測試,沒有覆蓋 timeout、`maxBuffer` 超限、以及 `opencode` 分支建立的暫存 prompt 檔在例外發生時是否確實清理。這些都是外部 CLI 整合最常出問題的失敗路徑,沒有測到就很難確定不會留下殘檔或把流程卡死。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/gitea.js:239",
|
||||
"role": "Rogue",
|
||||
"original_finding": "這裡逐一 await 每個 review 的 comments,PR review 一多就變成 N 次遠端呼叫的線性延遲累加;例如 30 個 review 就是 30 個 round-trip 排隊等,時間都被網路空轉偷走。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:442",
|
||||
"role": "Rogue",
|
||||
"original_finding": "這裡每一筆 finding 都要跟整包 exclusions 做一次 `.some()`,而且內層還反覆跑 `normalizeText` 和字串包含比對,資料一多就直接變成 O(F×E) 的熱點。像 300 筆 finding 配 500 筆 exclusions,會吃掉 15 萬次以上的比對與正規化,CPU 和字串配置都在浪費。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/resolve.js:213",
|
||||
"role": "Mage",
|
||||
"original_finding": "`findingSig` 只用檔案路徑加上 `suggestion` 來識別問題,忽略了 `problem`、`role`,也沒有留下任何穩定的 thread 識別;最小重現:同一個 `a.js` 內有兩條都建議「加上 null 檢查」但其實是不同位置的 finding,先解掉其中一條後,另一條也會被當成同一筆而被 `dropResolvedFindings` / `addCarriedFindings` 誤合併或誤刪。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:40",
|
||||
"role": "Mage",
|
||||
"original_finding": "這裡把 `file:0` 也視為有效行號;最小重現:只要上游傳進 `app/foo.js:0`,`parseLocation()` 會回傳 line=0,後續 `postPullReviewComment` 會帶著 `new_position: 0` 發到 Gitea,通常會被拒絕或定位失敗。也就是說,0 行號沒有被當成缺值處理。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:233",
|
||||
"role": "Maya",
|
||||
"original_finding": "`postFindingsReview` 的降級流程有兩層:先嘗試批次 review,再失敗時改成 summary-only,最後 summary-only 也失敗才退回一般 comment。現在的測試只驗到第一層失敗後、第二層成功的情境,沒有驗證 summary-only 也失敗時是否真的會呼叫 `postIssue(body)`,這是最脆弱的 fallback 路徑之一。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/config.js:31",
|
||||
"role": "Maya",
|
||||
"original_finding": "這裡是整個 action 讀取 `INPUT_*`、`GITEA_*` 與事件 payload 的入口,但測試只覆蓋了 `getLLMConfig()`,沒有把 `GITEA_TOKEN`、`GITEA_COMMENT_TOKEN`、`PR_NUMBER`、`PR_HEAD_SHA` 這些環境與 payload 的優先序鎖住。特別是 comment token 退回主 token、以及 event 檔讀不到時回到空值的情境,都是 CI 最容易因環境差異壞掉的地方。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/config.js:27",
|
||||
"role": "Bard",
|
||||
"original_finding": "這段註解已經跟著介面走音了。它宣稱使用端「只需傳 `with: token`」,但這次 action 其實已新增 `comment_token` 與 `model` 等輸入,註解仍停留在舊旋律,容易讓讀者誤判介面現況。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:227",
|
||||
"role": "Mage",
|
||||
"original_finding": "這個抽取器一旦命中目標檔案,就一路把後面的 diff 全部帶進去,沒有在下一個 `diff --git` 區塊時停下來。最小重現:diff 同時有 `a.js` 和 `b.js`,要補 `a.js` 的行號時,送給 LLM 的內容會混進 `b.js` 的 hunks,結果很容易定位到錯的行,或讓模型把別檔的內容誤認成目標檔上下文。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:277",
|
||||
"role": "Maya",
|
||||
"original_finding": "`deduplicateWithAI` 是新的核心語意去重流程,但目前完全沒有直接測試它的成功與失敗分支。尤其是 LLM 回傳排序不同、夾雜幻覺項目、回傳空陣列或超量結果時,程式會改走保守 fallback,這些都是很容易壞掉但現在沒被驗證的邊界。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:322",
|
||||
"role": "Rogue",
|
||||
"original_finding": "每一筆缺行號的 finding 都重新呼叫 `extractFileDiff(diff, file)` 掃完整份 diff,若同一檔案有 k 筆問題,就會重複做 k 次整份 diff 解析,浪費量是 O(k × diff長度)。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/llm.js:56",
|
||||
"role": "Bard",
|
||||
"original_finding": "`cliArgs` 把不同提供者的參數拼湊在同一個分支裡,還讓 `opencode` 走了另一套文字輸入路線,整個 helper 的節奏忽然一分為二。讀起來像兩個介面硬塞進同一支笛子。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:1",
|
||||
"role": "Leo",
|
||||
"original_finding": "這個模組同時處理舊 findings 載入、合併去重、缺行號補齊、排除規則正規化、誤報過濾、AI 去重、以及 exclusions 的讀寫,職責已經混成一包。更麻煩的是 `loadExclusions`、`appendExclusions`、`applyExclusions` 各自都有一套相近但不完全一致的比對邏輯,未來只要規則改一處,另一處沒同步就會開始出現不可預期的行為差異。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/json.js:113",
|
||||
"role": "Mage",
|
||||
"original_finding": "這裡只檢查 `JSON.parse(normalized)` 能不能成功,沒有確認修復後的內容真的是陣列。最小重現是 AI 把 `findings.json` 修成 `{ \"a\": 1 }`,函式會照樣寫回檔案並回報成功,但下一輪讀取時 `readJSONArray` 會把它當成非陣列而視為空值,等於把資料靜默吃掉。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/resolve.js:74",
|
||||
"role": "Mage",
|
||||
"original_finding": "這裡用 `path + line` 當唯一群組鍵,且只保留第一筆 `botFinding`。最小重現是同一個檔案同一行同時被兩個角色指出不同問題,`groupConversations` 會把它們合成同一組,後來的那筆 finding 會被吞掉,導致後續關閉、回寫或保留時少掉一個問題。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:214",
|
||||
"role": "Maya",
|
||||
"original_finding": "這裡新增了 `postOldFindingsComment` 與 `postNewNonCriticalComment` 兩條公開的留言分流路徑,但現有測試只驗證了 `postNewCriticalComments` 與 `postFindingsReview`,完全沒有案例確認這兩個函式的篩選條件、空陣列時是否跳過、以及輸出的 Markdown 內容是否真的只包含對應的 findings。這種分流邏輯一旦算錯,就會發生該發的沒發、或不該公告的問題被貼出去。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:171",
|
||||
"role": "Maya",
|
||||
"original_finding": "`postFindingsReview` 的救援路徑只測到「批次 review 失敗後,改發逐筆 inline comment」這一段,卻沒有驗證第二次 `postReview({ comments: [] })` 也失敗時,會正確降級到 `postIssue(body)`。這條路徑是 Gitea review API 整個故障時保住摘要的最後保險絲,沒測到的話,真正出事時很容易靜默漏報。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/main.js:17",
|
||||
"role": "Maya",
|
||||
"original_finding": "`main()` 整個流程目前沒有任何直接測試,只能靠零散的子函式單測推測結果;但這裡包含多個關鍵分支與 `process.exit` 行為,例如 preflight 失敗、bot 自動提交跳過、空 diff 提早結束、JSON 驗證失敗、以及偵測到 critical 後結束失敗。只要接線順序或退出碼改壞,現有測試不會第一時間抓到。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/gitea.js:71",
|
||||
"role": "Assassin",
|
||||
"original_finding": "`.reviewignore` 是從被審查的 PR head 直接讀回來的,提交者自己就能在同一個 PR 裡新增排除規則,把惡意檔案或關鍵目錄整批從 diff 中消失。攻擊者只要加幾條前綴,就能讓這個審查流程根本看不到真正危險的變更。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/roles.js:41",
|
||||
"role": "Assassin",
|
||||
"original_finding": "角色 prompt 直接從目前 checkout 的 `src/prompts/roles/*.md` 載入,等於把 system prompt 放在可被 PR 修改的位置。攻擊者只要改這些 markdown,就能改寫審查角色的指令,命令模型忽略漏洞、輸出空陣列,或把所有問題打成誤報。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:16",
|
||||
"role": "Assassin",
|
||||
"original_finding": "這裡把整份 diff 直接塞進 LLM 輸入,沒有做結構化封裝或輸出約束。惡意提交者可以在程式碼註解、字串或檔案內容裡埋 prompt injection,誘導模型少報、漏報,甚至捏造不該存在的問題,讓後續去重與發布流程建立在被污染的判斷上。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:153",
|
||||
"role": "Leo",
|
||||
"original_finding": "這裡開始對 `is_new` 的解讀就和前面的統計邏輯不一致了:`!f.is_new` 會把 `undefined` 當成舊問題,但同檔前面的 `newFindingsOnly()` 又把 `undefined` 當新問題。之後 `formatFindingsStats`、舊問題留言、新問題留言會各自走不同分類,未來只要上游少填一個欄位,結果就會悄悄分岔,很難追。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:91",
|
||||
"role": "Leo",
|
||||
"original_finding": "這裡的文字正規化規則和 `normalizeText()` 不一致,還在註解裡直接寫了「不確定是否預期」。再往下又有 `mergeFindings`、`appendExclusions`、`applyExclusions` 各自用不同簽章做去重/比對,等於同一份排除資料在不同流程可能被視為不同東西。這種規則分裂最容易在半年後變成『怎麼這筆有時候去重,有時候又新增』的維運災難。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:426",
|
||||
"role": "Mage",
|
||||
"original_finding": "這裡只要 `exPath` 或 `ex.role` 存在,就直接把 `textMatches` 跳過。實際結果是:同一個檔案、同一個角色的任何其他 finding,只要碰上這筆排除規則就會被整包濾掉,哪怕問題本質完全不同。最小重現:先把 `app/a.js` 某個誤報加入 exclusions,之後同檔同角色的另一個真問題也會一起消失。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:202",
|
||||
"role": "Mage",
|
||||
"original_finding": "這個去重 key 把 `suggestion` 截成前 50 個字元。只要兩筆 finding 在同檔、同角色、同位置,且建議文字前 50 字相同,後面的差異就會被吃掉。最小重現:兩個不同問題的 suggestion 都以相同開頭描述,第二筆會被當成重複直接丟失。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:303",
|
||||
"role": "Mage",
|
||||
"original_finding": "這裡回填 AI 去重結果時,也用同一個 `location + suggestion 前 50 字` 當對照鍵。只要兩筆原始 finding 的 key 撞到,`origMap` 會只留最後一筆,AI 回傳的結果就可能對到錯的原始 finding,或直接被 `filter(Boolean)` 吃掉。最小重現:同檔同位置兩筆 suggestion 前 50 字相同,去重後會錯配或少一筆。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:75",
|
||||
"role": "Mage",
|
||||
"original_finding": "這裡把 `file:0` 當成有效行號回傳。後續 `postFindingsReview` 和行內 critical comment 會把它送進 Gitea,但 `new_position = 0` 並不是有效 diff 行號,結果不是 API 拒絕,就是整筆 comment 被降級/略過。最小重現:LLM 回 `app/a.js:0`,流程仍會嘗試建立行內註解。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/resolve.js:91",
|
||||
"role": "Mage",
|
||||
"original_finding": "同一個 `path|line` 的多筆 bot comment 只會保留第一筆 `botFinding`,後面的 finding 會被靜默丟掉。最小重現:同一行上有兩個不同角色或不同建議的 bot comment,reconcile 時只會帶回第一筆,另一筆不會進入 resolved/excluded/carried 清單。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/main.js:31",
|
||||
"role": "Maya",
|
||||
"original_finding": "這個 orchestrator 是整條 pipeline 的入口,但目前測試都停在零件層,沒有直接驗證 `main()` 的關鍵分支:前置驗證失敗、bot 自動提交直接退出、diff 為空直接退出、JSON 驗證失敗退出、以及發現 critical 時的 exit 1。這些流程一旦接線錯了,單元測試還是可能全綠。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:267",
|
||||
"role": "Maya",
|
||||
"original_finding": "`postOldFindingsComment` 和緊接著的 `postNewNonCriticalComment` 都是新公開行為,但測試只覆蓋 `postNewCriticalComments` 與 `postFindingsReview`,沒有直接驗證這兩個 comment helper 的內容格式、空陣列跳過、以及 `is_new`/`level` 過濾是否正確。這會讓留言分流一旦退化,測試完全抓不到。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:249",
|
||||
"role": "Maya",
|
||||
"original_finding": "`mergeFindings` 與 `sortByLevel` 是 Step6 的核心邏輯,但目前沒有直接測到去重 key 的行為,也沒有測到合併後的排序是否真的維持 critical > warning > info。只靠上層流程的間接測試,對這種資料整理規則不夠穩。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/llm.js:17",
|
||||
"role": "Rogue",
|
||||
"original_finding": "這裡把 `limit <= 0` 解讀成「不限制」,直接開到 `items.length` 個 worker。只要 finding 或對話一多,就會同時 spawn 一整排 LLM 子行程,CPU、記憶體、檔案描述元一起被打爆,熱路徑很容易從平行加速變成資源風暴。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/resolve.js:169",
|
||||
"role": "Rogue",
|
||||
"original_finding": "這段為了產生約 41 行的 `codeWindow`,先把整個檔案內容從 Gitea 抓回來。大檔案時等於每個 open conversation 都在付整份檔案的網路與記憶體成本,實際只用到一小段片段,浪費量會跟檔案大小線性成長。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/resolve.js:228",
|
||||
"role": "Rogue",
|
||||
"original_finding": "這個 `extractFileDiff` 一旦開始抓到目標檔案,就一路把後面的所有 diff 都塞進去,根本沒有在下一個 `diff --git` 停下來。結果本來只想餵單一檔案的提示,可能膨脹成接近整份 PR diff,每次補行號都在多燒 token 與傳輸時間。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:406",
|
||||
"role": "Rogue",
|
||||
"original_finding": "這裡是典型的 `findings × exclusions` 雙層掃描,而且內層每次還要做字串正規化與比對。排除規則一多就變成 O(F*E) 熱點,幾百筆 finding 配幾百條 exclusions 時,白白重複掃描與正規化的成本會很明顯。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:25",
|
||||
"role": "Maya",
|
||||
"original_finding": "Markdown 表格列直接嵌入 role、location、suggestion,但測試沒有覆蓋 suggestion 含 `|`、換行或 Markdown 特殊字元時的輸出。這不是要求現在一定要改格式,而是目前缺少案例確認表格在真實 LLM 輸出下不會被破壞。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:102",
|
||||
"role": "Rogue",
|
||||
"original_finding": "統計表與單行摘要各欄位都用 `filter(...).length` 重掃多次,同一批 findings 會被走 4 到 8 次。資料量一大,連 log 文字本身都開始吃不必要的掃描成本。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:266",
|
||||
"role": "Maya",
|
||||
"original_finding": "`postOldFindingsComment` 與緊接著的 `postNewNonCriticalComment` 都是這次新加的對外 comment 發布行為,但目前沒有專門測試它們的空陣列早退、標題文字與表格內容。這會讓 comment 分流邏輯只靠間接測試支撐,回歸時很容易漏掉。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/findings.js:393",
|
||||
"role": "Rogue",
|
||||
"original_finding": "前面已經把 exclusions 正規化、去重過一次了,這裡為了 log 又再丟進 `buildExclusionContext` 重做 normalize / dedupe / group。等於同一批資料在同一輪流程裡被重算兩次,白白多吃一輪 O(n) 到 O(n log n) 的 CPU。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/resolve.js:88",
|
||||
"role": "Rogue",
|
||||
"original_finding": "這個 `codeWindow` 每遇到一筆 open conversation 就對整份檔案內容再 `split('\\n')` 一次。若同一個檔案有多條 thread,O(L) 的切割和陣列配置會被重複吃掉,明明同一份內容卻一直重複解剖。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/resolve.js:201",
|
||||
"role": "Leo",
|
||||
"original_finding": "`reconcileConversations()` 同時在做 comment 分組、關閉遠端 review、讀檔、抽 code window、AI 裁決、再把結果拆成 resolved / excluded / carried 三條路徑,流程很完整,但也很難局部理解或替換。未來任何一段判斷要調整,都得先吞下整個函式的心智負擔,維護門檻偏高。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/usage.js:212",
|
||||
"role": "Mage",
|
||||
"original_finding": "這個百分比計算只擋了 `limit <= 0`,沒有擋 `remaining < 0`。最小重現是 `resolveRemainingPercent({ available: true, used: 150, limit: 100 }, null)` 或 `remaining = -1`,會算出負百分比,讓使用量摘要出現不合理的 `-50%` 之類結果,和函式註解宣告的「負數視為無法計算」不一致。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:21",
|
||||
"role": "Leo",
|
||||
"original_finding": "Markdown 表格列直接把 `role`、`location`、`suggestion` 原樣插進去,沒有處理 `|`、換行或其他會破壞表格結構的字元。現在看起來能跑,但只要 LLM 產出一個含管線符號的建議,表格格式就會裂掉,後續維護者會一直在修奇怪的留言排版。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/preflight.js:126",
|
||||
"role": "Leo",
|
||||
"original_finding": "`clientVersion` 被硬編成 `0.142.5`,這種版本字串沒有單一來源,時間一久幾乎一定會過期。到時候 preflight 會因為一個靜態常數失效,維護者還得回頭搜尋到底是哪裡卡住,排查成本很高。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/roles.js:181",
|
||||
"role": "Maya",
|
||||
"original_finding": "`buildVerdictPrompt` 是防守方裁決流程的 prompt 契約,但目前沒有任何測試確認它會帶入預設 Paladin 人設、`exclusionHint`、以及 `confirmed / false_positive` 的輸出格式。這類 prompt 一旦格式偏掉,後面的對話收斂會很難診斷。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/resolve.js:54",
|
||||
"role": "Maya",
|
||||
"original_finding": "`groupConversations` 目前有測 `position` 與 `original_position`,但沒有測 `new_position` 這個常見的 Gitea review comment 欄位。這代表如果 API 回來的是 `new_position`,對話分組與 bot finding 對位是否正確,現在沒有被試煉過。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
},
|
||||
{
|
||||
"location": "src/comments.js:186",
|
||||
"role": "Rogue",
|
||||
"original_finding": "這裡先把 `commentFindings` 全部排序,再過濾掉 `is_new === false` 的舊問題。等於對一批最後根本不會送出的資料先付一次 O(n log n) 排序成本,舊 finding 越多,這筆白工越大。",
|
||||
"reason": "使用者裁定:本輪僅修復 main() 整合測試(critical #2),其餘 findings 一律判為排除(可接受現況/待後續人工處理)。"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,298 +1 @@
|
||||
[
|
||||
{
|
||||
"level": "critical",
|
||||
"role": "Assassin",
|
||||
"location": "src/resolve.js:253",
|
||||
"problem": "這裡會把 PR 上所有未解決的 review comment ID 全部送去 resolve,而不是只處理 AI Review bot 自己建立的 thread。攻擊者只要開 PR 觸發這個 action,就可能讓 bot 關閉人類審查者留下的安全疑慮或阻擋性對話,繞過人工審查流程。",
|
||||
"suggestion": "只 resolve 可證明由本 bot 建立且格式符合預期的 comment,例如檢查作者、固定 marker、review body 簽章或 botFinding 解析結果;人類留言與未知格式留言不得自動關閉。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "critical",
|
||||
"role": "Maya",
|
||||
"location": "src/main.js:44",
|
||||
"problem": "這個 `main()` 是整個 action 的流程總管,但目前沒有任何整合測試或端到端測試去驗證 Step3~Step11 的分支切換與 `process.exit()` 行為。像是前置驗證失敗、偵測到 bot 自動提交、diff 為空、所有角色分析都失敗、JSON 驗證失敗、出現 critical finding、以及 commit/push 降級路徑,現在都只靠人工推演,實際接線後一旦流程順序或退出條件出錯,現有單元測試抓不到。",
|
||||
"suggestion": "補一組 `main.test.js`,把各模組依賴都 mock 掉,分別覆蓋 `runPreflight=false`、`shouldSkipBotCommit=true`、`getPRDiff=''`、分析全失敗、JSON 驗證拋錯、filtered 含 critical、push 失敗但流程不中斷等分支,並斷言對應的 exit code、呼叫順序與關鍵 log。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "critical",
|
||||
"role": "Assassin",
|
||||
"location": "src/gitea.js:66",
|
||||
"problem": "這裡直接從 PR head 讀取 `.reviewignore`,再拿它當成排除規則。攻擊者可以在自己的分支塞入排除條目,讓 bot 故意跳過包含惡意變更的檔案或整個目錄,等於自己決定哪些地方不被審查。",
|
||||
"suggestion": "不要信任 PR head 裡的 `.reviewignore` 來決定安全掃描範圍;改從受保護的 base branch 或 maintainer 管控的位置讀取,且要與固定的預設排除清單合併,而不是讓它覆蓋預設規則。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "critical",
|
||||
"role": "Assassin",
|
||||
"location": "src/findings.js:352",
|
||||
"problem": "這裡會直接讀取 PR 工作樹中的 `.gitea/ai-review/exclusions.json` 當成可信排除來源。攻擊者可以先在分支裡放一份藏在 `.gitea/` 下的 exclusions 檔,利用被忽略的路徑把自己的問題先排除掉,讓後續的 findings 被靜默吃掉。",
|
||||
"suggestion": "把 exclusions 視為 bot 自己管理的狀態,不要從 PR head 的工作樹直接信任既有內容;應該改成只讀受保護來源,或在載入前驗證檔案確實由 bot 生成且未被 PR 作者預先植入。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "critical",
|
||||
"role": "Assassin",
|
||||
"location": "src/main.js:158",
|
||||
"problem": "這裡直接載入 PR 工作樹中的 `.gitea/ai-review/exclusions.json` 當成既有排除規則。攻擊者可以先在 PR 內預埋一份排除清單,因為 `.gitea/` 又被預設排除於 diff 之外,這些惡意排除不會被審查到,卻會被流程直接拿來吞掉真正的 findings,形成靜默的審查繞過。",
|
||||
"suggestion": "不要從 PR head 讀取可由提交者任意修改的 exclusions;只接受由受信任 bot、受保護分支或外部持久化儲存產生的排除資料,並驗證來源身分與 commit marker,避免使用者自行預埋排除規則。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Assassin",
|
||||
"location": "src/llm.js:21",
|
||||
"problem": "這裡把未清洗的 `userContent` 直接塞進模型提示詞,等於讓 PR 內容、留言內容或其他外部文字能反過來操控 LLM。攻擊者可以在 diff 裡埋入『忽略前述規則、回傳空陣列』這類指令,讓審查模型漏報真正的風險或把嚴重問題降級成誤報。",
|
||||
"suggestion": "不要把不可信內容當成可執行指令使用。至少要把 diff/留言做更強的結構化封裝與逸出處理,並在輸出端加上嚴格的 JSON schema 驗證與 deterministic guardrail,避免 LLM 直接決定安全性結論。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Mage",
|
||||
"location": "src/main.js:69",
|
||||
"problem": "這裡先檢查 head SHA 對應的訊息是否為 failure,但如果 SHA 查詢失敗或是空值,後面的 `shouldSkipBotCommit()` 仍可能只看到分支 head 上的 `[ai-review-bot]` 標記就直接跳過。最小重現:`getCommitMessageBySha()` 因 Gitea API 暫時失敗回空字串,而分支 head 正好是 `[ai-review-bot][failure]`,流程就會 exit 0,等於把本來應該失敗的 bot commit 當成可跳過的自動提交。",
|
||||
"suggestion": "把「是否跳過」和「是否 failure」分開判斷,或讓 helper 回傳解析出的 outcome;只允許 success 標記走 skip,failure 標記不論 SHA/branch 來源都應優先讓流程失敗。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Rogue",
|
||||
"location": "src/main.js:132",
|
||||
"problem": "這裡把每個角色的 LLM 分析逐一 await,6 個角色就把總耗時堆成約 6 倍單次模型延遲;這些分析彼此獨立,CPU 沒偷到時間,反而把整條 pipeline 卡在序列網路/CLI 呼叫上。",
|
||||
"suggestion": "改用 Promise.allSettled 平行執行 roles.map(role => analyzeWithRole(role, diff)),再彙整 fulfilled 結果與 warning;保留 fulfilledAnalyses 的判斷即可。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Leo",
|
||||
"location": "src/findings.js:381",
|
||||
"problem": "`loadExclusions()` 同時負責讀檔、解析多種格式、正規化、去重、記錄 repo 狀態、改寫原檔、鏡像寫入與建立 AI prompt 摘要。這個函式的職責過多,之後只要調整 exclusions 格式或同步策略,就很容易牽動不相關行為。",
|
||||
"suggestion": "拆成 `readExclusionsFile()`、`normalizeExclusionsData()`、`canonicalizeExclusionsFile()`、`logExclusionMetadata()` 等小函式,讓讀取、轉換、寫回與診斷各自可測。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Mage",
|
||||
"location": "src/findings.js:466",
|
||||
"problem": "這裡優先使用 `ex.textKey`,但 `textKey` 是由 `toKeyText()` 產生的無分隔且未轉小寫文字,而 findingText 是 `normalizeText()` 產生的小寫、以空白分隔文字。最小重現:排除文字 `Update tests` 會變成 `Updatetests`,finding suggestion 會變成 `update tests`,兩邊互相 `includes` 都不成立,導致純文字排除規則失效。",
|
||||
"suggestion": "排除條目與 finding 應使用同一套正規化函式比對;例如改存並使用 `normalizeText(ex.text || ex.suggestion || ex.title || '')`,或讓 finding 也轉成同樣的 compact/lowercase key。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/findings.js:416",
|
||||
"problem": "applyExclusions 的核心比對支援「只有文字、沒有路徑/角色」的排除規則,但現有測試多半靠相同檔案路徑命中,沒有驗證純文字排除、空文字排除、大小寫/標點差異等邊界。這條排除規則的最脆弱分支還沒被測到。",
|
||||
"suggestion": "補上 applyExclusions 的邊界測試:只有 suggestion/title 文字沒有 location 的 exclusion 應如何比對;空文字 exclusion 不應意外排除全部;標點、空白、大小寫正規化後相同的文字應依預期排除。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/gitea.js:95",
|
||||
"problem": "shouldSkipBotCommit 目前只看到命中 bot marker 的測試,缺少「commit API 失敗、分支查詢失敗、sha/branch 都沒有 marker」時應回 false 的失敗與保守路徑驗證。這是避免 workflow 誤跳過審查的關鍵判斷,不能只測快樂路徑。",
|
||||
"suggestion": "新增測試讓 getCommitMessageBySha / getBranchHeadCommitMessage 對應的 axios 呼叫拋錯或回一般 commit message,斷言 shouldSkipBotCommit 回 false,且不會把查詢失敗誤判成 bot commit。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/llm.js:66",
|
||||
"problem": "`runAssistantCLI()` 目前只有成功與一般失敗的測試,沒有覆蓋 timeout、`maxBuffer` 超限、以及 `opencode` 分支建立的暫存 prompt 檔在例外發生時是否確實清理。這些都是外部 CLI 整合最常出問題的失敗路徑,沒有測到就很難確定不會留下殘檔或把流程卡死。",
|
||||
"suggestion": "補 fake CLI 測試,讓子程序超時、輸出超過 `AI_ASSISTANT_MAX_BUFFER`、以及 `opencode` 在 `spawn`/`close` 前後失敗,分別斷言會回傳對應錯誤,且暫存目錄與 `prompt.md` 會被清掉。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Rogue",
|
||||
"location": "src/gitea.js:239",
|
||||
"problem": "這裡逐一 await 每個 review 的 comments,PR review 一多就變成 N 次遠端呼叫的線性延遲累加;例如 30 個 review 就是 30 個 round-trip 排隊等,時間都被網路空轉偷走。",
|
||||
"suggestion": "把 reviews.map(review => getPullReviewComments(review.id).catch(...)) 丟進 Promise.all 或 Promise.allSettled 平行抓取,再 flat 結果;單筆失敗仍可記 warn 後略過。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Rogue",
|
||||
"location": "src/findings.js:442",
|
||||
"problem": "這裡每一筆 finding 都要跟整包 exclusions 做一次 `.some()`,而且內層還反覆跑 `normalizeText` 和字串包含比對,資料一多就直接變成 O(F×E) 的熱點。像 300 筆 finding 配 500 筆 exclusions,會吃掉 15 萬次以上的比對與正規化,CPU 和字串配置都在浪費。",
|
||||
"suggestion": "先把 exclusions 在載入時一次正規化並依 `filePath / role / textKey` 建索引,讓過濾改成近似 O(F);至少把 `normalizeText` 移到內層迴圈外,避免同一段字串被重算成百上千次。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Mage",
|
||||
"location": "src/resolve.js:213",
|
||||
"problem": "`findingSig` 只用檔案路徑加上 `suggestion` 來識別問題,忽略了 `problem`、`role`,也沒有留下任何穩定的 thread 識別;最小重現:同一個 `a.js` 內有兩條都建議「加上 null 檢查」但其實是不同位置的 finding,先解掉其中一條後,另一條也會被當成同一筆而被 `dropResolvedFindings` / `addCarriedFindings` 誤合併或誤刪。",
|
||||
"suggestion": "把識別鍵改成更穩定的組合,例如檔案路徑 + 正規化後的 `problem` + `suggestion` + `role`,或直接使用可追蹤的 thread/issue id;不要只靠 `suggestion` 斷言是不是同一個問題。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Mage",
|
||||
"location": "src/comments.js:40",
|
||||
"problem": "這裡把 `file:0` 也視為有效行號;最小重現:只要上游傳進 `app/foo.js:0`,`parseLocation()` 會回傳 line=0,後續 `postPullReviewComment` 會帶著 `new_position: 0` 發到 Gitea,通常會被拒絕或定位失敗。也就是說,0 行號沒有被當成缺值處理。",
|
||||
"suggestion": "把行號門檻改成 `> 0`,`0` 與負數都應視為無效;同時讓需要行號的呼叫端把這種情況當作缺行號,重新定位或降級處理。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/comments.js:233",
|
||||
"problem": "`postFindingsReview` 的降級流程有兩層:先嘗試批次 review,再失敗時改成 summary-only,最後 summary-only 也失敗才退回一般 comment。現在的測試只驗到第一層失敗後、第二層成功的情境,沒有驗證 summary-only 也失敗時是否真的會呼叫 `postIssue(body)`,這是最脆弱的 fallback 路徑之一。",
|
||||
"suggestion": "新增一個測試讓第一次 `postReview({comments})` 失敗、第二次 `postReview({comments: []})` 也失敗,然後斷言 `postIssue(body)` 有被呼叫,且 inline comments 仍會逐筆嘗試送出。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/config.js:31",
|
||||
"problem": "這裡是整個 action 讀取 `INPUT_*`、`GITEA_*` 與事件 payload 的入口,但測試只覆蓋了 `getLLMConfig()`,沒有把 `GITEA_TOKEN`、`GITEA_COMMENT_TOKEN`、`PR_NUMBER`、`PR_HEAD_SHA` 這些環境與 payload 的優先序鎖住。特別是 comment token 退回主 token、以及 event 檔讀不到時回到空值的情境,都是 CI 最容易因環境差異壞掉的地方。",
|
||||
"suggestion": "新增 config 相關測試,分別用假 `process.env` 和暫存 event payload 檔驗證:`INPUT_*` 會蓋過 ambient env、`GITEA_COMMENT_TOKEN` 缺值時會 fallback 到主 token、`GITEA_EVENT_PATH` / `GITHUB_EVENT_PATH` 讀取失敗時不會拋錯且回傳預設值。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Bard",
|
||||
"location": "src/config.js:27",
|
||||
"problem": "這段註解已經跟著介面走音了。它宣稱使用端「只需傳 `with: token`」,但這次 action 其實已新增 `comment_token` 與 `model` 等輸入,註解仍停留在舊旋律,容易讓讀者誤判介面現況。",
|
||||
"suggestion": "把這組說明改成與目前 inputs 一致,明確列出 `token`、`comment_token`、`model` 的優先序與用途;如果無法精簡,就直接移到 README 或設計文件,避免在程式中留下過時註記。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Mage",
|
||||
"location": "src/findings.js:227",
|
||||
"problem": "這個抽取器一旦命中目標檔案,就一路把後面的 diff 全部帶進去,沒有在下一個 `diff --git` 區塊時停下來。最小重現:diff 同時有 `a.js` 和 `b.js`,要補 `a.js` 的行號時,送給 LLM 的內容會混進 `b.js` 的 hunks,結果很容易定位到錯的行,或讓模型把別檔的內容誤認成目標檔上下文。",
|
||||
"suggestion": "在開始捕捉後,遇到下一個 `diff --git ` 就應該停止,只回傳目前檔案那一段;找不到目標檔時再退回整份 diff。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/findings.js:277",
|
||||
"problem": "`deduplicateWithAI` 是新的核心語意去重流程,但目前完全沒有直接測試它的成功與失敗分支。尤其是 LLM 回傳排序不同、夾雜幻覺項目、回傳空陣列或超量結果時,程式會改走保守 fallback,這些都是很容易壞掉但現在沒被驗證的邊界。",
|
||||
"suggestion": "替 `deduplicateWithAI` 補測兩類情境:一是 stub `chatJSON` 回傳重排後的重複項與一筆幻覺項,確認只保留能對應回原始 findings 的資料;二是回傳空陣列或不合法結果時,確認會保守回傳原始 findings。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Rogue",
|
||||
"location": "src/findings.js:322",
|
||||
"problem": "每一筆缺行號的 finding 都重新呼叫 `extractFileDiff(diff, file)` 掃完整份 diff,若同一檔案有 k 筆問題,就會重複做 k 次整份 diff 解析,浪費量是 O(k × diff長度)。",
|
||||
"suggestion": "先把 diff 依檔名切成快取 Map,一次掃描建立好 `file -> fileDiff`,後續同檔 finding 直接共用已切好的片段。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Bard",
|
||||
"location": "src/llm.js:56",
|
||||
"problem": "`cliArgs` 把不同提供者的參數拼湊在同一個分支裡,還讓 `opencode` 走了另一套文字輸入路線,整個 helper 的節奏忽然一分為二。讀起來像兩個介面硬塞進同一支笛子。",
|
||||
"suggestion": "拆成各提供者各自的 argv builder,或至少把 prompt 輸入方式抽成獨立 helper,讓每個分支只處理一種責任,結構會更俐落。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Leo",
|
||||
"location": "src/findings.js:1",
|
||||
"problem": "這個模組同時處理舊 findings 載入、合併去重、缺行號補齊、排除規則正規化、誤報過濾、AI 去重、以及 exclusions 的讀寫,職責已經混成一包。更麻煩的是 `loadExclusions`、`appendExclusions`、`applyExclusions` 各自都有一套相近但不完全一致的比對邏輯,未來只要規則改一處,另一處沒同步就會開始出現不可預期的行為差異。",
|
||||
"suggestion": "把 exclusions 的正規化與比對規則抽成唯一來源,例如 `normalizeExclusionEntry` + `matchesExclusion` 之類的共用 helper,並把 AI 去重、行號補齊、檔案持久化拆到不同模組,減少這個檔案的責任面。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Mage",
|
||||
"location": "src/json.js:113",
|
||||
"problem": "這裡只檢查 `JSON.parse(normalized)` 能不能成功,沒有確認修復後的內容真的是陣列。最小重現是 AI 把 `findings.json` 修成 `{ \"a\": 1 }`,函式會照樣寫回檔案並回報成功,但下一輪讀取時 `readJSONArray` 會把它當成非陣列而視為空值,等於把資料靜默吃掉。",
|
||||
"suggestion": "在寫檔前先 `const parsed = JSON.parse(normalized)`,再加上 `Array.isArray(parsed)` 檢查;不是陣列就直接丟錯,不要覆寫原檔。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Mage",
|
||||
"location": "src/resolve.js:74",
|
||||
"problem": "這裡用 `path + line` 當唯一群組鍵,且只保留第一筆 `botFinding`。最小重現是同一個檔案同一行同時被兩個角色指出不同問題,`groupConversations` 會把它們合成同一組,後來的那筆 finding 會被吞掉,導致後續關閉、回寫或保留時少掉一個問題。",
|
||||
"suggestion": "不要只用 `path + line` 折疊所有 comment;至少要保留同一組內的所有 botFinding,或改成以 comment id / finding 本身為單位處理,再在最後階段做去重。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/comments.js:214",
|
||||
"problem": "這裡新增了 `postOldFindingsComment` 與 `postNewNonCriticalComment` 兩條公開的留言分流路徑,但現有測試只驗證了 `postNewCriticalComments` 與 `postFindingsReview`,完全沒有案例確認這兩個函式的篩選條件、空陣列時是否跳過、以及輸出的 Markdown 內容是否真的只包含對應的 findings。這種分流邏輯一旦算錯,就會發生該發的沒發、或不該公告的問題被貼出去。",
|
||||
"suggestion": "補上這兩個函式的單元測試:空陣列時不呼叫 `postComment`;`postOldFindingsComment` 只送出 `is_new === false` 的項目;`postNewNonCriticalComment` 只送出 `is_new` 且 `level !== 'critical'` 的項目;再斷言 comment 標題與表格列數都符合預期。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/comments.js:171",
|
||||
"problem": "`postFindingsReview` 的救援路徑只測到「批次 review 失敗後,改發逐筆 inline comment」這一段,卻沒有驗證第二次 `postReview({ comments: [] })` 也失敗時,會正確降級到 `postIssue(body)`。這條路徑是 Gitea review API 整個故障時保住摘要的最後保險絲,沒測到的話,真正出事時很容易靜默漏報。",
|
||||
"suggestion": "新增一個雙重失敗測試:第一次 `postReview` 因 comments 拋錯、第二次 `postReview` 也拋錯,最後斷言有呼叫 `postIssue`,而且 inline comments 仍會依序嘗試發布。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Maya",
|
||||
"location": "src/main.js:17",
|
||||
"problem": "`main()` 整個流程目前沒有任何直接測試,只能靠零散的子函式單測推測結果;但這裡包含多個關鍵分支與 `process.exit` 行為,例如 preflight 失敗、bot 自動提交跳過、空 diff 提早結束、JSON 驗證失敗、以及偵測到 critical 後結束失敗。只要接線順序或退出碼改壞,現有測試不會第一時間抓到。",
|
||||
"suggestion": "補一組整合測試,把 `runPreflight`、`getPRDiff`、`reconcileConversations`、`validateJSONArrayFile`、`commitAndPush` 以 stub 注入,逐一覆蓋 Step3/5/9/11 的 exit 0/1 分支,至少驗證 `process.exit` 與主要副作用被正確觸發。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"role": "Maya",
|
||||
"location": "src/comments.js:25",
|
||||
"problem": "Markdown 表格列直接嵌入 role、location、suggestion,但測試沒有覆蓋 suggestion 含 `|`、換行或 Markdown 特殊字元時的輸出。這不是要求現在一定要改格式,而是目前缺少案例確認表格在真實 LLM 輸出下不會被破壞。",
|
||||
"suggestion": "補一個 comment body 格式測試,輸入 suggestion 含 pipe、換行與粗體符號,斷言輸出的 Markdown 結構符合預期;若目前行為會破表,應先定義轉義或替換規則再測。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"role": "Rogue",
|
||||
"location": "src/comments.js:102",
|
||||
"problem": "統計表與單行摘要各欄位都用 `filter(...).length` 重掃多次,同一批 findings 會被走 4 到 8 次。資料量一大,連 log 文字本身都開始吃不必要的掃描成本。",
|
||||
"suggestion": "改成單次迴圈同時累加 critical / warning / info / 未分類計數,再把結果組成表格和摘要;一次走完就好,別讓統計自己變熱點。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"role": "Maya",
|
||||
"location": "src/comments.js:266",
|
||||
"problem": "`postOldFindingsComment` 與緊接著的 `postNewNonCriticalComment` 都是這次新加的對外 comment 發布行為,但目前沒有專門測試它們的空陣列早退、標題文字與表格內容。這會讓 comment 分流邏輯只靠間接測試支撐,回歸時很容易漏掉。",
|
||||
"suggestion": "補這兩個函式的單元測試:至少驗證空陣列時不會送 comment、非空時 body 內容包含正確標題與表格,且 `postOldFindingsComment` 只收舊問題、`postNewNonCriticalComment` 只收新非 critical 問題。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"role": "Rogue",
|
||||
"location": "src/findings.js:393",
|
||||
"problem": "前面已經把 exclusions 正規化、去重過一次了,這裡為了 log 又再丟進 `buildExclusionContext` 重做 normalize / dedupe / group。等於同一批資料在同一輪流程裡被重算兩次,白白多吃一輪 O(n) 到 O(n log n) 的 CPU。",
|
||||
"suggestion": "把第一次處理的摘要一起回傳或快取下來,後面的 log 直接重用同一份結果,不要再對同一批 exclusions 重跑分組。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"role": "Rogue",
|
||||
"location": "src/resolve.js:88",
|
||||
"problem": "這個 `codeWindow` 每遇到一筆 open conversation 就對整份檔案內容再 `split('\\n')` 一次。若同一個檔案有多條 thread,O(L) 的切割和陣列配置會被重複吃掉,明明同一份內容卻一直重複解剖。",
|
||||
"suggestion": "先把檔案內容預先切成行陣列並快取,或讓 `codeWindow` 直接吃已分割好的 lines;這樣同檔多條對話就不用重複掃描整份內容。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"role": "Leo",
|
||||
"location": "src/resolve.js:201",
|
||||
"problem": "`reconcileConversations()` 同時在做 comment 分組、關閉遠端 review、讀檔、抽 code window、AI 裁決、再把結果拆成 resolved / excluded / carried 三條路徑,流程很完整,但也很難局部理解或替換。未來任何一段判斷要調整,都得先吞下整個函式的心智負擔,維護門檻偏高。",
|
||||
"suggestion": "把它拆成幾個可單獨測試的步驟,例如 `collectOpenConversations()`、`loadConversationCode()`、`judgeConversationVerdicts()`、`mapVerdictsToFindings()`,讓主流程只保留編排,不要把資料轉換與外部副作用全塞在一起。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"role": "Mage",
|
||||
"location": "src/usage.js:212",
|
||||
"problem": "這個百分比計算只擋了 `limit <= 0`,沒有擋 `remaining < 0`。最小重現是 `resolveRemainingPercent({ available: true, used: 150, limit: 100 }, null)` 或 `remaining = -1`,會算出負百分比,讓使用量摘要出現不合理的 `-50%` 之類結果,和函式註解宣告的「負數視為無法計算」不一致。",
|
||||
"suggestion": "把 `remaining < 0` 也納入無效值判斷,直接回 `null`;若你想保守顯示,可另外在輸出層把負值夾到 0,但不要讓計算層回傳負百分比。",
|
||||
"is_new": true
|
||||
}
|
||||
]
|
||||
[]
|
||||
|
||||
@@ -1,46 +1,112 @@
|
||||
# 用途:CI workflow 的 command-file 草稿,保留原始流程並補上逐行說明。
|
||||
# 更新時間:2026/07/11 19:00:45
|
||||
# workflow 名稱,對應 Gitea UI 中的顯示標題。
|
||||
name: CI
|
||||
# 定義此 workflow 的觸發事件。
|
||||
on:
|
||||
# 在 pull request 事件時執行。
|
||||
pull_request:
|
||||
# 只在建立與同步更新 PR 時觸發。
|
||||
types: [opened, synchronize]
|
||||
# workflow 內的工作列表。
|
||||
jobs:
|
||||
# 第一個 job:負責版本計算與 release 發佈。
|
||||
build:
|
||||
# job 顯示名稱,用來區分執行階段。
|
||||
name: 1. BUILD
|
||||
# 使用 Ubuntu runner 執行。
|
||||
runs-on: ubuntu
|
||||
# job 層級環境變數。
|
||||
env:
|
||||
VERSION: "0.0.0-beta.${{ gitea.run_number }}"
|
||||
# 目標分支為 develop 時視為 beta。
|
||||
IS_BETA: ${{ gitea.base_ref == 'develop' }}
|
||||
# 對外輸出供後續 job 使用。
|
||||
outputs:
|
||||
# 輸出版本字串。
|
||||
version: ${{ env.VERSION }}
|
||||
# 輸出是否為 beta。
|
||||
is_beta: ${{ env.IS_BETA }}
|
||||
# build job 的執行步驟。
|
||||
steps:
|
||||
# 先依 repo 狀態計算版本號。
|
||||
- name: Calculate Version
|
||||
# 供後續步驟讀取輸出用的 step id。
|
||||
id: calculate-version
|
||||
# 使用版本計算 action。
|
||||
uses: https://gitea.jsc.idv.tw/actions/calculate-version@${{ vars.ACTION_CALCULATE_VERSION }}
|
||||
# 傳入 action 參數。
|
||||
with:
|
||||
# 告知 action 是否為 beta 分支情境。
|
||||
is_beta: ${{ env.IS_BETA }}
|
||||
# 依計算出的版本建立 release。
|
||||
- name: Publishing Release
|
||||
# 使用 release action。
|
||||
uses: akkuman/gitea-release-action@${{ vars.ACTION_GITEA_RELEASE_VERSION }}
|
||||
# 這個 step 的環境變數。
|
||||
env:
|
||||
# 取前一步算出的版本號。
|
||||
VERSION: ${{ steps.calculate-version.outputs.version }}
|
||||
# release action 的參數。
|
||||
with:
|
||||
# release 名稱。
|
||||
name: "${{ gitea.event.repository.name }} v${{ env.VERSION }}"
|
||||
# release tag 名稱。
|
||||
tag_name: "v${{ env.VERSION }}"
|
||||
# 指向目前提交。
|
||||
target_commitish: ${{ gitea.sha }}
|
||||
prerelease: ${{ gitea.base_ref == 'develop' }}
|
||||
# beta 情境時標記為 prerelease。
|
||||
prerelease: ${{ env.IS_BETA }}
|
||||
# 第二個 job:在 beta 情境執行 AI Code Review。
|
||||
test:
|
||||
# job 顯示名稱。
|
||||
name: 2. TEST
|
||||
# 使用 Ubuntu runner。
|
||||
runs-on: ubuntu
|
||||
# 必須等 build job 完成。
|
||||
needs: [build]
|
||||
# 只有 beta 情境才執行。
|
||||
if: ${{ needs.build.outputs.is_beta == 'true' }}
|
||||
# job 層級環境變數。
|
||||
env:
|
||||
# 使用 build job 輸出的版本號。
|
||||
VERSION: ${{ needs.build.outputs.version }}
|
||||
# test job 的步驟。
|
||||
steps:
|
||||
# 安裝或設定 LLM CLI。
|
||||
- name: Setup LLM CLI
|
||||
# 使用對應的 setup action。
|
||||
uses: https://gitea.jsc.idv.tw/actions/setup-${{ vars.ACTION_SETUP_LLM_CLI }}
|
||||
# 傳入設定。
|
||||
with:
|
||||
# LLM CLI 的 OAuth 憑證。
|
||||
oauth: ${{ secrets.LLM_OAUTH }}
|
||||
# 執行 AI Code Review action。
|
||||
- name: Run AI Code Review
|
||||
# step id,方便追蹤。
|
||||
id: ai-code-review
|
||||
# 使用本 repo 發佈的 action。
|
||||
uses: https://gitea.jsc.idv.tw/actions/ai-code-review@v${{ env.VERSION }}
|
||||
# action 參數。
|
||||
with:
|
||||
# 存取 Gitea API 的 token。
|
||||
token: ${{ secrets.TOKEN }}
|
||||
# 指定 LLM 模型名稱。
|
||||
model: ${{ vars.LLM_NAME }}
|
||||
# 第三個 job:輸出最終版本資訊。
|
||||
result:
|
||||
# job 顯示名稱。
|
||||
name: 3. RESULT
|
||||
# 使用 Ubuntu runner。
|
||||
runs-on: ubuntu
|
||||
# 需等待前兩個 job。
|
||||
needs: [build,test]
|
||||
# job 層級環境變數。
|
||||
env:
|
||||
# 延續 build 的版本號。
|
||||
VERSION: ${{ needs.build.outputs.version }}
|
||||
# result job 的步驟。
|
||||
steps:
|
||||
# 顯示版本號。
|
||||
- name: Show Version
|
||||
# 將版本輸出到 log。
|
||||
run: echo "$VERSION"
|
||||
|
||||
@@ -1,21 +1,52 @@
|
||||
# 用途:master workflow 的 command-file 草稿,保留原始流程並補上逐行說明。
|
||||
# 更新時間:2026/07/11 19:00:45
|
||||
# workflow 名稱,對應 Gitea UI 中的顯示標題。
|
||||
name: CD
|
||||
# 定義此 workflow 的觸發事件。
|
||||
on:
|
||||
# 在 push 事件時執行。
|
||||
push:
|
||||
# 限定觸發分支。
|
||||
branches:
|
||||
# 只有推送到 master 分支才執行。
|
||||
- master
|
||||
# workflow 內的工作列表。
|
||||
jobs:
|
||||
# 單一 job:輸出 context、檢查 commit tag。
|
||||
deploy:
|
||||
# job 顯示名稱。
|
||||
name: DEPLOY
|
||||
# 使用 Ubuntu runner。
|
||||
runs-on: ubuntu
|
||||
# job 層級環境變數。
|
||||
env:
|
||||
# 將完整 Gitea context 轉成 JSON 字串。
|
||||
GITEA_CONTEXT: ${{ toJSON(gitea) }}
|
||||
# 取第二筆 commit 的 id 作為查詢目標。
|
||||
COMMIT_SHA: ${{ gitea.event.commits[1].id }}
|
||||
# deploy job 的步驟。
|
||||
steps:
|
||||
# 顯示 Gitea context。
|
||||
- name: Show Gitea Context
|
||||
# 將 context 格式化輸出。
|
||||
run: echo "$GITEA_CONTEXT" | jq .
|
||||
# 取回完整原始碼與 tags。
|
||||
- name: Source Code Checkout
|
||||
# 使用 checkout action。
|
||||
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
|
||||
# checkout 參數。
|
||||
with:
|
||||
# 取得完整歷史。
|
||||
fetch-depth: 0
|
||||
# 一併抓取 tags。
|
||||
fetch-tags: true
|
||||
# 查詢指定 commit 對應的 tag。
|
||||
- name: Get Commit Tag
|
||||
# 供後續步驟讀取輸出。
|
||||
id: commit
|
||||
# 把查到的 tag 寫入 action 輸出。
|
||||
run: echo "tag=$(git describe --contains ${{ env.COMMIT_SHA }})" >> $GITEA_OUTPUT
|
||||
# 顯示剛查到的 tag。
|
||||
- name: Show Tag
|
||||
# 將 tag 印到 log。
|
||||
run: echo "${{ steps.commit.outputs.tag }}"
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
# GITEA NODE ACTION 的工作流列表
|
||||
# GITEA NODE ACTION 工作流說明草稿
|
||||
|
||||
- CI
|
||||
- BUILD
|
||||
- TEST
|
||||
- RESULT
|
||||
- CD
|
||||
- BUILD
|
||||
- DEPLOY
|
||||
更新時間:2026/07/11 18:54:51
|
||||
|
||||
## 總覽
|
||||
|
||||
此專案目前包含兩個 workflow:
|
||||
|
||||
- `CI`:處理 pull request 期間的版本計算、釋出與 AI 程式碼審查。
|
||||
- `CD`:處理推送到 `master` 分支後的部署相關檢查與資訊輸出。
|
||||
|
||||
## Workflow 明細
|
||||
|
||||
### CI
|
||||
|
||||
- 檔案位置:`.gitea/workflows/ci.yaml`
|
||||
- 用途:在 pull request 事件中計算版本,必要時建立 release,並在 beta 情境下執行 AI 程式碼審查。
|
||||
- 觸發條件:`pull_request`,事件類型為 `opened` 與 `synchronize`。
|
||||
- 主要輸入 / 環境參數:
|
||||
- `gitea.base_ref`:用來判斷是否為 `develop`,進而決定 `IS_BETA`。
|
||||
- `vars.ACTION_CALCULATE_VERSION`:提供 `calculate-version` action 的版本。
|
||||
- `vars.ACTION_GITEA_RELEASE_VERSION`:提供 release action 的版本。
|
||||
- `secrets.LLM_OAUTH`:設定 LLM CLI 的 OAuth。
|
||||
- `secrets.TOKEN`:提供 AI 程式碼審查 action 存取 Gitea API。
|
||||
- `vars.LLM_NAME`:指定審查使用的模型名稱。
|
||||
- 重要注意事項:
|
||||
- `test` job 只會在 `IS_BETA == true` 時執行,也就是 pull request 目標分支為 `develop` 時。
|
||||
- `Publishing Release` 會使用 `VERSION` 與 `gitea.sha` 建立 release 與 tag。
|
||||
- 若變數或 secret 未設定,對應步驟會失敗,需人工確認部署前置條件。
|
||||
|
||||
### CD
|
||||
|
||||
- 檔案位置:`.gitea/workflows/master.yaml`
|
||||
- 用途:在 `master` 分支推送後輸出 Gitea context、檢查提交標籤,作為後續部署流程的基礎。
|
||||
- 觸發條件:`push` 到 `master` 分支。
|
||||
- 主要輸入 / 環境參數:
|
||||
- `gitea` 事件內容:轉成 `GITEA_CONTEXT` 後交給 `jq` 顯示。
|
||||
- `gitea.event.commits[1].id`:作為 `COMMIT_SHA`,用來查詢 commit tag。
|
||||
- `vars.ACTION_CHECKOUT_VERSION`:提供 `actions/checkout` 的版本。
|
||||
- `GITEA_OUTPUT`:寫入 `git describe --contains` 的結果。
|
||||
- 重要注意事項:
|
||||
- `COMMIT_SHA` 取用 commits 陣列的第 2 筆資料,若 push 事件實際只有 1 筆 commit,需人工確認是否會發生索引風險。
|
||||
- `Get Commit Tag` 依賴完整的 git 歷史與 tags,因此 checkout 已設定 `fetch-depth: 0` 與 `fetch-tags: true`。
|
||||
- `Show Gitea Context` 會輸出完整事件內容,若包含敏感資訊,需注意執行環境的日誌保存策略。
|
||||
|
||||
## 備註
|
||||
|
||||
- 本檔為草稿版本,僅整理 workflow 行為與設定重點,不修改任何 workflow 實際邏輯。
|
||||
- 若後續要覆蓋正式檔,請先確認 `ci.yaml` 與 `master.yaml` 的變數與 secret 已在目標環境中正確配置。
|
||||
|
||||
+18
@@ -1,16 +1,34 @@
|
||||
# 用途:AI Code Review Action 的設定檔草稿,保留原始輸入與 Node 入口,並補上逐行說明。
|
||||
# 更新時間:2026/07/11 18:54:51
|
||||
# Action 顯示名稱,讓工作流程與使用者介面可以辨識此動作。
|
||||
name: 'AI Code Review'
|
||||
# Action 的簡短用途說明。
|
||||
description: 'AI 程式碼審查'
|
||||
# 作者或維護者名稱。
|
||||
author: 'Jeffery'
|
||||
# 定義此 Action 對外提供的輸入參數。
|
||||
inputs:
|
||||
# 用於存取 Gitea API 的授權 Token。
|
||||
token:
|
||||
# token 參數的用途說明。
|
||||
description: '操作 Gitea API 的 Token'
|
||||
# 此參數為必要,執行時必須提供。
|
||||
required: true
|
||||
# 用於存取 Gitea Commit API 的授權 Token。
|
||||
comment_token:
|
||||
# comment_token 參數的用途說明。
|
||||
description: '操作 Gitea Commit API 的 Token'
|
||||
# 此參數為選填,沒有提供時不影響 Action 啟動。
|
||||
required: false
|
||||
# 指定執行 AI 程式碼審查時所使用的模型名稱。
|
||||
model:
|
||||
# model 參數的用途說明。
|
||||
description: '執行 AI 程式碼審查使用的 LLM 名稱'
|
||||
# 此參數為選填,未提供時由執行環境或預設值決定。
|
||||
required: false
|
||||
# 宣告 Action 的執行方式與主要進入點。
|
||||
runs:
|
||||
# 使用 Node.js 24 執行此 Action。
|
||||
using: 'node24'
|
||||
# Action 的主要進入檔,實際邏輯從此檔案開始。
|
||||
main: 'src/main.js'
|
||||
|
||||
@@ -51,6 +51,12 @@ export const EXCLUSIONS_PATH = '.gitea/ai-review/exclusions.json';
|
||||
* @returns {import('https').Agent} 已關閉憑證驗證的 HTTPS Agent 單例。
|
||||
*/
|
||||
let _insecureHttpsAgent = null;
|
||||
/**
|
||||
* 取得一個關閉 TLS 憑證驗證的 HTTPS Agent 單例,供內部服務連線使用。
|
||||
*
|
||||
* @remarks 只應在信任的內網或測試環境使用;若需要完整 TLS 安全性,應改用預設
|
||||
* `https.Agent`,不要調用這個函式。
|
||||
*/
|
||||
export function getInsecureHttpsAgent() {
|
||||
return (_insecureHttpsAgent ??= new https.Agent({ rejectUnauthorized: false }));
|
||||
}
|
||||
@@ -86,10 +92,22 @@ const CLI_CANDIDATES = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 取得目前支援的 LLM CLI 指令名稱清單。
|
||||
*
|
||||
* @remarks 內容直接取自 `CLI_CANDIDATES`,若日後候選清單增減,輸出會同步變動。
|
||||
*/
|
||||
export function getLLMCLICommands() {
|
||||
return CLI_CANDIDATES.map(c => c.command);
|
||||
}
|
||||
|
||||
/**
|
||||
* 檢查指定 CLI 指令是否可在目前環境中執行。
|
||||
*
|
||||
* @param {string} command - 要檢查的指令名稱。
|
||||
* @returns {boolean} 找得到指令時回傳 `true`,否則回傳 `false`。
|
||||
* @remarks 透過 `/bin/sh -lc "command -v <command>"` 檢查,屬於同步存在性檢查。
|
||||
*/
|
||||
function commandExists(command) {
|
||||
try {
|
||||
execFileSync('/bin/sh', ['-lc', `command -v ${command}`], { stdio: 'ignore' });
|
||||
|
||||
@@ -111,6 +111,12 @@ function cleanText(value) {
|
||||
* 以模組層級 Map 對「字串輸入」做 memoization,避免重複跑 NFKC/正則替換。
|
||||
*/
|
||||
const _normalizeTextCache = new Map();
|
||||
/**
|
||||
* 將文字正規化成比對用形式。
|
||||
*
|
||||
* @param {*} value - 任意值。
|
||||
* @remarks 適合用於誤報過濾與排除條目比對。
|
||||
*/
|
||||
export function normalizeText(value) {
|
||||
if (typeof value === 'string' && _normalizeTextCache.has(value)) return _normalizeTextCache.get(value);
|
||||
const result = cleanText(value)
|
||||
|
||||
+28
-1
@@ -56,6 +56,16 @@ function buildPrompt(systemPrompt, userContent) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 依不同 AI provider 產生 CLI 參數。
|
||||
*
|
||||
* @param {*} provider - AI provider 名稱。
|
||||
* @param {*} model - 模型名稱。
|
||||
* @param {*} promptFile - prompt 檔路徑,供 `opencode` 使用。
|
||||
* @param {*} prompt - 直接傳給 CLI 的 prompt 文字,供部分 provider 使用。
|
||||
* @remarks 適合把不同 CLI 的參數差異集中管理。
|
||||
* @remarks 目前支援的 provider 名稱是硬編碼的,新增 provider 時需人工確認是否同步更新所有呼叫端。
|
||||
*/
|
||||
function cliArgs({ provider, model, promptFile = null, prompt = null }) {
|
||||
if (provider === 'codex') {
|
||||
return ['exec', '--model', model, '--sandbox', 'read-only', '--skip-git-repo-check', '-'];
|
||||
@@ -92,12 +102,30 @@ export function extractMeaningfulError(raw, limit = 1000) {
|
||||
return picked.length > limit ? picked.slice(-limit) : picked;
|
||||
}
|
||||
|
||||
/**
|
||||
* 將 CLI 例外整理成較精簡的錯誤摘要。
|
||||
*
|
||||
* @param {*} e - 被拋出的錯誤物件,可能含 `stderr`、`stdout`、`message`。
|
||||
* @remarks 適合在 log 與錯誤重新拋出前先整理訊息。
|
||||
* @remarks 若錯誤物件結構和預期不同,仍會退回字串化處理,屬保守容錯。
|
||||
*/
|
||||
function summarizeCliError(e) {
|
||||
const stderr = String(e.stderr || '').trim();
|
||||
const stdout = String(e.stdout || '').trim();
|
||||
return extractMeaningfulError(stderr || stdout || e.message || String(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* 執行 AI 助理 CLI 並回傳純文字結果。
|
||||
*
|
||||
* @param {*} provider - CLI provider 名稱。
|
||||
* @param {*} command - 實際可執行指令。
|
||||
* @param {*} model - 要使用的模型名稱。
|
||||
* @param {*} prompt - 送給 CLI 的完整 prompt 內容。
|
||||
* @remarks 適合用在需呼叫外部 AI CLI 的情境。
|
||||
* @remarks 逾時與輸出上限由環境變數控制,預設值是保守設定。
|
||||
* @remarks 若子行程回傳非 0,錯誤訊息會由上層摘要處理。
|
||||
*/
|
||||
async function runAssistantCLI({ provider, command, model }, prompt) {
|
||||
let tempDir = null;
|
||||
let promptFile = null;
|
||||
@@ -120,7 +148,6 @@ async function runAssistantCLI({ provider, command, model }, prompt) {
|
||||
child.kill('SIGTERM');
|
||||
reject(new Error(`${provider} CLI 逾時 (${timeout}ms)`));
|
||||
}, timeout);
|
||||
|
||||
const append = (kind, chunk) => {
|
||||
if (kind === 'stdout') stdout += chunk;
|
||||
else stderr += chunk;
|
||||
|
||||
+7
-3
@@ -52,7 +52,7 @@ const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace';
|
||||
* 降級處理:Step4 對話收斂、Step5 角色介紹 comment 與個別角色分析、Step6 clone repo、
|
||||
* Step8 Review 發布等非致命步驟失敗時,僅 `warn` 後繼續執行。
|
||||
*/
|
||||
async function main() {
|
||||
export async function main() {
|
||||
section('AI Code Review Pipeline');
|
||||
|
||||
// Step1 啟動
|
||||
@@ -236,7 +236,11 @@ async function main() {
|
||||
section('Pipeline 結束');
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
// 預設一律自動啟動 pipeline(正式以 node 執行、或由 Gitea node action runtime 載入時皆會執行)。
|
||||
// 僅單元測試會設 AI_REVIEW_SKIP_MAIN=1 略過自動執行,改為手動呼叫 export 的 main() 注入 mock 測試。
|
||||
if (!process.env.AI_REVIEW_SKIP_MAIN) {
|
||||
main().catch(e => {
|
||||
error(`Runner failed: ${e.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"test": "node --test test/*.test.js"
|
||||
"test": "node --experimental-test-module-mocks --test test/*.test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.7",
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
// main() 是整個 action 的流程總管(Step1~Step11),本測試用 node:test 的 module mock
|
||||
// 把所有相依模組換成可控 stub,逐一驗證關鍵分支的 exit code 與退出時機。
|
||||
// 需要 `--experimental-test-module-mocks`(見 package.json test script)。
|
||||
|
||||
// 略過 main.js 被 import 時的自動執行;本測試改為手動呼叫 export 的 main()。
|
||||
process.env.AI_REVIEW_SKIP_MAIN = '1';
|
||||
|
||||
const U = (rel) => new URL(rel, import.meta.url).href;
|
||||
|
||||
// 每個相依模組的預設 stub(快樂路徑:無 critical、diff 非空、角色分析成功)。
|
||||
// 各測試以 overrides 覆寫要驗證的分支。
|
||||
function baseStubs() {
|
||||
return {
|
||||
config: {
|
||||
GITEA_REPOSITORY: 'owner/repo', PR_NUMBER: '1', PR_HEAD_BRANCH: 'feat', PR_BASE_BRANCH: 'develop',
|
||||
FINDINGS_PATH: '.gitea/ai-review/findings.json', EXCLUSIONS_PATH: '.gitea/ai-review/exclusions.json',
|
||||
getLLMConfig: () => ({ provider: 'codex', apiKeys: ['codex'], baseURL: null, model: 'gpt-5.5', command: 'codex' }),
|
||||
},
|
||||
roles: { loadRoles: () => [{ name: 'Mage' }], getRoleIntro: () => 'intro' },
|
||||
gitea: {
|
||||
getPRDiff: async () => 'diff --git a/x b/x\n+code',
|
||||
postComment: async () => ({ id: 1 }),
|
||||
getCommitMessageBySha: async () => 'normal commit',
|
||||
getBotReviewOutcome: () => null,
|
||||
shouldSkipBotCommit: async () => false,
|
||||
},
|
||||
findings: {
|
||||
analyzeWithRole: async () => [],
|
||||
loadOldFindings: () => [],
|
||||
mergeFindings: (a, b) => [...a, ...b],
|
||||
sortByLevel: (a) => a,
|
||||
deduplicateWithAI: async (a) => a,
|
||||
loadExclusions: () => [],
|
||||
applyExclusions: (a) => a,
|
||||
filterFalsePositivesWithAI: async (a) => a,
|
||||
appendExclusions: () => null,
|
||||
resolveMissingLineNumbers: async (a) => a,
|
||||
},
|
||||
resolve: {
|
||||
reconcileConversations: async () => ({ resolvedFindings: [], excludedFindings: [], carriedFindings: [], resolvedCount: 0, falsePositiveCount: 0, openCount: 0, closedCount: 0 }),
|
||||
dropResolvedFindings: (a) => a,
|
||||
addCarriedFindings: (a) => a,
|
||||
},
|
||||
comments: { saveFindings: () => {}, postFindingsReview: async () => {}, formatFindingsStatsLine: () => '' },
|
||||
usage: { getRunUsage: () => ({}), getRateLimit: () => ({}), fetchAccountQuota: async () => ({}), formatUsageStats: () => '', formatUsageStatsLine: () => '' },
|
||||
git: { cloneRepo: () => '/repo', commitAndPush: async () => {}, getRepoState: () => ({ branch: 'feat', shortSha: 'abc' }) },
|
||||
json: { validateJSONArrayFile: async () => ({ exists: true }), ensureJSONArrayFileExists: () => {} },
|
||||
preflight: { runPreflight: async () => true },
|
||||
llm: { mapWithConcurrency: async (items, _l, fn) => Promise.all(items.map(fn)), LLM_CONCURRENCY: 0 },
|
||||
log: { section() {}, step() {}, line() {}, input() {}, output() {}, result() {}, warn() {}, error() {} },
|
||||
};
|
||||
}
|
||||
|
||||
// 套用 mock 並載入一份全新的 main(以 query 破快取),回傳 main()。
|
||||
let importSeq = 0;
|
||||
async function loadMain(overrides = {}) {
|
||||
const s = baseStubs();
|
||||
for (const k of Object.keys(overrides)) s[k] = { ...s[k], ...overrides[k] };
|
||||
mock.module(U('../config.js'), { namedExports: s.config });
|
||||
mock.module(U('../roles.js'), { namedExports: s.roles });
|
||||
mock.module(U('../gitea.js'), { namedExports: s.gitea });
|
||||
mock.module(U('../findings.js'), { namedExports: s.findings });
|
||||
mock.module(U('../resolve.js'), { namedExports: s.resolve });
|
||||
mock.module(U('../comments.js'), { namedExports: s.comments });
|
||||
mock.module(U('../usage.js'), { namedExports: s.usage });
|
||||
mock.module(U('../git.js'), { namedExports: s.git });
|
||||
mock.module(U('../json.js'), { namedExports: s.json });
|
||||
mock.module(U('../preflight.js'), { namedExports: s.preflight });
|
||||
mock.module(U('../llm.js'), { namedExports: s.llm });
|
||||
mock.module(U('../log.js'), { namedExports: s.log });
|
||||
const mod = await import(`../main.js?seq=${importSeq++}`);
|
||||
return mod.main;
|
||||
}
|
||||
|
||||
// 執行 main(),攔截 process.exit 並回傳 exit code(正常結束回 0)。
|
||||
async function runMain(overrides) {
|
||||
const main = await loadMain(overrides);
|
||||
mock.method(process, 'exit', (code) => { throw Object.assign(new Error('__exit__'), { __code: code ?? 0 }); });
|
||||
try {
|
||||
await main();
|
||||
return 0;
|
||||
} catch (e) {
|
||||
if (e && e.__code !== undefined) return e.__code;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
describe('main pipeline', () => {
|
||||
beforeEach(() => { process.env.PR_HEAD_SHA = 'deadbeef'; });
|
||||
afterEach(() => { mock.restoreAll(); delete process.env.PR_HEAD_SHA; });
|
||||
|
||||
it('前置驗證失敗 → exit 1', async () => {
|
||||
assert.equal(await runMain({ preflight: { runPreflight: async () => false } }), 1);
|
||||
});
|
||||
|
||||
it('偵測到 [ai-review-bot][failure] → exit 1', async () => {
|
||||
assert.equal(await runMain({
|
||||
gitea: { getCommitMessageBySha: async () => 'chore: update [ai-review-bot][failure]', getBotReviewOutcome: () => 'failure' },
|
||||
}), 1);
|
||||
});
|
||||
|
||||
it('本次為 bot 自動提交 → exit 0(跳過審查)', async () => {
|
||||
assert.equal(await runMain({ gitea: { shouldSkipBotCommit: async () => true } }), 0);
|
||||
});
|
||||
|
||||
it('未偵測到 LLM provider → exit 1', async () => {
|
||||
assert.equal(await runMain({ config: { getLLMConfig: () => ({ provider: null, apiKeys: [], baseURL: null, model: null, command: null }) } }), 1);
|
||||
});
|
||||
|
||||
it('diff 為空 → exit 0', async () => {
|
||||
assert.equal(await runMain({ gitea: { getPRDiff: async () => ' ' } }), 0);
|
||||
});
|
||||
|
||||
it('所有角色分析皆失敗 → exit 1', async () => {
|
||||
assert.equal(await runMain({ findings: { analyzeWithRole: async () => { throw new Error('boom'); } } }), 1);
|
||||
});
|
||||
|
||||
it('JSON 格式驗證失敗 → exit 1', async () => {
|
||||
assert.equal(await runMain({ json: { validateJSONArrayFile: async () => { throw new Error('bad json'); } } }), 1);
|
||||
});
|
||||
|
||||
it('產生 critical finding → exit 1', async () => {
|
||||
const crit = [{ level: 'critical', role: 'Mage', location: 'a.js:1', suggestion: 'fix' }];
|
||||
assert.equal(await runMain({ findings: { analyzeWithRole: async () => crit, filterFalsePositivesWithAI: async () => crit } }), 1);
|
||||
});
|
||||
|
||||
it('無 critical → 正常走完(exit 0)', async () => {
|
||||
assert.equal(await runMain(), 0);
|
||||
});
|
||||
|
||||
it('clone 失敗仍繼續、不因 commitAndPush 中斷(無 critical → exit 0)', async () => {
|
||||
assert.equal(await runMain({ git: { cloneRepo: () => { throw new Error('clone fail'); } } }), 0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user