2 Commits
17 changed files with 331 additions and 1963 deletions
-948
View File
@@ -1,948 +0,0 @@
[
{
"addedAt": "2026/07/20 13:53:11",
"prNumber": 6,
"reviewer": "Mage",
"severity": "警告",
"file": "src/index.js",
"startLine": 205,
"endLine": 215,
"problem": "建立 issue 與寫入暫存留言不是可重入操作。最小重現:`createIssue` 成功後,第一則 `createCommentOnIssue` 因暫時性 500 或網路中斷而失敗;主流程退出,但已建立的 issue 不會被記錄或回貼 PR。workflow 重跑時 `issue` 又從 null 開始,因此會再建立一個內容相同的 issue,留下孤兒與重複追蹤項目。後續嚴重問題留言或 PR 回貼失敗也有相同結果。",
"reason": "Paladin:可排除(重複)。與 F004 指涉同一段 issue 建立及暫存留言沖刷流程,也描述相同的中途失敗後重跑會建立重複 issue 問題。"
},
{
"addedAt": "2026/07/20 15:03:30",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 205,
"endLine": 218,
"problem": "`ensureIssueCreated` 把「建立 issue」與「逐筆沖刷暫存留言」綁成一個不可恢復的流程。若 issue 已成功建立,但其中一則留言失敗,主流程會中止;下次重跑又會建立另一張 issue,留下重複且內容不完整的追蹤單。半年後維護者也很難從現有狀態判斷應重試留言、沿用既有 issue,還是重新建立。",
"reason": "Paladin:可排除(命中已知排除事項)。其指涉的正是 issue 建立成功、留言沖刷失敗後重跑會產生重複追蹤單的同一問題。"
},
{
"addedAt": "2026/07/20 15:26:21",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 179,
"endLine": 234,
"problem": "`main()` 新增了留言路由、issue 狀態、緩衝佇列、issue 建立與緩衝留言沖刷等職責,後續流程又多次以 `ctx.createIssue` 分支決定發布方式。這使主流程同時負責審查編排與發布狀態機;六個月後若再增加發布管道或重試策略,模式判斷會繼續散落,難以獨立測試各種狀態轉換。",
"reason": "Paladin:可排除(重複)。與 F006 指涉同一段 main() 內的留言路由、issue 狀態及緩衝佇列職責,亦提出相同的發布器抽離方向。"
},
{
"addedAt": "2026/07/20 15:26:21",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 213,
"endLine": 233,
"problem": "`ensureIssueCreated` 在建立 issue 後逐則寫入 `issueBuffer`,但整段沒有可恢復或冪等機制。若寫到一半 API 失敗,主流程會中止;重新執行時無法辨識已建立但內容不完整的 issue,因而可能再建立一張重複 issue。這類部分完成狀態日後會很難人工清理與追查。",
"reason": "Paladin:可排除(命中已知排除事項且與歷史 finding 重複)。其描述的 issue 建立後沖刷留言失敗、重跑產生重複 issue,正是既有排除事項與歷史 finding 已記錄的問題。"
},
{
"addedAt": "2026/07/20 15:26:21",
"prNumber": 6,
"reviewer": "Leo",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 171,
"endLine": 194,
"problem": "新增並匯出的 `addLabelsToIssue` 沒有被本次流程使用,註解也明確表示主流程已不再需要它。保留這個預想中的通用 API 會擴大公開介面與測試範圍;未來 Gitea API 行為改變時,維護者仍得判斷這個無使用者的函式是否需要同步修改。",
"reason": "Paladin:可排除(重複)。與 F007 指涉相同的未使用 addLabelsToIssue 函式及匯出,問題與移除建議均相同。"
},
{
"addedAt": "2026/07/20 15:26:21",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 331,
"endLine": 331,
"problem": "建問題模式在此直接建立新的追蹤 issue,卻沒有任何可重入或去重機制。最小重現:issue 建立成功後,寫入 `issueBuffer`、發布 finding、回貼 PR 連結或最後 push 任一步驟失敗,整個 Action 會以失敗結束;重新執行同一個 commit 時仍會再次呼叫 `ensureIssueCreated`,因此產生內容相同的重複 issue。失敗若持續發生,每次重跑都會再新增一筆。",
"reason": "Paladin:可排除(命中已知排除事項且與 F009、歷史 finding 重複)。同樣是建立 issue 後任一步驟失敗,重跑無法沿用既有 issue 而重複建立的問題。"
},
{
"addedAt": "2026/07/20 15:26:21",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 190,
"endLine": 238,
"problem": "新增的一般模式/建問題模式留言分流與 `issueBuffer` 清空流程,這次 diff 沒有對應測試。尚未驗證 issue 建立前留言只暫存、建立後依序送出、建立後的新留言直接送往 issue,以及任一 API 寫入失敗時的行為;這是本次建問題模式的核心路徑,沒有測試即無法確認留言不會遺失、重複或誤發到 PR。",
"reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已明確涵蓋建問題模式核心流程、暫存留言依序送出、發布分流及外部 API 狀態切換缺乏測試。"
},
{
"addedAt": "2026/07/20 15:26:21",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 312,
"endLine": 389,
"problem": "建問題模式新增了「無 finding 靜默通過」「有 finding 才建 issue」「標籤失敗降級」「嚴重/其他問題分流」「PR 回貼連結」「相依 API 失敗不阻斷」等多個分支,但本次 diff 沒有測試驗證這些結果。尤其 `kept`、`severe`、`others` 的空集合組合與 API 失敗路徑未經試煉,重構後很容易出現 `issue` 為 null 卻被取用、建立空 issue,或意外在 PR 留言。",
"reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已列出有問題才建立 issue、無問題靜默通過、嚴重與非嚴重問題分流、標籤失敗及 PR 回貼等相同未測試分支。"
},
{
"addedAt": "2026/07/20 15:26:21",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 103,
"endLine": 157,
"problem": "`resolveMergeBase` 新增多階段 fetch/重試狀態機,卻沒有新增測試驗證策略順序與停止條件。未驗證非 shallow、`--unshallow` 成功、各 fetch 失敗、補抓後仍無共同祖先等邊界,可能讓正常 checkout 多做 fetch,或在仍可恢復時提早拋錯。",
"reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已針對 resolveMergeBase 的多階段 fetch、淺層與非淺層分支、降級及最終失敗路徑缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 15:26:21",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 16,
"endLine": 77,
"problem": "新增的失敗診斷與機密遮罩會處理 CI 日誌中的敏感輸出,但本次 diff 沒有測試其邊界與失敗情境。未驗證 debug 預設關閉、大小寫/空白解析、控制字元單行化、各 token 格式遮罩、500 字截斷,以及 `error``stderr``output` 缺漏時仍能穩定回傳摘要。",
"reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已指出 agent 失敗診斷的退出狀態、空輸出、單行化與截斷等邊界缺少測試;本條只是進一步枚舉相同測試缺口。"
},
{
"addedAt": "2026/07/20 15:26:21",
"prNumber": 6,
"reviewer": "Rogue",
"severity": "警告",
"file": "src/index.js",
"startLine": 226,
"endLine": 228,
"problem": "`issueBuffer` 內每則留言以 `await` 串行送出,建立 issue 的等待時間會線性累加為約 `留言數 × API RTT`;若每次往返 200~500 ms,6 則留言便額外卡住約 1.2~3 秒。",
"reason": "Paladin:可排除(誤判)。此處逐筆 await 用來維持暫存留言的 FIFO 發布順序;留言數量有限,所述短暫延遲不足以證明缺陷,而改成並行反而可能破壞順序並增加 API 限流風險。"
},
{
"addedAt": "2026/07/20 15:44:22",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 179,
"endLine": 230,
"problem": "建問題模式的狀態管理、留言路由、issue 建立及緩衝區清空都直接塞進已負責整條審查流程的 `main()`,並透過 `issue`、`issueBuffer` 兩個可變閉包共享狀態。後續若再增加重試、不同發布目的地或補償處理,維護者必須同時追蹤閉包狀態與主流程分支;目前 `ensureIssueCreated` 若在逐則寫入緩衝留言時失敗,issue 已建立但狀態沒有可恢復的進度,重跑也可能再建立重複 issue。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。已知排除事項已涵蓋 main() 中留言路由、issue 狀態與緩衝佇列職責散落;其中重跑產生重複 issue 的部分也與歷史 finding 相同。"
},
{
"addedAt": "2026/07/20 15:44:22",
"prNumber": 6,
"reviewer": "Leo",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 171,
"endLine": 195,
"problem": "新增的 `addLabelsToIssue` 沒有任何呼叫端,註解甚至明確說明主流程已不再使用它。現在就保留這個公開匯出會擴大 Gitea client 的 API 表面,未來維護者仍需替它維護文件、測試與版本相容性,卻沒有實際需求驗證其語意。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。已知排除事項已記錄相同的未使用 addLabelsToIssue 函式、公開匯出及移除建議。"
},
{
"addedAt": "2026/07/20 15:44:22",
"prNumber": 6,
"reviewer": "Mage",
"severity": "警告",
"file": "src/index.js",
"startLine": 216,
"endLine": 225,
"problem": "追蹤 issue 的建立與暫存留言寫入不是可重入操作:`createIssue` 成功後,只要任一 `createCommentOnIssue` 失敗,主流程就直接中止,但已建立的 issue 不會回滾或留下可供重跑辨識的狀態。最小重現為:建立 issue 成功、第一則留言遇到暫時性 5xx;workflow 重跑後會再建立一張相同 issue,產生孤兒或重複追蹤問題。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。issue 建立成功、留言失敗後重跑會建立重複 issue,正是已知排除事項及歷史 finding 已記錄的同一問題。"
},
{
"addedAt": "2026/07/20 15:44:22",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 184,
"endLine": 374,
"problem": "建問題模式新增了大量分支行為,但本次 diff 沒有對應測試驗證:留言先暫存再依序寫入 issue、無保留問題時靜默通過、標籤挑選失敗時降級、嚴重與非嚴重問題分流、PR 回貼連結,以及建立相依失敗時不中斷流程。這些都是可觀察的核心流程;沒有測試時,任何分支回歸都可能造成漏建 issue、留言送錯位置或流程意外失敗。",
"reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已涵蓋建問題模式的暫存留言、靜默通過、問題分流、標籤失敗及 PR 回貼等核心分支缺少測試。"
},
{
"addedAt": "2026/07/20 15:44:22",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 105,
"endLine": 157,
"problem": "`resolveMergeBase` 新增多階段 fetch/重試策略,卻沒有測試驗證最容易出錯的淺層歷史與失敗路徑,包括非 shallow repo、`--unshallow` 成功後立即停止、某次 fetch 失敗後繼續下一策略,以及全部失敗時的 diagnostics 與 `cause`。這類命令編排很容易因呼叫順序或 off-by-one 次重試而產生多餘網路請求,甚至在其實可取得 merge-base 時仍失敗。",
"reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已指出 resolveMergeBase 多階段 fetch 在淺層、非淺層、降級及最終失敗路徑缺少測試。"
},
{
"addedAt": "2026/07/20 15:44:22",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 17,
"endLine": 81,
"problem": "新增的 AI CLI 失敗診斷只在失敗與 debug 模式執行,但沒有測試覆蓋其邊界:`res`/`error` 缺值、逾時與 signal、debug 大小寫及空白、控制字元單行化、500 字截斷,以及多種 token/URL 帳密遮罩。這些失敗路徑若回歸,可能使診斷完全缺失或將未遮罩內容寫入 CI log。",
"reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已記錄 AI CLI 失敗診斷在退出狀態、空輸出、單行化、截斷及相關邊界缺少測試;本條是對同一測試缺口的細項枚舉。"
},
{
"addedAt": "2026/07/20 15:57:46",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 268,
"endLine": 268,
"problem": "舊審查留言仍在本回合結果完整發布前就被標記為過時;建議把清理舊留言移到本輪所有必要結果均成功產生之後,避免失敗重跑留下「舊結果已清除、新結果不完整」的中間狀態。",
"reason": "維護者裁示(議題 #8 留言 #4838,系統管理員):此功能用途為把前一輪 PR 產生的訊息標記為已解決/過時,讓審查人員辨識舊訊息,因此刻意在本回合結果之前執行,不可延後,否則會誤導審查人員。依維護者決定不採納此修改,記為排除以免後續審查重複提出。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 54,
"endLine": 75,
"problem": "開啟 `ACTIONS_STEP_DEBUG=true` 後會把 AI CLI 的原始 stderr/stdout 寫入永久 CI log,但 `redactSecrets` 是可繞過的黑名單,也完全沒有移除 PII。攻擊者可透過惡意 diff/提示注入,要求 CLI 以未涵蓋的格式輸出憑證、電子郵件或其他敏感資料;例如短版金鑰、含標點的 token、JWT 或拆段輸出都可能避開現有正規表示式,造成機密與 PII 外洩。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 debug 模式輸出 AI CLI stderr/stdout、黑名單遮罩可繞過及敏感內容外洩提出相同問題。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 220,
"endLine": 231,
"problem": "`ensureIssueCreated` 之名暗示「若尚未建立才建立」的冪等保證,實作卻會無條件建立新 issue。即使目前只呼叫一次,命名仍會誤導後續維護者,長註解也無法替代準確的動詞。",
"reason": "Paladin:可排除(重複)。歷史 finding 已明確指出 `ensureIssueCreated` 名稱暗示冪等沿用,實際卻建立新 issue 並修改外層狀態。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "action.yml",
"startLine": 45,
"endLine": 57,
"problem": "新增的 `push-token` 以區塊註解、欄位描述、選填註解與預設值註解反覆敘述「PAT 觸發 CI、留空退回 token」;同一旋律重奏過多,真正重要的使用方式反而埋在十餘行說明裡,且明顯比鄰近 input 冗長。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 `push-token` 在註解、description、required 與 default 說明中重複敘述相同問題。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 234,
"problem": "`main()` 新增 `issueBuffer`、可變的 `issue`、`postComment` 與 `ensureIssueCreated`,把「決定留言目的地、建立 issue、暫存與沖刷留言」等多項職責綁在閉包狀態中。後續流程又必須知道 issue 是否已建立並直接讀取 `issue.number`;半年後若增加留言種類、重試或其他發布目的地,維護者得同時修改多個分支,也很難隔離單元測試發布狀態轉換。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。已知排除事項與歷史 finding 均已涵蓋 `main()` 內留言路由、issue 狀態、緩衝佇列及發布職責耦合的問題。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 221,
"endLine": 234,
"problem": "`ensureIssueCreated` 在所有暫存留言成功寫入前,就先把閉包中的 `issue` 設為已建立;沖刷途中任何一則 API 呼叫失敗,都會留下只寫入部分內容的 issue,且主流程沒有保存進度或可重入機制。重新執行時又會建立另一個 issue。這種部分完成狀態日後很難追查,也會讓重試與錯誤處理持續累積例外分支。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。issue 建立後沖刷留言失敗、留下部分完成狀態並在重跑時建立重複 issue,正是既有排除事項與歷史 finding 記錄的同一問題。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 342,
"endLine": 344,
"problem": "一般模式在發布新的嚴重/警告留言前,就先把舊審查留言標為過時。最小重現:`resolveOldComments` 成功後,步驟 9 的 `createReview` 或步驟 10 的留言 API 暫時失敗;本次 action 以失敗結束,但上一回合完整結果已被清除,新結果也未完整發布,PR 會失去可信的審查結果。這仍違反本次改動宣稱的「沒有新結果時保留舊結果」語義。",
"reason": "Paladin:可排除(命中已知排除事項)。維護者已明確裁示舊留言必須刻意在本回合結果之前標記為過時,以免舊資訊繼續誤導審查人員。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Mage",
"severity": "警告",
"file": "src/index.js",
"startLine": 224,
"endLine": 233,
"problem": "追蹤 issue 的建立與暫存留言寫入不是可重入操作。最小重現:`createIssue` 成功,但寫入 `issueBuffer` 第 2 則留言時 API 失敗;action 結束前沒有在 PR 留下連結或保存已建立的 issue 編號。工作流程重跑時會再建立一個新 issue,留下孤立且內容不完整的舊 issue,並可能持續產生重複 issue。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。建立 issue 後留言寫入失敗、未保存可恢復狀態而使重跑建立重複 issue,已由多筆既有排除事項及歷史 finding 完整涵蓋。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 311,
"endLine": 386,
"problem": "建問題模式的核心流程已大幅改寫,但本次 diff 沒有對應測試驗證:有保留問題時才建立 issue、標籤失敗時降級、暫存留言依序送出、嚴重與其他問題分流、PR 回貼連結,以及相依 API 失敗不阻斷流程。這些分支牽涉多次外部呼叫與狀態變化,未經測試容易出現 issue 未建立卻存取 `issue.number`、留言遺失或呼叫順序錯誤。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋建問題模式的 issue 建立條件、標籤降級、暫存留言、問題分流、PR 回貼及相依 API 失敗等核心分支缺少測試。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 103,
"endLine": 151,
"problem": "`resolveMergeBase` 新增多階段 fetch 復原與錯誤診斷,但沒有測試覆蓋淺層與失敗路徑。尚未驗證 unshallow 成功後會立即停止、fetch 失敗會繼續下一策略、非淺層 repo 不會執行 unshallow,以及所有策略失敗時的錯誤與 `cause` 是否正確;這類 git 邊界通常只會在 CI 的 shallow checkout 才暴露。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 `resolveMergeBase` 的淺層與非淺層分支、多階段 fetch、停止條件及最終錯誤資訊缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 270,
"endLine": 313,
"problem": "新增的 PAT 推送與憑證注入路徑沒有對應測試。尚未驗證有 `pushToken` 時必須略過 origin、無 `pushToken` 時只有 origin 失敗才使用一般 token、Authorization 僅透過環境變數傳入,以及推送失敗時不會把 token、遠端 URL或原始命令帶入錯誤。這些未驗證行為同時影響 CI 是否重觸發與失敗路徑的機密保護。",
"reason": "Paladin:可排除(重複)。歷史 finding 已明確記錄 pushToken、origin 與一般 token 的兩套推送策略、認證資訊遮蔽及失敗路徑缺少測試。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 16,
"endLine": 80,
"problem": "新增的失敗診斷與機密遮罩會處理不可信的 stderr/stdout,但沒有測試驗證邊界與失敗格式。特別是換行控制字元、Authorization、URL 帳密、各種 token 格式、超長輸出截斷,以及 `ACTIONS_STEP_DEBUG` 開關;任何漏測都可能造成除錯模式洩漏機密或產生可偽造的多行 CI 日誌。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 `redactSecrets``agentFailureDetail` 對控制字元、各類憑證、URL 帳密、截斷及 debug 開關等邊界缺少測試。"
},
{
"addedAt": "2026/07/20 16:03:44",
"prNumber": 6,
"reviewer": "Rogue",
"severity": "警告",
"file": "src/index.js",
"startLine": 224,
"endLine": 226,
"problem": "`issueBuffer` 內每則留言都逐一 `await` 遠端 API,總耗時為 O(n × API RTT)。目前固定情境留言也會累加多次網路往返;若日後增加階段,等待時間會線性成長。",
"reason": "Paladin:可排除(命中已知排除事項)。逐筆 `await` 是為維持暫存留言的 FIFO 發布順序,留言數量有限;現有證據不足以證明延遲構成缺陷,而並行發送會增加順序不確定及 API 限流風險。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 73,
"endLine": 74,
"problem": "攻擊者可在 PR diff 中植入不符合現有正規表示式的機密或 PII,再誘使 AI CLI 失敗並將提示內容回顯至 stderr;此處預設把前 500 字寫入 CI log。`redactSecrets` 僅是樣式黑名單,無法遮蔽電子郵件、短 token、含特殊符號的憑證或原始碼敏感資料,會形成可長期讀取的資料外洩通道。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對除錯模式輸出 AI CLI stderr/stdout、黑名單式遮罩可繞過,以及敏感內容或 PII 外洩提出相同問題。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 179,
"endLine": 232,
"problem": "`main()` 新增了 `issueBuffer`、可變的 `issue`,以及兩個捕捉外部狀態的區域函式,使原本已相當冗長的流程編排同時承擔留言路由、緩衝、issue 建立與緩衝區清空等細節。尤其 `ensureIssueCreated` 這個名稱暗示可重複安全呼叫,實作卻會無條件建立新 issue,名稱與行為並不押韻。",
"reason": "Paladin:可排除(重複)。歷史 findings 已分別涵蓋 main() 承擔留言路由與 issue 狀態管理,以及 ensureIssueCreated 名稱暗示冪等、實際卻建立新 issue 的問題。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "action.yml",
"startLine": 47,
"endLine": 58,
"problem": "`push-token` 周圍的註解重複敘述 PAT、CI 再觸發、步驟 1 快速回報與空值退回行為,且 `description` 又把同一段旋律再奏一次。Action manifest 因實作細節過密而難以快速掃讀,也與其他 input 較精簡的註解密度不一致。",
"reason": "Paladin:可排除(重複)。歷史 finding 與已知排除事項已記錄 push-token 在註解、description、required 與 default 說明中重複敘述相同資訊。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 181,
"endLine": 231,
"problem": "`main()` 新增以 `issueBuffer`、`issue` 及兩個閉包管理追蹤 issue,後續又在主流程內負責挑標籤、建立 issue、排空留言、發布各類問題、回貼 PR 與設定相依關係。建問題模式的狀態與發布規則因此散落於整個超長函式;未來新增留言種類或調整建立時機時,維護者必須同步追蹤多處 `ctx.createIssue` 分支與可變閉包狀態,容易漏改,也難以獨立單元測試部分失敗及重試行為。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 生命週期、緩衝佇列及發布職責耦合,並提出相同的發布器抽離方向。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 345,
"endLine": 347,
"problem": "舊留言在新問題留言真正發布成功前就被標記為過時。最小重現:`resolveOldComments` 成功後,步驟 9 的 `postSevereComments` 或步驟 10 的 `postComment` 因暫時性 API 錯誤失敗;本次流程以例外結束,但舊審查結果已被清除,PR 上只剩工具/角色等情境留言,沒有任何有效 finding。這仍違反此次延後清理所宣稱的「成功產生結果後才執行」語義。",
"reason": "Paladin:可排除(命中已知排除事項)。維護者已明確裁示舊留言應刻意在本回合結果發布前標記為過時,以免舊資訊持續誤導審查人員。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Mage",
"severity": "警告",
"file": "src/index.js",
"startLine": 219,
"endLine": 229,
"problem": "建立追蹤 issue 與寫入暫存留言不是可恢復的原子流程。最小重現:`createIssue` 成功,但寫入第 2 則 `issueBuffer` 留言時 API 失敗;流程中止後已留下內容不完整且沒有 PR 回鏈的 issue,下次重跑又會建立另一個 issue,造成重複追蹤與狀態分歧。後續 PR 回鏈、相依設定或 commit/push 失敗也有相同的重跑問題。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。建立 issue 後留言或後續步驟失敗、重跑無法沿用既有 issue 而產生孤兒或重複 issue,已由多筆既有紀錄完整涵蓋。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 105,
"endLine": 157,
"problem": "`resolveMergeBase` 新增多階段 fetchmerge-base 復原流程,但本次變更沒有對應測試。淺層與非淺層儲存庫、fetch 失敗後繼續、任一策略成功即停止,以及全部失敗時的診斷與 `cause` 都是新增行為;沒有驗證就可能在 runner 的 shallow checkout 上才暴露錯誤或多餘 fetch。",
"reason": "Paladin:可排除(重複)。歷史 finding 已記錄 resolveMergeBase 多階段 fetch 在淺層、非淺層、策略停止、降級與最終失敗診斷等路徑缺少測試。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 247,
"endLine": 311,
"problem": "新增 `pushToken` 與 `pushWithCredential` 後,推送分支及失敗路徑未見測試覆蓋。尚未驗證有 PAT 時確實略過 origin、無 PAT 時先推 origin 再以 `token` 降級、認證只進環境變數,以及失敗錯誤不帶 token/遠端 URL;這些都是本次新增且直接影響結果 commit 能否推送的行為。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 pushToken、origin 與一般 token 的推送策略、呼叫順序、認證遮蔽及失敗路徑缺少測試。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 309,
"endLine": 386,
"problem": "建問題模式改成「有保留問題才建 issue」、緩衝留言、逐類發布、回貼 PR 與設定相依關係,但本次沒有流程測試驗證這組新增分支。尤其 `kept` 為空、只有嚴重問題、只有警告/建議、標籤挑選失敗,以及相依 API 失敗降級等路徑,若接線錯誤可能造成漏留言、對 `null` issue 取值,或非預期中斷審查。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的 issue 建立條件、暫存留言、問題分流、標籤降級、PR 回貼及相依 API 失敗等核心分支缺少測試。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 16,
"endLine": 85,
"problem": "新增的失敗診斷與機密遮罩沒有對應測試,尚未驗證空值、控制字元、多種 token 格式、URL 內嵌帳密、5002000 字截斷邊界,以及 `ACTIONS_STEP_DEBUG` 開關。這段程式會處理失敗路徑中的非可信 CLI 輸出;若正規表示式或截斷順序退化,測試無法及時發現機密殘留或假日誌換行。",
"reason": "Paladin:可排除(重複)。歷史 finding 已逐項記錄失敗診斷與遮罩對空值、控制字元、各類憑證、URL 帳密、截斷及 ACTIONS_STEP_DEBUG 開關缺少測試。"
},
{
"addedAt": "2026/07/20 16:20:39",
"prNumber": 6,
"reviewer": "Maya",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 171,
"endLine": 181,
"problem": "新 API `addIssueDependency` 未見測試驗證 endpoint 與 IssueMeta payload。`issueNumber` 與 `dependency` 的方向一旦顛倒,請求仍可能成功,卻會建立相反的阻擋關係;只靠主流程成功測試不容易察覺。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向的測試,與本條指控相同。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 74,
"endLine": 77,
"problem": "攻擊者可在 PR diff 中植入機密或個資,誘使 AI CLI 在失敗時原樣回顯;此處卻預設把 stderr 與 stdout 寫入長期保存的 CI log。`redactSecrets()` 只是可繞過的黑名單,例如 `Authorization: Bearer <憑證>` 只會遮掉 `Bearer`,後方憑證仍會留下,且電子郵件、姓名、短 token 與未知格式完全不會被遮罩,違反回應不得含 PII 的規範。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出除錯模式輸出 AI CLI stderr/stdout、黑名單式遮罩可繞過,以及機密或個資外洩的相同問題。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 183,
"endLine": 229,
"problem": "新增的 `postComment`、`ensureIssueCreated`、`issueBuffer` 與可變的 `issue` 全部嵌在本就冗長的 `main()` 中,且第二個函式透過閉包同時讀寫多個外部狀態。讀者要在主流程、留言路由與 issue 生命週期三條旋律間來回切換,主流程輪廓因此被大量細節淹沒。",
"reason": "Paladin:可排除(重複)。與 F005 及歷史 finding 指涉相同的 main() 職責膨脹、閉包狀態與 issue 發布流程耦合問題。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 181,
"endLine": 229,
"problem": "`main()` 新增了 `issueBuffer`、`issue`、`postComment` 與 `ensureIssueCreated` 等可變狀態及閉包,並在後續流程散落多個 `ctx.createIssue` 分支。留言路由、issue 建立、緩衝區清空與主審查編排因此緊密耦合;半年後新增第三種發布方式或調整 issue 建立時機時,維護者必須同時追蹤整個 `main()` 的狀態轉移,也很難獨立單元測試發布行為。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列與發布職責耦合的問題。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 344,
"endLine": 382,
"problem": "一般模式在嚴重問題與彙整留言發布前就呼叫 `resolveOldComments`。若後續 `postSevereComments` 或 `postComment` 因 API 暫時失敗而中斷,舊的完整審查結果已被標成過時,新回合卻只留下工具、diff、角色等情境留言。這與本次變更宣稱的「成功產生結果後才清舊留言」保證不一致,也讓未來維護者難以判斷何時才算發布完成。",
"reason": "Paladin:可排除(命中已知排除事項)。維護者已明確裁示舊留言須刻意在本回合結果發布前標記為過時,以免舊資訊繼續誤導審查人員。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 213,
"endLine": 225,
"problem": "建立追蹤 issue 與寫入暫存留言不是原子操作。最小重現:`gitea.createIssue` 成功後,第 2 則 `createCommentOnIssue` 因暫時性 API 錯誤失敗;主流程會中止,但遠端已留下內容不完整的 issue。工作重跑時沒有查找或續寫既有 issue 的機制,因此會再建立一張重複 issue,且先前已成功寫入的留言也可能重複。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。建立 issue 後沖刷暫存留言失敗、重跑產生孤兒或重複 issue,已由多筆既有紀錄完整涵蓋。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 342,
"endLine": 344,
"problem": "舊審查留言仍在新結果完整發布前被標記為過時。最小重現:防守方裁決與 findings 寫檔成功後,`resolveOldComments` 先清除舊結果;接著 `postSevereComments`、`postComment` 或最後的 commit/push 任一步失敗,流程便以失敗結束,PR 上只剩工具/角色等情境留言,既有問題已過時、新問題卻未完整發布。這仍違反本次改動宣稱的「失敗時保留舊結果」語義。",
"reason": "Paladin:可排除(命中已知排除事項且與 F006 重複)。維護者已裁示不得將舊留言清理延後至新結果完整發布之後。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 105,
"endLine": 159,
"problem": "新增的 merge-base 補抓流程沒有對應測試驗證。尚未確認非淺層/淺層 repository、首次 fetch 失敗、unshallow 成功、deepen base 或 HEAD 才成功,以及所有策略失敗時的診斷與 `cause`;這些分支會直接決定送審 diff 的基準,未經試煉仍可能漏審或錯審。",
"reason": "Paladin:可排除(重複)。歷史 finding 已記錄 resolveMergeBase 多階段 fetch 在淺層、非淺層、停止條件、降級與最終失敗診斷等路徑缺少測試。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 267,
"endLine": 308,
"problem": "新增的 `pushToken` 推送分流與 `pushWithCredential` 失敗降級沒有測試覆蓋。尚未驗證有 PAT 時確實略過 origin、無 PAT 時 origin 成功不重試、origin 失敗才使用一般 token,以及認證環境變數與固定錯誤訊息不會帶出 token;這是本次 CI 再觸發機制的核心行為。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 pushToken、origin 與一般 token 的推送策略、呼叫順序、認證遮蔽及失敗路徑缺少測試。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 309,
"endLine": 387,
"problem": "建問題模式被大幅改寫,但 diff 中沒有測試驗證各分支的外部行為。尤其 `kept` 為空時必須不建 issue、不碰 PR 留言;有問題時必須依序建立 issue、沖出暫存留言、發布各類 finding、回貼 PR 連結;標籤挑選與相依 API 失敗又必須降級而不中斷。這些成功與失敗路徑目前都未被驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的 issue 建立條件、暫存留言、問題分流、標籤降級、PR 回貼及相依 API 失敗等核心分支缺少測試。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 16,
"endLine": 82,
"problem": "新增的失敗診斷與機密遮罩邏輯沒有測試驗證邊界。尚未確認 Authorization、token/password/secret、URL 內嵌帳密、GitHub 樣式 token、長字串、控制字元,以及機密剛好跨越 2,000 字截斷邊界時是否仍會被完整遮蔽;也未驗證 timeout、數字/字串 code、signal 與空輸出的摘要。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋失敗診斷與遮罩對空值、控制字元、各類憑證、URL 帳密、截斷及退出狀態等邊界缺少測試。"
},
{
"addedAt": "2026/07/20 16:39:40",
"prNumber": 6,
"reviewer": "Maya",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 171,
"endLine": 195,
"problem": "新加入的 `addIssueDependency` API 封裝沒有契約測試,尚未驗證 URL 中使用相依方 PR 編號,而 request body 的 `index` 使用阻擋來源 issue 編號;兩者若對調,請求可能仍是合法格式,卻會建立反向的相依關係。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向的契約測試。"
},
{
"addedAt": "2026/07/20 17:21:16",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 70,
"endLine": 73,
"problem": "攻擊者可以把惡意內容塞進 PR diff 或 AI 工具輸出,誘使 AI CLI 在失敗時把環境變數、原始碼片段、token、PR 內容或 PII 印到 stdout/stderr;這裡預設把 stdout/stderr 前 500 字寫進 CI log。`redactSecrets` 只是黑名單式遮罩,擋不住 Gitea PAT、JWT、雲端金鑰、短 token、帶標點的密碼或一般 PII。尤其本 PR 又新增 `push-token` PAT,失敗診斷變成一條可被 prompt injection 利用的外洩通道。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 debug/失敗診斷輸出 AI CLI stdout/stderr、黑名單式遮罩可繞過,以及機密或個資外洩提出相同問題。"
},
{
"addedAt": "2026/07/20 17:21:16",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 365,
"problem": "`main()` 這次把「留言目的地切換、issue 暫存與 flush、標籤挑選、PR 回貼、issue dependency、舊留言清理時機」全部塞進同一段流程。半年後要改建問題模式時,維護者必須同時理解 `ctx.createIssue`、`issue`、`issueBuffer`、`currentRunCommentIds` 這幾個閉包狀態的互動,任何一個留言新增在錯誤位置,都可能變成被暫存後丟棄、發錯地方,或被舊留言清理誤處理。這不是單純長度問題,而是 orchestration 和 delivery policy 已經耦合在一起。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列與發布職責耦合的問題;本條只是擴充描述同一設計負擔。"
},
{
"addedAt": "2026/07/20 17:21:16",
"prNumber": 6,
"reviewer": "Leo",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 30,
"endLine": 39,
"problem": "`agentFailureDetail()` 的說明文字前後不一致:開頭說「原始輸出預設隱藏」,下一段又說失敗時預設附上遮罩後的 stderr/stdout 片段。這種註解矛盾會讓未來維護者不確定目前政策到底是偏向保守隱藏,還是偏向可診斷輸出,尤其這段又是多個 AI CLI 失敗路徑共用的行為。",
"reason": "Paladin:可排除(列表內重複)。與 F002 指涉同一段 agentFailureDetail 註解前後政策描述不一致的問題。"
},
{
"addedAt": "2026/07/20 17:21:16",
"prNumber": 6,
"reviewer": "Mage",
"severity": "警告",
"file": "src/index.js",
"startLine": 213,
"endLine": 224,
"problem": "`ensureIssueCreated` 先建立追蹤 issue,再逐則寫入 `issueBuffer`。只要 issue 建立成功後任一 buffered comment 發生暫時性 API 失敗,例外會往上拋出,action 以失敗結束,但已建立的 issue 不會回滾,也沒有被記錄成可重用狀態。\n\n最小重現情境:`create-issue=true`、有保留問題;`gitea.createIssue` 成功建立 #10;第一則 `createCommentOnIssue` timeout。這次 run 失敗;下次重跑又建立 #11,造成重複追蹤 issue,且 PR 可能沒有任何連結指向第一次建立的 issue。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。issue 建立成功後 buffered comment 失敗、重跑產生重複追蹤 issue,正是既有排除事項與歷史 finding 已裁定涵蓋的同一問題。"
},
{
"addedAt": "2026/07/20 17:21:16",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 309,
"endLine": 381,
"problem": "建問題模式這次改了主要行為:有保留問題時才建 issue、先暫存工具/diff/角色留言、之後逐條留言到 issue、再回貼 PR 連結並建立 dependency;但 diff 沒看到對應測試。這條路徑牽涉多個外部 API 呼叫與順序,沒有測試就無法驗證「PR 只留連結、issue 收到完整內容、無保留問題時靜默通過」這些新契約。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式有 finding 才建 issue、暫存留言 flush、PR 只回貼連結、dependency 失敗降級等核心分支缺少測試。"
},
{
"addedAt": "2026/07/20 17:21:16",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 102,
"endLine": 146,
"problem": "`resolveMergeBase` 新增了多段 fetch fallback(明確更新 base、淺層時 unshallow、deepen base、deepen HEAD)以及失敗診斷,但沒有看到測試覆蓋這些邊界與失敗路徑。這段是 diff 基準來源;只要淺層 checkout 或 fetch 策略順序沒被驗證,審查範圍可能整個錯掉。",
"reason": "Paladin:可排除(重複)。歷史 findings 已指出 resolveMergeBase 多階段 fetch、淺層與非淺層路徑、停止條件及最終失敗診斷缺少測試。"
},
{
"addedAt": "2026/07/20 17:21:16",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 267,
"endLine": 324,
"problem": "`push-token` 與 `pushWithCredential` 是新的提交推送行為,但 diff 沒有對應測試驗證分支選擇與失敗處理。尤其 `pushToken` 存在時會跳過 origin、未提供時才 fallback,這些都是會影響 CI 是否重新觸發的關鍵行為。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushToken、origin fallback、認證傳遞與推送失敗遮蔽等分支缺少測試。"
},
{
"addedAt": "2026/07/20 17:21:16",
"prNumber": 6,
"reviewer": "Maya",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 72,
"problem": "`redactSecrets` / `agentFailureDetail` 新增了輸出 stderr/stdout 診斷的行為,但沒有測試驗證遮罩規則與限長。這不是單純格式調整;一旦正規式或順序被改壞,失敗 log 可能變得不可診斷,或把控制字元與憑證樣式原樣輸出。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 redactSecretsagentFailureDetail 的控制字元、憑證樣式、URL 帳密、截斷與 debug 開關缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 17:27:30",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 74,
"endLine": 77,
"problem": "攻擊者只要讓 AI CLI 失敗,這裡就會把 stderr/stdout 片段寫進 CI log。那些輸出可能包含 prompt、diff 內容、環境診斷、第三方 CLI 錯誤訊息,甚至 token、API key、JWT、email 或其他 PII`redactSecrets` 只是樣式比對,漏掉未列舉格式時,秘密會被永久留在多人可讀的建置紀錄裡。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI stderr/stdout 寫入 CI log、黑名單式遮罩可繞過,以及 token/PII 外洩風險提出相同問題。"
},
{
"addedAt": "2026/07/20 17:27:30",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 206,
"endLine": 217,
"problem": "`ensureIssueCreated` 這個名字唱的是「確保存在」,但實作每次呼叫都會直接建立新 issue;名稱與行為不同拍,日後重用時很容易誤讀。",
"reason": "Paladin:可排除(重複)。歷史 finding 已明確指出 `ensureIssueCreated` 名稱暗示冪等沿用,實作卻會建立新 issue,與本條指控相同。"
},
{
"addedAt": "2026/07/20 17:27:30",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 176,
"endLine": 363,
"problem": "建問題模式把「留言目的地切換、issue 延後建立、標籤挑選、嚴重/非嚴重發布、PR 回貼連結、相依設定」全部塞進 `main()`,讓主流程同時承擔流程編排與發布策略。六個月後要改任何一種輸出模式時,很容易漏改其中一個 `ctx.createIssue` 分支,或讓一般模式與建問題模式的行為逐步分岔。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 `main()` 內留言路由、issue 生命週期、標籤、相依設定與發布職責耦合的問題。"
},
{
"addedAt": "2026/07/20 17:27:30",
"prNumber": 6,
"reviewer": "Leo",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 31,
"endLine": 53,
"problem": "`agentFailureDetail` 的說明提到「需要原始輸出診斷時,於 workflow 設定 secret `ACTIONS_STEP_DEBUG=true` 再重跑」,但實作其實不看 `ACTIONS_STEP_DEBUG`,預設就會輸出遮罩後的 stderr/stdout 片段。註解與行為不一致會讓未來維護者誤判這段診斷的暴露條件。",
"reason": "Paladin:可排除(與 F001 重複)。本條同樣指向 `agentFailureDetail` 預設輸出 stderr/stdout 片段、與 debug 暴露條件不一致的同一行為;F001 已保留其核心風險。"
},
{
"addedAt": "2026/07/20 17:27:30",
"prNumber": 6,
"reviewer": "Mage",
"severity": "警告",
"file": "src/index.js",
"startLine": 219,
"endLine": 228,
"problem": "`ensureIssueCreated` 先建立 issue,再逐則寫入暫存留言;任一留言 API 在中途失敗時,流程會直接丟出例外結束,但已建立的 issue 不會回滾,也沒有可重用的識別。最小情境:issue 建立成功,寫入第 2 則 `issueBuffer` 留言時 Gitea 暫時回 500;本次 action 失敗,下一次重跑會再建立一個新 issue,留下重複且內容不完整的追蹤 issue。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。建立 issue 後沖刷暫存留言失敗、重跑又建立重複 issue,正是既有排除事項與多筆歷史 finding 已涵蓋的問題。"
},
{
"addedAt": "2026/07/20 17:27:30",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 312,
"endLine": 383,
"problem": "建問題模式的核心流程被大幅改寫,但這段新增行為還沒看到對應測試驗證:`kept.length > 0` 才建 issue、標籤挑選失敗要降級、嚴重與非嚴重問題要改發到 issue、最後還要回貼 PR 並嘗試建立 issue dependency。這些都是跨 API 的分支與失敗路徑,沒有測試時很容易只跑到快樂路徑,漏掉「沒有保留問題」「標籤 API 失敗」「dependency API 不支援」這類實際會發生的情境。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的 issue 建立條件、標籤降級、問題分流、PR 回貼與 dependency 失敗路徑缺少測試。"
},
{
"addedAt": "2026/07/20 17:27:30",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 102,
"endLine": 151,
"problem": "`resolveMergeBase` 新增了淺層 checkout 修復策略與多段 fallback,但邊界與失敗路徑還沒被驗證:初次 fetch 成功但 merge-base 失敗、`--unshallow` 成功後立即成功、`deepen base` 成功、`deepen HEAD` 成功,以及所有策略都失敗時錯誤訊息要包含診斷。這段是 review diff 的基準點,沒測到 fallback 行為就等於沒有確認淺層 checkout 的修復真的可用。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 `resolveMergeBase` 多階段 fetch、淺層/非淺層分支、停止條件與最終診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 17:27:30",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 266,
"endLine": 314,
"problem": "push 行為從「先 origin、失敗才帶 token URL 重試」改成一律透過 `pushWithCredential` 用環境變數注入 Basic header,但新增的認證與錯誤遮蔽行為沒有測試驗證。這裡的失敗路徑如果沒測,很容易在 push 失敗時回歸成洩漏 argv/token,或 refspec/remoteUrl 組錯卻直到 CI 才發現。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 `pushToken`、origin 與 credential push 策略、認證遮蔽及 push 失敗路徑缺少測試。"
},
{
"addedAt": "2026/07/20 17:27:30",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 14,
"endLine": 73,
"problem": "`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 log,並依賴 `redactSecrets` 做遮罩與單行化;但這段新增的失敗診斷路徑沒有看到測試覆蓋。這不只是快樂路徑問題,邊界包含空輸出、只有 stdout、逾時 killed、換行控制字元、Authorization/token/URL credentials/長 token 等格式,任何一個漏掉都會讓診斷品質或遮罩行為失真。",
"reason": "Paladin:可排除(重複)。歷史 finding 已記錄 `agentFailureDetail``redactSecrets` 對空輸出、控制字元、憑證格式、URL 帳密、截斷與 debug 開關缺少測試。"
},
{
"addedAt": "2026/07/20 17:32:00",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 52,
"endLine": 69,
"problem": "AI CLI 失敗時會把 stdout/stderr 片段預設寫進 CI log。攻擊者只要讓 CLI 失敗,並讓錯誤輸出夾帶 prompt、diff、環境診斷、原始碼片段或不符合目前 regex 的 token/PII,就能把本應留在審查沙箱內的內容長期落到多人可讀的 workflow log。`redactSecrets` 是黑名單式遮罩,擋不住未知格式憑證、中文個資、內部 URL、客戶資料或模型/CLI 回顯的任意文字。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI 失敗輸出 stdout/stderr、黑名單式遮罩可繞過,以及機密或個資落入 CI log 的風險提出相同問題。"
},
{
"addedAt": "2026/07/20 17:32:00",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 178,
"endLine": 235,
"problem": "`main()` 裡新加入的兩個閉包與大段 JSDoc 讓主流程開場變得過於厚重。這些說明本身有價值,但它們插在流程步驟之前,讓讀者要先穿過一整段留言派送與建 issue 細節,才聽見真正的 10 步驟主旋律。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 main() 承擔留言路由、issue 狀態、緩衝佇列與發布職責耦合,並提出抽離 helper/發布器的方向。"
},
{
"addedAt": "2026/07/20 17:32:00",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 177,
"endLine": 378,
"problem": "`main()` 這次把「一般模式」與「建問題模式」的留言路由、issue 建立、暫存 buffer、標籤挑選、相依關係、舊留言清理全部塞進同一個流程函式。六個月後要改任一個發佈規則時,維護者必須同時理解 `ctx.createIssue`、`issue` 閉包狀態、`issueBuffer`、`currentRunCommentIds` 與步驟順序,控制流已經變成隱含狀態機,測試也很難只針對「留言目的地」或「issue 收束」單獨驗證。",
"reason": "Paladin:可排除(重複)。此條與歷史 Leo finding 同樣指向 main() 內一般模式/建問題模式發布規則、issueBuffer、issue 閉包狀態與 ctx.createIssue 分支耦合。"
},
{
"addedAt": "2026/07/20 17:32:00",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 177,
"endLine": 390,
"problem": "建問題模式這次新增了大量可觀察行為,但 diff 沒看到對應測試:留言先暫存再寫入 issue、無保留問題時靜默通過、有嚴重問題才加 PR 相依、只有警告/建議時不阻擋合併,以及一般模式才延後 resolve 舊留言。這些都是很容易在分支條件中漏掉的流程行為,現在還沒有被試煉過。",
"reason": "Paladin:可排除(重複)。歷史 Maya findings 已涵蓋建問題模式的暫存留言、靜默通過、嚴重/非嚴重分流、PR 相依與一般模式舊留言清理等核心分支缺少測試。"
},
{
"addedAt": "2026/07/20 17:32:00",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 102,
"endLine": 158,
"problem": "`resolveMergeBase` 新增了多段 fetch 補救策略與錯誤診斷,但沒有看到測試驗證邊界與失敗路徑:首次 merge-base 成功、淺層 repo 走 `--unshallow`、unshallow 失敗後 deepen base/head、所有策略都失敗時要拋出含診斷且保留 `cause` 的錯誤。這段若沒測,很容易在淺層 checkout 才暴露審查整段中斷。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、停止條件、降級及最終錯誤診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 17:32:00",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 266,
"endLine": 306,
"problem": "`commitAndPushFindings` 改成一律透過 `pushWithCredential` 使用 token 推送,且 `pushWithCredential` 要保證 token 不進 argv、失敗錯誤被固定訊息取代;但 diff 沒有對這些失敗與遮蔽保證新增測試。這條路徑失敗時會直接影響 findings commit,也可能讓敏感資訊出現在測不到的例外訊息中。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 pushTokenpushWithCredential 推送策略、認證遮蔽與失敗路徑缺少測試。"
},
{
"addedAt": "2026/07/20 17:32:00",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 72,
"problem": "`agentFailureDetail` 現在會把 stderr/stdout 片段寫進 CI log,雖然有 `redactSecrets`,但沒有看到測試驗證遮罩規則與截斷邊界。這不是快樂路徑;一旦 AI CLI 失敗,未驗證的遮罩就會變成長期保存的 log 風險。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 redactSecretsagentFailureDetail 對 Authorization、token、URL 帳密、控制字元、截斷與空輸出等邊界缺少測試。"
},
{
"addedAt": "2026/07/20 17:36:21",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 72,
"endLine": 75,
"problem": "這裡把 AI CLI 的 stderr/stdout 片段預設寫進 CI log。攻擊者可以讓 CLI 失敗並把 prompt、diff、環境診斷、token、JWT、內部 URL 或 PR 內容中的敏感資料噴到 stdout/stderr`redactSecrets` 只是正規表示式盡力遮罩,漏掉格式外的憑證或 PII 時,機密就被長期保存到多人可讀的 CI log。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 debug/失敗診斷輸出 AI CLI stderr/stdout、黑名單式遮罩可繞過,以及機密或個資外洩提出相同問題。"
},
{
"addedAt": "2026/07/20 17:36:21",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 180,
"endLine": 235,
"problem": "`main()` 裡新增了兩個閉包 helper,再各自搭配整段 JSDoc,像把副歌、橋段與註腳全塞進同一小節。這些註解本身不差,但放在主流程中間會稀釋流程主線,讓讀者在真正開始步驟 3 前先穿過一大段實作細節。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 `main()` 內留言路由、issue 建立、緩衝佇列與閉包 helper 造成主流程職責混雜,並提出抽出發布協作者的相同方向。"
},
{
"addedAt": "2026/07/20 17:36:21",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 234,
"problem": "`main()` 這次被塞進建問題模式的留言路由、issue 暫存佇列、issue 建立流程與標籤套用邏輯。未來只要要調整「留言要發到 PR 還是 issue」、「何時 flush 暫存留言」、「哪些模式要靜默通過」,維護者都必須在主流程裡追閉包狀態(`issueBuffer`、`issue`、`currentRunCommentIds`),主流程會越來越像狀態機但沒有清楚邊界。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 `main()` 中 issue 狀態、`issueBuffer`、留言路由與發布狀態機職責耦合的同一問題。"
},
{
"addedAt": "2026/07/20 17:36:21",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 309,
"endLine": 390,
"problem": "建問題模式的收束流程散在多段 `if (ctx.createIssue)` 分支中:建立 issue、選標籤、嚴重問題留言、其他問題留言、PR 回貼連結、設定 dependency 都在 `main()` 裡交錯。這讓「建問題模式」沒有單一可讀入口,未來維護者要確認模式行為時必須跨多個區塊拼湊流程,尤其容易漏掉 `issue` 只在 `kept.length > 0` 後才存在的隱含前提。",
"reason": "Paladin:可排除(重複)。歷史 findings 已指出建問題模式的 issue 建立、留言分流、PR 回貼與 dependency 等流程散落在 `main()`,同屬發布流程邊界不清。"
},
{
"addedAt": "2026/07/20 17:36:21",
"prNumber": 6,
"reviewer": "Leo",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 30,
"endLine": 44,
"problem": "`agentFailureDetail` 的文件先說「原始輸出預設隱藏」,後面又說預設附上遮罩後的 stderr/stdout 片段。這種註解與實作語意互相打架,未來維護者很容易誤判 CI log 會不會包含 CLI 輸出,進而在調整遮罩或除錯策略時做錯取捨。",
"reason": "Paladin:可排除(重複)。與 F003 指涉同一段 `agentFailureDetail` 文件,問題同為「預設隱藏原始輸出」與「預設輸出遮罩片段」語意互相衝突。"
},
{
"addedAt": "2026/07/20 17:36:21",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 397,
"endLine": 415,
"problem": "在 `create-issue=true` 且本輪有保留的嚴重問題、但 `excluded` 為空時,`exclusionsChanged` 會是 `false`,建問題模式又不 commit findings,因此收尾會「略過 commit/push」後直接 `return 0`。最小重現:PR 只有 1 條嚴重 finding、沒有任何誤判排除、問題相依 API 未啟用或設定失敗;流程會建立 issue、相依設定被 catch 成 WRN,沒有 `[failure]` 結果 commit,也沒有下一輪步驟 1 可回報失敗,最後 CI 成功通過。",
"reason": "Paladin:可排除(重複)。與 F001 指涉同一個嚴重 finding gate 依賴後續 commit/push 或 dependency side effect、而本輪可能直接回傳 0 的問題。"
},
{
"addedAt": "2026/07/20 17:36:21",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 409,
"problem": "建問題模式的流程被大幅改寫,但 diff 沒看到對應測試驗證這些新分支:留言先暫存到 `issueBuffer`、`kept.length > 0` 才建立 issue、無保留問題/無可審查變更時靜默通過、PR 回貼 issue 連結,以及只有嚴重問題才呼叫 `addIssueDependency`。這些都是使用者可觀察行為,沒有測試就很容易在之後調整流程時被改壞。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、有 finding 才建 issue、靜默通過、PR 回貼、嚴重問題與 dependency 等核心分支缺少測試。"
},
{
"addedAt": "2026/07/20 17:36:21",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 102,
"endLine": 159,
"problem": "`resolveMergeBase` 新增了多段 fetchunshallowdeepen fallback 與錯誤診斷,但沒有看到針對淺層 checkout、fetch 失敗、merge-base 首次失敗後成功、所有策略失敗等邊界的測試。這段決定送審 diff 的基準,一旦 fallback 順序或錯誤處理壞掉,審查可能漏看或多看變更。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 `resolveMergeBase` 多階段 fetch、淺層 checkout、成功停止條件與全部失敗診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 17:36:21",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 266,
"endLine": 297,
"problem": "push 行為改成一律走 `pushWithCredential`,並宣稱 token 不進 argv、失敗時隱藏 URL/認證資訊,但沒有測試覆蓋成功與失敗路徑。這裡一旦 regression,可能導致結果 commit 推不上去,或失敗訊息洩漏認證材料。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushTokenpushWithCredential 的推送策略、認證環境變數、失敗遮蔽與成功失敗路徑缺少測試。"
},
{
"addedAt": "2026/07/20 17:36:21",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 84,
"problem": "`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 CI log,並依賴 `redactSecrets` 遮罩機密;但新增的遮罩規則與截斷規則沒有測試。這是典型失敗路徑,平常快樂路徑不會跑到,沒測過就無法相信它真的能處理 Authorization、token、URL 帳密、控制字元與長金鑰。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 `redactSecrets``agentFailureDetail` 對 Authorization、token、URL 帳密、控制字元、長輸出與空輸出等邊界缺少測試。"
}
]
@@ -1,12 +0,0 @@
{
"generatedAt": "2026/07/17 19:03:13",
"commitSha": "c4c23d45314bb260d7ec350db4f968021076ee17",
"prNumber": 4,
"tool": {
"name": "codex",
"version": "codex-cli 0.144.5",
"model": "(工具預設)"
},
"findings": [],
"excluded": []
}
@@ -1,12 +0,0 @@
{
"generatedAt": "2026/07/20 09:46:49",
"commitSha": "8662e8ca801c3dbf7f74ed7e758a7a55176735b8",
"prNumber": 4,
"tool": {
"name": "claude",
"version": "2.1.215 (Claude Code)",
"model": "(工具預設)"
},
"findings": [],
"excluded": []
}
@@ -1,78 +0,0 @@
{
"generatedAt": "2026/07/20 15:20:13",
"commitSha": "c980add8077dd1316a6e4d62be481bc4f1c94e25",
"prNumber": 6,
"tool": {
"name": "code-review-resolve",
"version": "0.0.8",
"model": "(工具預設)"
},
"findings": [
{
"id": "F001",
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 210,
"endLine": 224,
"problem": "`ensureIssueCreated` 同時建立 issue、修改外層 `issue` 狀態、逐筆清空 `issueBuffer`,但整段流程沒有可重入或冪等機制。若 issue 建立成功後,寫入其中一則暫存留言時失敗,主流程會中止;重跑後又會建立另一個 issue,留下內容不完整的孤兒 issue。這種依賴閉包可變狀態的半完成狀態,半年後要加入重試、續傳或測試失敗情境都會很痛苦。",
"suggestion": "把「建立追蹤 issue 並沖刷留言」抽成獨立、可注入 Gitea client 的服務函式,明確回傳 issue 與已寫入進度;建立前以 PR 編號或隱藏識別標記查找既有追蹤 issue,讓重跑能接續而非重複建立。至少也應保留已建立的 issue 編號並在錯誤訊息中回報,避免留下無法追蹤的半成品。",
"suggestedCode": ""
},
{
"id": "F002",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 306,
"endLine": 359,
"problem": "建問題模式的核心流程已大幅改變,但 diff 中沒有對應測試驗證各分支:有保留問題時才建立 issue、暫存留言依序送出、無問題時靜默通過、嚴重與非嚴重問題送往正確位置,以及標籤失敗後仍須回貼 PR 連結。這些分支牽涉多次外部 API 呼叫與狀態切換,未測試時很容易出現漏留言、留言送錯 PR/issue,或在 `issue` 尚未建立時解參考的回歸。",
"suggestion": "新增主流程測試並 mock Gitea、agent 與 git 操作,至少涵蓋:`kept=[]` 時不建立 issue 且不留言;僅嚴重問題;僅警告/建議;混合問題;建立 issue 或寫入暫存留言失敗;標籤查詢、AI 選標籤及補掛標籤失敗時仍回貼 issue 連結。除了呼叫次數,也應斷言 API 呼叫順序、目標 issue 編號及留言內容。",
"suggestedCode": ""
},
{
"id": "F003",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitea.js",
"startLine": 188,
"endLine": 191,
"problem": "新增的 `addLabelsToIssue` 沒有對應測試,尚未驗證空值捷徑與實際 API 請求格式。這個函式位於新建問題流程的收尾路徑;若 endpoint、HTTP method 或 `{ labels }` payload 不符預期,追蹤 issue 將無法取得標籤,而空陣列是否真的不發出請求也未被保護。",
"suggestion": "新增單元測試,分別傳入 `undefined`、`null`、空陣列及多個 label id;斷言前三者回傳 `null` 且完全不呼叫 API,多個 id 時以 POST 呼叫正確的 ownerrepoissue endpoint 並傳送 `{ labels: [...] }`,另驗證 API 拋錯會原樣往上傳遞。",
"suggestedCode": ""
},
{
"id": "F004",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 100,
"endLine": 122,
"problem": "`resolveMergeBase` 新增多階段 fetch 與淺層 checkout 修復邏輯,但沒有看到測試驗證成功、降級與最終失敗路徑。尤其初次 merge-base 失敗後,淺層與非淺層 repository 會走不同路徑,且多個 `tryGit` 失敗會被刻意吞掉;若參數、refspec 或重試順序有誤,只會在實際 CI checkout 深度不足時才暴露。",
"suggestion": "以 stub 的 git 執行器或暫存 repository 補齊案例:首次 merge-base 成功;淺層 repository 經 `--unshallow` 後成功;`--unshallow` 失敗但 `--deepen=1000` 後成功;非淺層首次失敗後重試成功;所有策略失敗時拋出含 `baseRef` 且保留原始 `cause` 的錯誤。並斷言 base/head fetch 的 refspec 與執行順序。",
"suggestedCode": ""
},
{
"id": "F005",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 17,
"endLine": 45,
"problem": "新增的 agent 失敗診斷涵蓋逾時、數字或字串 exit code、signal、空輸出及 500 字截斷等多個邊界,但沒有測試鎖定輸出。這些資訊只在失敗路徑出現,正常審查不會自然覆蓋;日後修改時可能悄悄遺失真正的 CLI 錯誤內容,或破壞單行與截斷約束。",
"suggestion": "將摘要邏輯匯出供測試,或透過失敗的 `runAttackers``runDefenders``fillPurposes` 測試間接斷言 log。至少覆蓋 killed、數字 exit code、字串 code、signal、stderr 與 stdout 同時存在、超過 500 字、完全無資訊,以及 `res` 為 nullundefined 的案例。",
"suggestedCode": ""
}
],
"excluded": []
}
@@ -1,143 +0,0 @@
{
"generatedAt": "2026/07/20 15:57:46",
"commitSha": "a6534ee0764fe2363ebbb5525586ad3868dc6720",
"prNumber": 6,
"tool": {
"name": "code-review-resolve",
"version": "0.0.8",
"model": "(工具預設)"
},
"findings": [
{
"id": "F001",
"reviewer": "Assassin",
"focus": "security",
"badge": "🗡️",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 72,
"endLine": 81,
"problem": "開啟 ACTIONS_STEP_DEBUG=true 後,會把攻擊者可間接操控的 AI CLI stderrstdout(經黑名單式 redactSecrets 遮罩)寫入長期保存的 CI log。短 token、JWT、含標點或空白的密碼、非典型金鑰及 PII 仍可能繞過遮罩。建議即使除錯模式也只記錄退出碼/signal/逾時/診斷 ID,若需原文則寫入受限、短期保存、需授權取得的安全 artifact。",
"suggestion": "CI log 不輸出 AI CLI 原文,即使 debug 模式;需要原文時改寫入存取受限的安全 artifact,並套用允許清單式結構化診斷與 PII/機密掃描。此為安全性與可除錯性的政策取捨:現行已刻意保留 debug-gated 遮罩輸出,是否完全移除需維護者裁示。",
"suggestedCode": "function agentFailureDetail(res) {\n const err = res && res.error;\n if (err && err.killed) return '已逾時終止';\n if (err && typeof err.code === 'number') return `exit ${err.code}`;\n if (err && err.signal) return `signal ${err.signal}`;\n return 'AI CLI 執行失敗(原始輸出已隱藏)';\n}"
},
{
"id": "F002",
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 176,
"endLine": 218,
"problem": "main() 內新增 issueBuffer、可變 issue、postComment 與 ensureIssueCreated 閉包,後續多處依 ctx.createIssue 分支。留言路由、issue 生命週期、標籤、相依關係與審查編排共享同一批可變狀態;未來增加發布目的地或重試策略時須同步理解並修改整個超長主流程,測試也只能透過 main() 間接覆蓋。",
"suggestion": "抽出具明確介面的發布器(如 PrReviewPublisher 與 IssueReviewPublisher),封裝留言暫存、issue 建立、沖刷、問題明細與收束關聯;main() 只呼叫一致的 publishContextpublishFindingsfinalize,便於分別注入假的 Gitea client 測試兩種模式並移除散落的模式判斷。屬大範圍重構+設計取捨,需維護者確認方向。",
"suggestedCode": ""
},
{
"id": "F003",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 184,
"endLine": 245,
"problem": "建問題模式新增「issue 建立前暫存留言、建立後依序沖刷」的狀態流程,但沒有對應測試。尚未驗證 createIssue 或沖刷途中拋錯、空 buffer、重複呼叫、以及一般/建問題模式留言目的地是否正確;失敗路徑可能造成 issue 已建立但內容不完整或留言誤發到 PR。",
"suggestion": "補主流程整合測試並 mock Gitea API,驗證:一般模式直接發 PR 且記錄留言 id;issue 未建立時只暫存;建立後依序沖刷並清空 buffer;createIssue 與第 N 則沖刷留言失敗時以失敗結束且不再發布後續內容。屬測試架構決策(專案目前無測試框架)。",
"suggestedCode": ""
},
{
"id": "F004",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 309,
"endLine": 395,
"problem": "建問題模式核心分支被大幅改寫,但沒有測試驗證各種 findings 組合與 API 失敗行為。kept.length===0、只有嚴重、只有警告/建議、兩者皆有,以及標籤挑選/issue 建立/PR 回貼連結/相依 API 失敗等路徑尚未試煉,無法確認「無問題完全靜默」「有問題才建 issue」及相依失敗僅降級等契約成立。",
"suggestion": "以參數化測試覆蓋 findings 四種組合,斷言 API 呼叫順序/次數/issue number/統計;並分別讓 listLabelsselectLabelscreateIssue/問題留言/PR 連結留言/addIssueDependency 拋錯,驗證哪些中止、哪些僅記警告續行;零 findings 時斷言所有寫入 API 皆不呼叫。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F005",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitea.js",
"startLine": 171,
"endLine": 215,
"problem": "新增的問題相依 API 包裝沒有測試驗證 endpoint、HTTP method 與 payload。特別是 addIssueDependency 容易顛倒的「PR 相依於 issue」方向未被斷言;若 index 或 body 欄位放反,合併阻擋語意會相反。(原併列的 addLabelsToIssue 已於本次移除。)",
"suggestion": "mock 底層 API,對相依關係使用不同的 PR/issue 編號,精確斷言 URL 指向 PR、body.index 指向阻擋來源 issue,並覆蓋 API 非 2xx 時錯誤原樣往上拋出的案例。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F006",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 103,
"endLine": 157,
"problem": "resolveMergeBase 新增多階段 fetch/重試策略,卻沒有測試鎖定淺層與失敗路徑:首次成功提早返回、非 shallow 不 unshallow、某次 fetch 失敗後仍嘗試下一策略、補抓成功立即停止、全部失敗時診斷與 cause 是否完整;易因呼叫順序或 off-by-one 在 runner 上才暴露。",
"suggestion": "將 git 執行器注入或 stub,建立表格化案例覆蓋首次成功/unshallow 後成功/deepen base 後成功/deepen HEAD 後成功/各 fetch 個別失敗/全部失敗;逐案斷言 git 參數與順序、成功後不再額外 fetch,並檢查最終錯誤含各策略成敗摘要且保留首次錯誤為 cause。屬可測性重構+測試架構決策。",
"suggestedCode": ""
},
{
"id": "F007",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 249,
"endLine": 289,
"problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)",
"suggestion": "mock git 執行器與 URLenv 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F008",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 16,
"endLine": 80,
"problem": "redactSecretsagentFailureDetail 直接決定 CI 日誌是否洩漏內容及失敗診斷是否可用,但沒有測試覆蓋。空值、控制字元、Authorization/Bearer、URL 帳密、各種 token 樣式、截斷,以及 ACTIONS_STEP_DEBUG 大小寫與未啟用時隱藏 stdout/stderr 等邊界尚未驗證。",
"suggestion": "為這兩個純函式補單元測試(必要時受控匯出或抽獨立模組),以假憑證逐一測試遮罩規則與換行注入並斷言輸出不含原始秘密;保存還原 ACTIONS_STEP_DEBUG 驗證預設/true/混合大小寫/逾時/exit codesignal/無 error/超長輸出。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F009",
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "action.yml",
"startLine": 47,
"endLine": 61,
"problem": "push-token 的用途與退回行為在區塊註解、欄位描述、required 與 default 註解中反覆說明,且單行 description 過長,資訊雖完整但重複,日後修改語意易只改到一處。",
"suggestion": "保留一段「為何需要 PAT」的必要背景,其餘讓欄位名稱、required、default 自行表意,將 description 收斂成呼叫端真正需要知道的契約。註:本專案採 doc-funcs 高密度註解慣例,是否精簡屬慣例取捨,需維護者確認。",
"suggestedCode": " # 專用推送 PAT;以 PAT 推送可重新觸發 CI。留空時沿用 token。\n push-token:\n description: '推送審查結果 commit 的 PAT(留空時沿用 token'\n required: false\n default: ''"
},
{
"id": "F010",
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 203,
"endLine": 226,
"problem": "ensureIssueCreated 之名帶有「已存在便沿用」的冪等語意,實際卻無條件建立新 issue 並悄悄改寫外層 issue;名稱、行為與副作用不一致,閱讀呼叫處易形成錯誤預期。",
"suggestion": "若此函式只允許呼叫一次,改用直接表達「建立並沖刷暫存留言」的名稱並回傳建立結果,由呼叫端明確指派 issue,讓資料流一眼可見。註:本項與 F002(抽出 publisher 大重構)指向同一段核心流程、維護者正審視中,宜與該重構一併處理,避免重複改動。",
"suggestedCode": "const createIssueAndFlushBuffer = async (labelIds = []) => {\n const createdIssue = await gitea.createIssue(ctx, {\n title: ctx.prTitle || `AI Code ReviewPR #${ctx.prNumber}`,\n body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),\n labels: labelIds,\n });\n for (const body of issueBuffer) {\n await gitea.createCommentOnIssue(ctx, createdIssue.number, body);\n }\n issueBuffer.length = 0;\n return createdIssue;\n};\nissue = await createIssueAndFlushBuffer(labelIds);"
}
],
"excluded": []
}
+3 -68
View File
@@ -1,110 +1,45 @@
# ============================================================================
# 用途:在 pull request 針對 develop 分支開啟或同步時,分別以 Antigravity、Codex、Claude 環境執行本機 AI Code Review action。
# 更新時間:2026/07/17 18:49:58
# ============================================================================
# Workflow 顯示名稱。
name: CI name: CI
# Workflow 觸發條件區塊。
on: on:
# 以 pull request 事件觸發。
pull_request: pull_request:
# 限定 pull request 目標分支。
branches: branches:
# 僅在目標分支為 develop 時執行。
- develop - develop
# 限定 pull request 開啟與同步更新時執行。
types: [opened, synchronize] types: [opened, synchronize]
# Job 定義區塊。
jobs: jobs:
# Antigravity 工具環境測試 job。
test-antigravity: test-antigravity:
# Job 在 workflow UI 顯示的名稱。
name: TEST (Antigravity) name: TEST (Antigravity)
# 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。
runs-on: ubuntu runs-on: ubuntu
# Job 執行步驟。
steps: steps:
# 取得存取庫內容供後續 action 使用。
- name: 取得存取庫資訊 - name: 取得存取庫資訊
# 使用呼叫端 vars 指定的 checkout action 版本。
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
# 安裝 Antigravity 工具環境。
- name: 安裝工具 - name: 安裝工具
# 使用 Gitea composite action 安裝 Antigravity,版本由 vars 指定。
uses: https://gitea.jsc.idv.tw/composite-actions/setup-antigravity@${{ vars.ACTION_SETUP_ANTIGRAVITY_VERSION }} uses: https://gitea.jsc.idv.tw/composite-actions/setup-antigravity@${{ vars.ACTION_SETUP_ANTIGRAVITY_VERSION }}
with:
oauth: ${{ secrets.ANTIGRAVITY_OAUTH }}
# 執行目前存取庫的 AI Code Review action。
- name: 程式碼審查 - name: 程式碼審查
# 以目前存取庫根目錄的 action.yml 作為 action 來源。
uses: ./ uses: ./
# 傳入 action inputs。
with: with:
# PR 留言與 findings 寫回用的 token(呼叫端以 secrets 傳入)。 token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.TOKEN }}
# 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。
create-issue: 'true'
# 指定 AI 模型:CI 測試固定用 Antigravity 最省模型以降低消耗。
model: gemini-3.5-flash
# Codex 工具環境測試 job。
test-codex: test-codex:
# Job 在 workflow UI 顯示的名稱。
name: TEST (Codex) name: TEST (Codex)
# 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。
runs-on: ubuntu runs-on: ubuntu
# Job 執行步驟。
steps: steps:
# 取得存取庫內容供後續 action 使用。
- name: 取得存取庫資訊 - name: 取得存取庫資訊
# 使用呼叫端 vars 指定的 checkout action 版本。
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
# 安裝 Codex 工具環境。
- name: 安裝工具 - name: 安裝工具
# 使用 Gitea composite action 安裝 Codex,版本由 vars 指定。
uses: https://gitea.jsc.idv.tw/composite-actions/setup-codex@${{ vars.ACTION_SETUP_CODEX_VERSION }} uses: https://gitea.jsc.idv.tw/composite-actions/setup-codex@${{ vars.ACTION_SETUP_CODEX_VERSION }}
with:
oauth: ${{ secrets.CODEX_OAUTH }}
# 執行目前存取庫的 AI Code Review action。
- name: 程式碼審查 - name: 程式碼審查
# 以目前存取庫根目錄的 action.yml 作為 action 來源。
uses: ./ uses: ./
# 傳入 action inputs。
with: with:
# PR 留言與 findings 寫回用的 token(呼叫端以 secrets 傳入)。 token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.TOKEN }}
# 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。
create-issue: 'true'
# 指定 AI 模型:CI 測試固定用 Codex 最省模型以降低消耗。
model: gpt-5.5
# Claude 工具環境測試 job。
test-claude: test-claude:
# Job 在 workflow UI 顯示的名稱。
name: TEST (Claude) name: TEST (Claude)
# 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。
runs-on: ubuntu runs-on: ubuntu
# Job 執行步驟。
steps: steps:
# 取得存取庫內容供後續 action 使用。
- name: 取得存取庫資訊 - name: 取得存取庫資訊
# 使用呼叫端 vars 指定的 checkout action 版本。
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
# 安裝 Claude 工具環境。
- name: 安裝工具 - name: 安裝工具
# 使用 Gitea composite action 安裝 Claude,版本由 vars 指定。
uses: https://gitea.jsc.idv.tw/composite-actions/setup-claude@${{ vars.ACTION_SETUP_CLAUDE_VERSION }} uses: https://gitea.jsc.idv.tw/composite-actions/setup-claude@${{ vars.ACTION_SETUP_CLAUDE_VERSION }}
with:
oauth: ${{ secrets.CLAUDE_OAUTH }}
# 執行目前存取庫的 AI Code Review action。
- name: 程式碼審查 - name: 程式碼審查
# 以目前存取庫根目錄的 action.yml 作為 action 來源。
uses: ./ uses: ./
# 傳入 action inputs。
with: with:
# PR 留言與 findings 寫回用的 token(呼叫端以 secrets 傳入)。 token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.TOKEN }}
# 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。
create-issue: 'true'
# 指定 AI 模型:Claude job 使用 Opus 4.8。
model: claude-opus-4-8
-41
View File
@@ -1,41 +0,0 @@
# Workflow 文件草稿
更新時間:2026/07/17 18:49:58
## Workflow 總覽
| 名稱 | 檔案位置 | 用途 | 觸發條件 |
| --- | --- | --- | --- |
| CI | `.gitea/workflows/ci.yaml` | 在 PR 事件中分別以 Antigravity、Codex、Claude 工具環境執行本存取庫的 AI Code Review action,驗證 action 可在不同 AI 工具環境下運作。 | `pull_request` 目標分支為 `develop`,事件類型為 `opened``synchronize`。 |
## CI
- Workflow 名稱:`CI`
- 檔案位置:`.gitea/workflows/ci.yaml`
- 用途:針對送往 `develop` 的 pull request 執行三組測試 job,分別安裝 Antigravity、Codex、Claude 環境後呼叫 `uses: ./` 執行目前 action。
- 觸發條件:`pull_request.branches``develop``pull_request.types``opened``synchronize`
## Job
| Job ID | 顯示名稱 | Runner | 主要流程 |
| --- | --- | --- | --- |
| `test-antigravity` | `TEST (Antigravity)` | `ubuntu` | checkout 存取庫、安裝 Antigravity、執行本地 action。 |
| `test-codex` | `TEST (Codex)` | `ubuntu` | checkout 存取庫、安裝 Codex、執行本地 action。 |
| `test-claude` | `TEST (Claude)` | `ubuntu` | checkout 存取庫、安裝 Claude、執行本地 action。 |
## 主要輸入 / 環境參數
| 名稱 | 來源 | 使用位置 | 說明 |
| --- | --- | --- | --- |
| `vars.ACTION_CHECKOUT_VERSION` | Gitea / GitHub repository 或 organization vars | `actions/checkout@...` | 指定 checkout action 版本。 |
| `vars.ACTION_SETUP_ANTIGRAVITY_VERSION` | Gitea / GitHub vars | `setup-antigravity@...` | 指定 Antigravity 安裝 action 版本。 |
| `vars.ACTION_SETUP_CODEX_VERSION` | Gitea / GitHub vars | `setup-codex@...` | 指定 Codex 安裝 action 版本。 |
| `vars.ACTION_SETUP_CLAUDE_VERSION` | Gitea / GitHub vars | `setup-claude@...` | 指定 Claude 安裝 action 版本。 |
| `secrets.GITHUB_TOKEN` | Gitea / GitHub secrets | 本地 action input `token` | 提供 action 呼叫 Gitea API 留言與寫回 findings 所需 token。 |
## 注意事項
- `.gitea/workflows/readme.md` 目前不存在;本檔為新增 workflow README 的草稿。
- `runs-on: ubuntu` 是否符合實際 Gitea runner 標籤需人工確認。
- `vars.*``secrets.GITHUB_TOKEN` 必須由呼叫端環境提供,否則 checkout、工具安裝或程式碼審查步驟可能失敗。
- 三個 job 均以 `uses: ./` 呼叫目前存取庫根目錄的 `action.yml`;因此 `action.yml``runs.main` 需保持可被 runner 直接執行。
+27 -29
View File
@@ -1,55 +1,53 @@
# ============================================================================ # =====================================================
# 用途:定義 AI Code Review Node action 的名稱、輸入參數與 Node.js 24 進入點,供 Gitea / GitHub workflow 以 uses 引用。 # 用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings
# 更新時間2026/07/17 18:49:58 # 更新時間: 2026/07/17 16:49:21
# ============================================================================ # =====================================================
# Gitea / GitHub node action 的 manifestaction.yml): # Gitea / GitHub node action 的 manifestaction.yml):
# 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs), # 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs),
# 供呼叫端 workflow 以 `uses:` 引用;runner 讀取此檔後以 node24 執行 src/index.js。 # 供呼叫端 workflow 以 `uses:` 引用;runner 讀取此檔後以 node24 執行 src/index.js。
# action 顯示名稱:呼叫端 workflow log 與 marketplace 列表上看到的名稱 # action 顯示名稱:呼叫端 workflow log 與 marketplace 列表上看到的名稱
name: 'AI Code Review' name: 'AI Code Review'
# action 用途說明:多角色 AI code review 流程(攻擊方找問題、防守方裁決誤報), # action 用途說明:多角色 AI code review 流程(攻擊方找問題、防守方裁決誤報),
# 審查結果會留言到 PR 並保存 findings.gitea/ai-review/findings/ # 審查結果會留言到 PR 並保存 findings.gitea/ai-review/findings/
description: 'AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings' description: 'AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings'
# action 作者資訊(僅供辨識,不影響執行) # action 作者資訊(僅供辨識,不影響執行)
author: 'Jeffery' author: 'Jeffery'
# 輸入參數區塊:呼叫端 workflow 以 `with:` 傳入, # 輸入參數區塊:呼叫端 workflow 以 `with:` 傳入,
# runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取 # runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取
inputs: inputs:
# Gitea API token:用於對 PRissue 留言審查結果以及 push 審查結果檔findings/exclusions回 repo # Gitea API token:用於對 PR 留言審查結果以及 push 審查結果檔回 repo
token: token:
# 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。 # 參數用途說明:secrets/vars context 在 action 內不可用,
# 建議傳入「能觸發 CI 的 PAT」:以自動 tokengitea.token / GITHUB_TOKEN)推送的結果 commit 不會 # 故由呼叫端 workflow 以 secrets.GITHUB_TOKEN 傳入
# 再觸發 CI,導致新 head 缺檢查而卡合併;改用 PAT 推送會讓 PR 的 synchronize 事件再觸發 CI description: 'Gitea API tokenPR 留言與 push findings 用;呼叫端以 secrets.GITHUB_TOKEN 傳入)'
# 由主程式步驟 1 快速回報([success]/[failure])廉價地把結果蓋到新 head。 # 必填:缺少 token 無法呼叫 Gitea APIaction 無法運作
description: 'Gitea API tokenPR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)'
# 必填:缺少 token 無法呼叫 Gitea APIaction 無法運作。
required: true required: true
# 指定 AI 工具使用的模型名稱 # 指定 AI 工具使用的模型名稱
model: model:
# 參數用途說明:留空表示使用各 AI 工具自身的預設模型 # 參數用途說明:留空表示使用各 AI 工具自身的預設模型
description: '指定 AI 工具使用的模型(空值=各工具預設)' description: '指定 AI 工具使用的模型(空值=各工具預設)'
# 選填:未指定時採用預設值 # 選填:未指定時採用預設值
required: false required: false
# 預設為空字串,代表不覆寫各工具的預設模型 # 預設為空字串,代表不覆寫各工具的預設模型
default: '' default: ''
# 建問題模式開關:是否把審查保留的問題另建 issue 追蹤 # 建問題模式開關:是否把審查保留的問題另建 issue 追蹤
create-issue: create-issue:
# 參數用途說明:字串 'true' 時建立 issue(標題=PR 標題、描述=PR 描述、AI 挑標籤) # 參數用途說明:字串 'true' 時建立 issue(標題=PR 標題、描述=PR 描述、AI 挑標籤)
# 並逐條留言問題明細,findings 檔不進版控、收尾只 commit exclusions.json # 並逐條留言問題明細,findings 檔不進版控、收尾只 commit exclusions.json
# 預設 'false' 走原流程(findings 檔與 exclusions.json 一併 commit 回 PR 來源分支) # 預設 'false' 走原流程(findings 檔與 exclusions.json 一併 commit 回 PR 來源分支)
description: '是否將問題建到存取庫的問題追蹤(true 時建立 issue 逐條留言問題明細,最後只 commit exclusions.json;預設 false 走原流程)' description: '是否將問題建到存取庫的問題追蹤(true 時建立 issue 逐條留言問題明細,最後只 commit exclusions.json;預設 false 走原流程)'
# 選填:未指定時採用預設值 # 選填:未指定時採用預設值
required: false required: false
# 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用) # 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用)
default: 'false' default: 'false'
# 執行方式區塊:宣告本 action 為 node action 及其進入點 # 執行方式區塊:宣告本 action 為 node action 及其進入點
runs: runs:
# 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite # 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite
using: 'node24' using: 'node24'
# 主程式進入點:直接指向 src/index.jsentry point # 主程式進入點:直接指向 src/index.jsentry point
# 主程式為零外部相依(package.json 無 dependenciessrc 僅 require Node 內建模組與本地 lib), # 主程式為零外部相依(package.json 無 dependenciessrc 僅 require Node 內建模組與本地 lib),
# runner 不會自動 npm install,零相依時依 node action 慣例 main 直接指向 src/index.js 即正確; # runner 不會自動 npm install,零相依時依 node action 慣例 main 直接指向 src/index.js 即正確;
# 日後若新增外部相依,需改以 @vercel/ncc 打包(package.json 已備有 build script # 日後若新增外部相依,需改以 @vercel/ncc 打包(package.json 已備有 build script
# 並將 main 改指 dist/index.js、把 dist/ commit 進 repo # 並將 main 改指 dist/index.js、把 dist/ commit 進 repo
main: 'src/index.js' main: 'src/index.js'
+75 -75
View File
@@ -1,6 +1,6 @@
# AI Code Review # AI Code Review
> 更新時間:2026/07/17 18:49:58 > 更新時間:2026/07/17 16:49:21
AI 多角色 code review 的 Gitea **node action**`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]``[failure]`)供下次觸發快速回報。 AI 多角色 code review 的 Gitea **node action**`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]``[failure]`)供下次觸發快速回報。
@@ -38,14 +38,14 @@ jobs:
```mermaid ```mermaid
flowchart TD flowchart TD
S1[1 判斷 bot commit 標記] -->|命中| E0[直接回報 success/failure] S1[1 判斷 bot commit 標記] -->|命中| E0[直接回報 success/failure]
S1 -->|未命中| S3[3 偵測 AI 工具並留言] S1 -->|未命中| S2[2 偵測 AI 工具並留言]
S3 --> S4[4 讀 .reviewignore 整理 diff 並留言] S2 --> S3[3 讀 .reviewignore 整理 diff 並留言]
S4 --> S5[5 攻擊方登場留言] S3 --> S4[4 攻擊方登場留言]
S5 --> S6[6 攻擊方 sub agent 並行找問題] S4 --> S5[5 攻擊方 sub agent 並行找問題]
S6 --> S7[7 防守方登場留言] S5 --> S6[6 防守方登場留言]
S7 --> S8[8 防守方裁決 → 保存 findings 誤判回寫 exclusions.json] S6 --> S7[7 防守方裁決 → 保存 findings 誤判回寫 exclusions.json]
S8 --> S2[2 延後將舊留言標記解決(成功產生結果後才執行)] S7 --> S8[8 舊留言標記解決]
S2 --> S9[9 嚴重問題逐條掛行留言] S8 --> S9[9 嚴重問題逐條掛行留言]
S9 --> S10[10 警告+建議彙整表格留言] S9 --> S10[10 警告+建議彙整表格留言]
S10 --> E1[收尾 commit/push exit code] S10 --> E1[收尾 commit/push exit code]
``` ```
@@ -56,19 +56,19 @@ flowchart TD
| 專案名稱 | 專案描述 | | 專案名稱 | 專案描述 |
| --- | --- | | --- | --- |
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | Gitea node action:提供台北時區日誌工具、runner 上下文載入、git diffcommit 操作、Gitea REST API 客戶端(留言/reviewissue/標籤)、AI CLI 工具偵測與 sub agent 執行、角色提示載入、固定留言模板,以及多角色審查編排(攻擊方找問題、防守方裁決、findings 保存、誤判回寫、建問題模式) | | [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | Gitea node action:提供台北時區日誌工具、runner 上下文載入、git diffcommit 操作、Gitea REST API 客戶端(留言/reviewissue/標籤)、AI CLI 工具偵測與 sub agent 執行、角色提示載入、固定留言模板,以及多角色審查編排(攻擊方找問題、防守方裁決、findings 保存、誤判回寫、建問題模式) |
### 參考專案表 ### 參考專案表
| 專案名稱 | 參考專案列表 | | 專案名稱 | 參考專案列表 |
| --- | --- | | --- | --- |
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | 無 | | [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | 無 |
### NuGet 套件表 ### NuGet 套件表
| 專案名稱 | NuGet 套件列表 | | 專案名稱 | NuGet 套件列表 |
| --- | --- | | --- | --- |
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | 無 | | [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | 無 |
## 功能列表 ## 功能列表
@@ -76,56 +76,56 @@ flowchart TD
| 功能名稱 | 功能描述 | | 功能名稱 | 功能描述 |
| --- | --- | | --- | --- |
| [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L20) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) | | [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L20) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) |
| [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L41) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) | | [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L41) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) |
| [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) | | [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) |
| [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) | | [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) |
| [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) | | [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) |
| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L79) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | | [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L60) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) |
| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L100) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | | [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L81) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) |
| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L136) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | | [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L104) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) |
| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L157) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | | [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L125) | [取得單一檔案的 git diff 內容](#gitrepofilediff) |
| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L175) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | | [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L143) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) |
| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L213) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | | [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L181) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) |
| [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) | | [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) |
| [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) | | [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) |
| [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) | | [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) |
| [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L141) | [列出存取庫可用標籤](#gitealistlabels) | | [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L141) | [列出存取庫可用標籤](#gitealistlabels) |
| [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L164) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) | | [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L164) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) |
| [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L184) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) | | [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L184) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) |
| [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L202) | [編輯既有一般留言](#giteaeditissuecomment) | | [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L202) | [編輯既有一般留言](#giteaeditissuecomment) |
| [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L223) | [建立 code review 並掛行內留言](#giteacreatereview) | | [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L223) | [建立 code review 並掛行內留言](#giteacreatereview) |
| [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L243) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) | | [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L243) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) |
| [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L264) | [列出某 review 的全部行內留言](#gitealistreviewcomments) | | [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L264) | [列出某 review 的全部行內留言](#gitealistreviewcomments) |
| [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L287) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) | | [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L287) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) |
| [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L53) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) | | [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L53) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) |
| [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L98) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) | | [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L98) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) |
| [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L144) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) | | [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L144) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) |
| [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) | | [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) |
| [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) | | [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) |
| [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) | | [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) |
| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L88) | [產生步驟 3 審查工具留言](#templatestoolcomment) | | [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L88) | [產生步驟 2 審查工具留言](#templatestoolcomment) |
| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L132) | [產生步驟 4 變更摘要留言](#templatesdiffcomment) | | [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L132) | [產生步驟 3 變更摘要留言](#templatesdiffcomment) |
| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L173) | [產生步驟 57 角色登場留言](#templatesrolescomment) | | [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L173) | [產生步驟 46 角色登場留言](#templatesrolescomment) |
| [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) | | [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) |
| [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) | | [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) |
| [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) | | [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) |
| [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L309) | [產生建問題模式的 issue 本文](#templatesissuebody) | | [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L309) | [產生建問題模式的 issue 本文](#templatesissuebody) |
| [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L341) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) | | [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L341) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) |
| [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L379) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) | | [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L379) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) |
| [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L29) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) | | [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L29) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) |
| [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L53) | [判斷檔案是否忽略不送審](#reviewisignored) | | [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L53) | [判斷檔案是否忽略不送審](#reviewisignored) |
| [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L77) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) | | [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L77) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) |
| [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L127) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) | | [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L127) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) |
| [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L276) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) | | [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L276) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) |
| [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L448) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) | | [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L448) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) |
| [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L572) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) | | [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L572) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) |
| [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L521) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) | | [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L521) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) |
| [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L596) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) | | [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L596) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) |
| [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L627) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) | | [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L627) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) |
| [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L694) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) | | [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L694) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) |
| [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L778) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) | | [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L778) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) |
| [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L855) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) | | [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L855) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) |
## 使用範例 ## 使用範例
@@ -168,8 +168,8 @@ taipeiFromIso(''); // '—'
```js ```js
const { log } = require('./src/lib/log'); const { log } = require('./src/lib/log');
log('步驟4', 'INF', '變更檔案 5 個,送審 3 個。'); log('步驟3', 'INF', '變更檔案 5 個,送審 3 個。');
// [2026/07/17 16:46:13][步驟4][INF]: 變更檔案 5 個,送審 3 個。 // [2026/07/17 16:46:13][步驟3][INF]: 變更檔案 5 個,送審 3 個。
``` ```
<a id="contextloadcontext"></a> <a id="contextloadcontext"></a>
@@ -246,7 +246,7 @@ const committed = gitrepo.commitAndPushFindings(cwd, {
<a id="giteawhoami"></a> <a id="giteawhoami"></a>
### gitea.whoAmI ### gitea.whoAmI
取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 2`login` 比對留言作者辨識本 action 發過的留言。 取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 8`login` 比對留言作者辨識本 action 發過的留言。
```js ```js
const gitea = require('./src/lib/gitea'); const gitea = require('./src/lib/gitea');
@@ -269,7 +269,7 @@ await gitea.createCommentOnIssue(ctx, issue.number, '🔴 嚴重|...');
```js ```js
const created = await gitea.createIssueComment(ctx, '## 📋 變更摘要 ...'); const created = await gitea.createIssueComment(ctx, '## 📋 變更摘要 ...');
// created.id 記入本回合留言集合,步驟 2 標註過時時跳過 // created.id 記入本回合留言集合,步驟 8 標註過時時跳過
``` ```
<a id="gitealistlabels"></a> <a id="gitealistlabels"></a>
@@ -294,7 +294,7 @@ const issue = await gitea.createIssue(ctx, { title: 'PR 標題', body: '…', la
<a id="gitealistissuecomments"></a> <a id="gitealistissuecomments"></a>
### gitea.listIssueComments ### gitea.listIssueComments
列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 2 據此找出 bot 舊留言標註〔已過時〕。 列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 8 據此找出 bot 舊留言標註〔已過時〕。
```js ```js
const comments = await gitea.listIssueComments(ctx); const comments = await gitea.listIssueComments(ctx);
@@ -303,7 +303,7 @@ const comments = await gitea.listIssueComments(ctx);
<a id="giteaeditissuecomment"></a> <a id="giteaeditissuecomment"></a>
### gitea.editIssueComment ### gitea.editIssueComment
以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 2 用來替舊留言加上〔已過時〕前綴。 以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 8 用來替舊留言加上〔已過時〕前綴。
```js ```js
await gitea.editIssueComment(ctx, comment.id, `> 〔已過時〕…\n\n${comment.body}`); await gitea.editIssueComment(ctx, comment.id, `> 〔已過時〕…\n\n${comment.body}`);
@@ -323,7 +323,7 @@ await gitea.createReview(ctx, '## 🔴 嚴重問題(共 2 條)…', [
<a id="gitealistreviews"></a> <a id="gitealistreviews"></a>
### gitea.listReviews ### gitea.listReviews
列出 PR 全部 review(自動分頁);步驟 2 據此逐一取出行內留言嘗試解決。 列出 PR 全部 review(自動分頁);步驟 8 據此逐一取出行內留言嘗試解決。
```js ```js
const reviews = await gitea.listReviews(ctx); const reviews = await gitea.listReviews(ctx);
@@ -411,7 +411,7 @@ const defenders = defendersOf(roles); // [Paladin]
<a id="templatestoolcomment"></a> <a id="templatestoolcomment"></a>
### templates.toolComment ### templates.toolComment
產生步驟 3 的審查工具留言:工具/版本/模型/審查 commit/Run Job 連結表格+審查管線 mermaid 流程圖;開頭含隱藏標記供步驟 2 辨識。 產生步驟 2 的審查工具留言:工具/版本/模型/審查 commit/Run Job 連結表格+審查管線 mermaid 流程圖;開頭含隱藏標記供步驟 8 辨識。
```js ```js
const body = templates.toolComment({ const body = templates.toolComment({
@@ -424,7 +424,7 @@ const body = templates.toolComment({
<a id="templatesdiffcomment"></a> <a id="templatesdiffcomment"></a>
### templates.diffComment ### templates.diffComment
產生步驟 4 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。 產生步驟 3 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。
```js ```js
const body = templates.diffComment(diffRows, ignoredCount); const body = templates.diffComment(diffRows, ignoredCount);
@@ -433,7 +433,7 @@ const body = templates.diffComment(diffRows, ignoredCount);
<a id="templatesrolescomment"></a> <a id="templatesrolescomment"></a>
### templates.rolesComment ### templates.rolesComment
產生步驟 57 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。 產生步驟 46 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。
```js ```js
const body = templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers }); const body = templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers });
@@ -534,7 +534,7 @@ await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows });
<a id="reviewrunattackers"></a> <a id="reviewrunattackers"></a>
### review.runAttackers ### review.runAttackers
步驟 6:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。 步驟 5:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。
```js ```js
const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows });
@@ -543,7 +543,7 @@ const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attack
<a id="reviewrundefenders"></a> <a id="reviewrundefenders"></a>
### review.runDefenders ### review.runDefenders
步驟 8:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。 步驟 7:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。
```js ```js
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
@@ -600,7 +600,7 @@ if (ctx.createIssue && kept.length > 0) {
<a id="reviewresolveoldcomments"></a> <a id="reviewresolveoldcomments"></a>
### review.resolveOldComments ### review.resolveOldComments
步驟 2:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。 步驟 8:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。
```js ```js
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
+39 -174
View File
@@ -4,7 +4,7 @@
console.log('================================================'); console.log('================================================');
console.log('Action : AI Code Review'); console.log('Action : AI Code Review');
console.log('用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings'); console.log('用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings');
console.log('更新時間: 2026/07/17 18:49:58'); console.log('更新時間: 2026/07/17 16:49:21');
console.log('================================================'); console.log('================================================');
const fs = require('fs'); const fs = require('fs');
@@ -42,9 +42,9 @@ const BOT_COMMIT_PREFIX = 'chore: update ai-review findings [ai-review-bot]';
* @param {Array<Object>} params.excluded - 被裁決為誤報而排除的問題清單。 * @param {Array<Object>} params.excluded - 被裁決為誤報而排除的問題清單。
* @returns {string} findings JSON 檔相對於 repo 根目錄的路徑(例如 `.gitea/ai-review/findings/xxx.json`)。 * @returns {string} findings JSON 檔相對於 repo 根目錄的路徑(例如 `.gitea/ai-review/findings/xxx.json`)。
* @remarks * @remarks
* 使用情境:`main()` 步驟 8 於防守方裁決、`review.sortFindings(kept)` 排序後呼叫本函式保存結果, * 使用情境:`main()` 步驟 7 於防守方裁決、`review.sortFindings(kept)` 排序後呼叫本函式保存結果,
* 再將回傳的相對路徑交給 `commitFindings` commit 並 push 回 PR 來源分支; * 再將回傳的相對路徑交給 `commitFindings` commit 並 push 回 PR 來源分支;
* 另在步驟 4 判定無可審查變更時,也會以空清單保存一份空 findings 後以 success 收場。 * 另在步驟 3 判定無可審查變更時,也會以空清單保存一份空 findings 後以 success 收場。
* 本函式無 try/catch,檔案系統錯誤會往上拋出,由 `main().catch` 以 exit code 1 收場。 * 本函式無 try/catch,檔案系統錯誤會往上拋出,由 `main().catch` 以 exit code 1 收場。
*/ */
function saveFindings({ cwd, ctx, tool, kept, excluded }) { function saveFindings({ cwd, ctx, tool, kept, excluded }) {
@@ -61,7 +61,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) {
}; };
fs.writeFileSync(findingsPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); fs.writeFileSync(findingsPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
const relativePath = path.relative(cwd, findingsPath); const relativePath = path.relative(cwd, findingsPath);
log('步驟8', 'INF', `findings 已保存:${relativePath}(保留 ${kept.length} 條、排除 ${excluded.length} 條)。`); log('步驟7', 'INF', `findings 已保存:${relativePath}(保留 ${kept.length} 條、排除 ${excluded.length} 條)。`);
return relativePath; return relativePath;
} }
@@ -88,10 +88,8 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) {
* @remarks * @remarks
* 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、 * 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、
* 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json * 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json
* 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 4 判定無可審查變更且非建問題模式時, * 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 3 判定無可審查變更且非建問題模式時,
* 也會以 result: 'success' 提交空 findings。 * 也會以 result: 'success' 提交空 findings。
* 推送一律以 `ctx.token` 的身分進行(不走 runner 的 origin 自動 token);只要 token 是能觸發 CI 的
* PAT,結果 commit 就會再觸發 CI、由步驟 1 快速回報;
* 注意 commit 訊息與模組常數 `BOT_COMMIT_PREFIX` 耦合,修改前綴會使步驟 1 的快速回報失效。 * 注意 commit 訊息與模組常數 `BOT_COMMIT_PREFIX` 耦合,修改前綴會使步驟 1 的快速回報失效。
*/ */
function commitFindings({ cwd, ctx, files, result }) { function commitFindings({ cwd, ctx, files, result }) {
@@ -119,40 +117,29 @@ function commitFindings({ cwd, ctx, files, result }) {
/** /**
* AI code review 主流程:依固定 10 步驟執行多角色審查,回傳 process exit code。 * AI code review 主流程:依固定 10 步驟執行多角色審查,回傳 process exit code。
* *
* 流程概要(步驟 2~10 描述一般模式;建問題模式差異見末段) * 流程概要:
* 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit[success]/[failure]),直接回報 0/1 不重審; * 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit[success]/[failure]),直接回報 0/1 不重審;
* 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步)—— * 2. 偵測 AI 工具(antigravitycodexclaude)並留言;
* 此步延後到「本回合審查已成功產生結果、即將發布問題留言前」才執行,避免工具偵測/diff/ * 3. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings,
* 攻防裁決任一失敗時舊結果先被清掉卻沒有新結果(一般模式);
* 3. 偵測 AI 工具(antigravitycodexclaude)並留言;
* 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings,
* 一般模式 commit success、建問題模式略過 commit,回傳 0); * 一般模式 commit success、建問題模式略過 commit,回傳 0);
* 56. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題; * 45. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題;
* 78. 防守方登場留言、裁決誤報後排序並保存 findings JSON * 67. 防守方登場留言、裁決誤報後排序並保存 findings JSON
* 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json * 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json
* 8. 將 PR 既有舊留言標記為解決(跳過本回合留言);
* 9. 嚴重問題逐條掛在程式碼行上留言; * 9. 嚴重問題逐條掛在程式碼行上留言;
* 10. 警告+建議彙整為單一表格留言; * 10. 警告+建議彙整為單一表格留言;
* 建問題模式(input: create-issue):不執行步驟 2、不觸碰 PR 既有留言;步驟 3~10 的所有留言 * 建問題模式(input: create-issue):保留問題另建 issuecreateIssueWithFindings)逐條留言明細;
* 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後先挑好標籤、連同標籤一次建立 issue
* 並寫入暫存留言,嚴重問題與警告+建議再逐條發到該 issue,讓每條問題都能被個別回覆);
* 無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過);
* 收束時在 PR 回貼 issue 連結形成雙向關聯,
* 並「僅在有嚴重問題時」讓 PR 相依於該 issueaddIssueDependencyissue 關閉前 PR 無法合併;
* 需 repo 啟用問題相依功能)——僅有警告/建議時 issue 仍建立供追蹤,但不阻擋合併;
* 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、 * 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、
* 建問題模式只 commit exclusions.json、無檔案可 commit 時略過; * 建問題模式只 commit exclusions.json、無檔案可 commit 時略過;
* commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。 * commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。
* *
* @returns {Promise<number>} process exit code本輪「審查」一律回傳 0(不因嚴重問題直接讓檢查失敗—— * @returns {Promise<number>} process exit code0=成功(無嚴重問題或無可審查變更、或偵測到 success 標記);
* 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報); * 1=失敗(有嚴重問題、缺 PR 編號/token、找不到 AI 工具、或偵測到 failure 標記)。
* 回傳 1 僅發生於:步驟 1 偵測到 `[ai-review-bot][failure]` 結果 commit,或前置條件不足
* (缺 PR 編號/token、找不到 AI 工具)等無法進行審查的情況。
* @remarks * @remarks
* 使用情境:由本檔尾端的頂層呼叫端執行 —— `main().then((code) => process.exit(code))` * 使用情境:由本檔尾端的頂層呼叫端執行 —— `main().then((code) => process.exit(code))`
* 非預期例外由頂層 `catch` 記 ERR log 後以 exit code 1 收場,且刻意不 commit 結果標記, * 非預期例外由頂層 `catch` 記 ERR log 後以 exit code 1 收場,且刻意不 commit 結果標記,
* 讓下一次 workflow 觸發時重新完整審查。警告+建議等級不影響結果標記,只有「嚴重」會使結果 commit * 讓下一次 workflow 觸發時重新完整審查。警告+建議等級的問題不影響成敗,只有「嚴重」會使結果為 failure
* 標記為 failure;而「失敗檢查(exit 1)」只由步驟 1 讀到該 failure 結果 commit 時產生,審查本輪不直接 exit 1 * 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變成敗判定
* 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變上述結果標記判定。
*/ */
async function main() { async function main() {
const ctx = loadContext(); const ctx = loadContext();
@@ -180,70 +167,21 @@ async function main() {
return 1; return 1;
} }
// 本回合(一般模式)發出的 PR 留言 idresolveOldComments 標註過時時要跳過這些。 // 本回合發出的一般留言 id:步驟 8 標註過時時要跳過這些。
const currentRunCommentIds = new Set(); const currentRunCommentIds = new Set();
// 建問題模式:issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色)
// 先暫存於 issueBuffer,建立 issue 後一次寫入。
const issueBuffer = [];
let issue = null;
/**
* 發布一則審查留言。依模式決定去向:
* - 一般模式:發到 PR,並記錄留言 id 供 `resolveOldComments` 排除。
* - 建問題模式:issue 已建立時發到 issue;尚未建立時先暫存到 `issueBuffer`。
*
* @param {string} body 要發布的 Markdown 留言內容。
* @returns {Promise<Object|null>} 一般模式、或建問題模式且 issue 已建立時回傳 Gitea 留言物件;
* 建問題模式尚未建立 issue 而先暫存時回傳 null。
* @remarks
* 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與
* 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。
*/
const postComment = async (body) => { const postComment = async (body) => {
if (ctx.createIssue) {
if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body);
issueBuffer.push(body);
return null;
}
const created = await gitea.createIssueComment(ctx, body); const created = await gitea.createIssueComment(ctx, body);
currentRunCommentIds.add(created.id); currentRunCommentIds.add(created.id);
return created; return created;
}; };
/**
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
* 並把 `issueBuffer` 內暫存的情境留言依序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
*
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
* 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。
* @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。
*/
const ensureIssueCreated = async (labelIds = []) => {
issue = await gitea.createIssue(ctx, {
title: ctx.prTitle || `AI Code ReviewPR #${ctx.prNumber}`,
body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
labels: labelIds,
});
log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`);
for (const body of issueBuffer) {
await gitea.createCommentOnIssue(ctx, issue.number, body);
}
issueBuffer.length = 0;
};
// ── 步驟 2延後執行 ─────────────────────────────────────────────────── // ── 步驟 2偵測 AI agent 工具並留言 ──────────────────────────────────
// 「將 PR 既有留言標記為解決」原本在此執行,但若工具偵測/diff/攻防裁決任一失敗,
// 舊結果會先被清掉卻沒有新結果。故延後到「本回合審查已成功產生結果、發布問題留言前」
// 才呼叫 review.resolveOldComments(見下方步驟 4 空變更路徑與步驟 9 前);
// 屆時本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時。
// 建問題模式全程不觸碰 PR 既有留言(審查內容改發到 issue)。
// ── 步驟 3:偵測 AI agent 工具並留言 ──────────────────────────────────
const tool = agents.detectTool(); const tool = agents.detectTool();
if (!tool) { if (!tool) {
log('步驟3', 'ERR', '找不到可用的 AI 工具(antigravitycodexclaude)。'); log('步驟2', 'ERR', '找不到可用的 AI 工具(antigravitycodexclaude)。');
return 1; return 1;
} }
log('步驟3', 'INF', `選用工具:${tool.name}${tool.version})。`); log('步驟2', 'INF', `選用工具:${tool.name}${tool.version})。`);
const runLink = `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`; const runLink = `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`;
await postComment( await postComment(
templates.toolComment({ templates.toolComment({
@@ -256,24 +194,17 @@ async function main() {
}), }),
); );
// ── 步驟 4:讀取 .reviewignore、整理 git diff 並留言 ─────────────────── // ── 步驟 3:讀取 .reviewignore、整理 git diff 並留言 ───────────────────
const ignores = review.loadReviewIgnore(cwd); const ignores = review.loadReviewIgnore(cwd);
const base = gitrepo.resolveMergeBase(cwd, ctx.baseRef); const base = gitrepo.resolveMergeBase(cwd, ctx.baseRef);
const allFiles = gitrepo.changedFiles(cwd, base); const allFiles = gitrepo.changedFiles(cwd, base);
const files = allFiles.filter((file) => !review.isIgnored(file, ignores)); const files = allFiles.filter((file) => !review.isIgnored(file, ignores));
const ignoredCount = allFiles.length - files.length; const ignoredCount = allFiles.length - files.length;
log('步驟4', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); log('步驟3', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`);
if (files.length === 0) { if (files.length === 0) {
// 沒有可審查的變更:保存空 findings、以 success 收場。 // 沒有可審查的變更:留言說明、保存空 findings、以 success 收場。
// 一般模式在 PR 留言告知;建問題模式靜默通過(不建 issue、PR 也不留言,暫存的情境留言捨棄)。
if (ctx.createIssue) {
log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。');
} else {
await postComment(templates.nothingToReviewComment(ignoredCount)); await postComment(templates.nothingToReviewComment(ignoredCount));
// 已成功產生本回合結果留言(無可審查變更),此時才把舊留言標為過時(本回合留言已排除)。
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
}
const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] }); const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] });
if (ctx.createIssue) { if (ctx.createIssue) {
// 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。 // 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。
@@ -288,20 +219,20 @@ async function main() {
await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows }); await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows });
await postComment(templates.diffComment(diffRows, ignoredCount)); await postComment(templates.diffComment(diffRows, ignoredCount));
// ── 步驟 5:攻擊方角色登場留言 ───────────────────────────────────────── // ── 步驟 4:攻擊方角色登場留言 ─────────────────────────────────────────
const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles')); const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'));
const attackers = attackersOf(roles); const attackers = attackersOf(roles);
const defenders = defendersOf(roles); const defenders = defendersOf(roles);
log('步驟5', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`); log('步驟4', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`);
await postComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers })); await postComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers }));
// ── 步驟 6:每個攻擊方一個 sub agent 並行分析,合併問題列表 ──────────── // ── 步驟 5:每個攻擊方一個 sub agent 並行分析,合併問題列表 ────────────
const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows });
// ── 步驟 7:防守方角色登場留言 ───────────────────────────────────────── // ── 步驟 6:防守方角色登場留言 ─────────────────────────────────────────
await postComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders })); await postComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders }));
// ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── // ── 步驟 7:防守方裁決 → 排除 → 排序 → 保存 findings ──────────────────
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
review.sortFindings(kept); review.sortFindings(kept);
const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded }); const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded });
@@ -309,92 +240,32 @@ async function main() {
// 誤判/重複的問題附加到 exclusions.json(之後與審查結果一起 commit)。 // 誤判/重複的問題附加到 exclusions.json(之後與審查結果一起 commit)。
const exclusionsChanged = review.appendExclusions({ cwd, excluded, prNumber: ctx.prNumber }); const exclusionsChanged = review.appendExclusions({ cwd, excluded, prNumber: ctx.prNumber });
// ── 步驟 8(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ─ // ── 步驟 7(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ─
const severe = kept.filter((finding) => finding.severity === '嚴重'); const severe = kept.filter((finding) => finding.severity === '嚴重');
const others = kept.filter((finding) => finding.severity !== '嚴重'); const others = kept.filter((finding) => finding.severity !== '嚴重');
log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); log('步驟7', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`);
// ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入; // ── 步驟 8:將 PR 既有留言標記為解決(本回合留言除外)───────────────────
// 無保留問題則不建 issue、PR 也完全不留言(靜默通過,暫存的情境留言捨棄)。 ──────
if (ctx.createIssue) {
if (kept.length > 0) {
// 先依保留問題挑好標籤,於建立 issue 時一次帶入(省去「先建空標籤 issue 再補掛」的多餘 API 往返);
// 標籤挑選失敗一律降級為不掛標籤,不阻斷建 issue 流程。
let labelIds = [];
try {
const labels = await gitea.listLabels(ctx);
labelIds = await review.selectLabels({
tool,
model: ctx.model,
cwd,
labels,
prTitle: ctx.prTitle,
prBody: ctx.prBody,
findings: kept,
});
} catch (err) {
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
}
await ensureIssueCreated(labelIds);
} else {
// 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。
log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。');
}
}
// ── 步驟 2(延後執行,一般模式):審查已成功產生結果,發布問題留言前才把舊留言標為過時 ─
// 延後到此可避免工具偵測/diff/攻防裁決任一失敗時舊結果先被清掉卻無新結果;
// 本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時;
// 嚴重/其他問題留言於本步驟之後才發布,同樣不受影響。
if (!ctx.createIssue) {
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
}
// ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue) // ── 步驟 9:嚴重問題逐條掛在程式碼行上留言(開發者可回覆)─────────────
if (severe.length > 0) { if (severe.length > 0) {
if (ctx.createIssue) {
await review.postSevereToIssue({ ctx, gitea, issueNumber: issue.number, severe });
} else {
await review.postSevereComments({ ctx, gitea, severe, cwd }); await review.postSevereComments({ ctx, gitea, severe, cwd });
} }
}
// ── 步驟 10:警告+建議——一般模式彙整為單一表格留言到 PR // ── 步驟 10:警告+建議彙整為單一表格留言 ──────────────────────────────
// 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ──
if (others.length > 0) { if (others.length > 0) {
if (ctx.createIssue) {
await review.postOthersToIssue({ ctx, gitea, issueNumber: issue.number, others });
} else {
await postComment(templates.othersComment(others)); await postComment(templates.othersComment(others));
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
} }
}
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue // ── 建問題模式input: create-issue):另建 issue 逐條留言問題明細 ─────
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → ensureIssueCreated),此處不再補掛。 if (ctx.createIssue) {
if (ctx.createIssue && issue) { if (kept.length > 0) {
await gitea.createIssueComment( await review.createIssueWithFindings({ ctx, gitea, tool, model: ctx.model, cwd, findings: kept });
ctx,
templates.issueLinkComment({
issueNumber: issue.number,
issueUrl: issue.html_url,
severeCount: severe.length,
otherCount: others.length,
}),
);
// 只有「嚴重」問題才讓 PR 相依於追蹤 issue(issue 關閉前無法合併,需 repo 啟用「問題相依」功能);
// 僅有警告/建議時,issue 仍建立供追蹤,但不掛相依、不阻擋 PR 合併。
if (severe.length > 0) {
try {
await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number);
log('建問題', 'INF', `有嚴重問題:已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}issue 關閉前無法合併。`);
} catch (err) {
log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}`);
}
} else { } else {
log('建問題', 'INF', `無嚴重問題(僅警告/建議):issue #${issue.number} 僅供追蹤,不阻擋 PR 合併。`); log('建問題', 'INF', '沒有保留的問題,略過建立 issue。');
} }
log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`);
} }
// ── 收尾:commit 並 pushsuccess=無嚴重問題、failure=有嚴重問題)─────── // ── 收尾:commit 並 pushsuccess=無嚴重問題、failure=有嚴重問題)───────
@@ -409,13 +280,7 @@ async function main() {
} else { } else {
log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。'); log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。');
} }
// 本輪「審查」一律以成功收場、不直接讓檢查失敗;有嚴重問題時已推出 [failure] 結果 commit return result === 'success' ? 0 : 1;
// 由它再觸發的下一輪在步驟 1 讀 commit 訊息時才回報失敗(exit 1)。如此失敗檢查落在帶有結果
// 標記的最新 head 上,與合併判定一致。(result 僅用於上方 commit 訊息的結果標記。)
if (result === 'failure') {
log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。');
}
return 0;
} }
main() main()
+1 -1
View File
@@ -47,7 +47,7 @@ const TOOLS = [
* @returns {{ name: string, buildArgs: Function, resultFrom: string, version: string } | null} * @returns {{ name: string, buildArgs: Function, resultFrom: string, version: string } | null}
* 中選工具的描述物件(TOOLS 項目加上 version 欄位);所有工具皆不可用時回傳 null。 * 中選工具的描述物件(TOOLS 項目加上 version 欄位);所有工具皆不可用時回傳 null。
* @remarks * @remarks
* 使用情境:action 主流程(步驟 3)啟動審查前呼叫一次,取得工具描述後交給 * 使用情境:action 主流程(步驟 2)啟動審查前呼叫一次,取得工具描述後交給
* runAgent 執行;若回傳 null,主流程會記 ERR 並以失敗收場(無工具即無法審查)。 * runAgent 執行;若回傳 null,主流程會記 ERR 並以失敗收場(無工具即無法審查)。
*/ */
function detectTool() { function detectTool() {
+1 -2
View File
@@ -37,8 +37,7 @@ const path = require('path');
* - repository`owner/repo` 全名(GITHUB_REPOSITORY)。 * - repository`owner/repo` 全名(GITHUB_REPOSITORY)。
* - owner / repo:自 repository 拆出的擁有者與專案名,缺值時為空字串。 * - owner / repo:自 repository 拆出的擁有者與專案名,缺值時為空字串。
* - apiBaseGitea REST API 基底網址(`<serverUrl>/api/v1`)。 * - apiBaseGitea REST API 基底網址(`<serverUrl>/api/v1`)。
* - tokenaction input `token`INPUT_TOKEN),用於 Gitea API 認證,以及 push findings/exclusions * - tokenaction input `token`INPUT_TOKEN),用於 API 認證,缺值時為空字串。
* commit 回 repo;建議為「能觸發 CI 的 PAT」(自動 token 推送不會再觸發 CI)。缺值時為空字串。
* - modelaction input `model`INPUT_MODEL,已 trim),指定 AI 模型,缺值時為空字串。 * - modelaction input `model`INPUT_MODEL,已 trim),指定 AI 模型,缺值時為空字串。
* - createIssueaction input `create-issue`INPUT_CREATE-ISSUE),是否將問題建到 * - createIssueaction input `create-issue`INPUT_CREATE-ISSUE),是否將問題建到
* 存取庫的問題追蹤(建問題模式);trim + 小寫後與字串 'true' 嚴格比對,預設 false。 * 存取庫的問題追蹤(建問題模式);trim + 小寫後與字串 'true' 嚴格比對,預設 false。
+13 -40
View File
@@ -80,7 +80,7 @@ async function listAll(ctx, apiPath) {
* @returns {Promise<object>} Gitea 使用者物件(含 `id`、`login` 等欄位, * @returns {Promise<object>} Gitea 使用者物件(含 `id`、`login` 等欄位,
* 依 Gitea API 回應而定)。 * 依 Gitea API 回應而定)。
* @throws {Error} 請求失敗(非 2xx,例如 token 無效時 401)由底層 `api` 丟出。 * @throws {Error} 請求失敗(非 2xx,例如 token 無效時 401)由底層 `api` 丟出。
* @remarks 使用情境:action 步驟 2 先查出 bot 自己的帳號, * @remarks 使用情境:action 步驟 8 先查出 bot 自己的帳號,
* 之後比對 PR 留言的作者,辨識哪些留言是本 action 先前發出的 * 之後比對 PR 留言的作者,辨識哪些留言是本 action 先前發出的
* (例如要將舊留言標註為已過時)。 * (例如要將舊留言標註為已過時)。
*/ */
@@ -99,8 +99,8 @@ function whoAmI(ctx) {
* @returns {Promise<object>} 建立成功的留言物件(含 `id`、`body`、`user` 等欄位, * @returns {Promise<object>} 建立成功的留言物件(含 `id`、`body`、`user` 等欄位,
* 依 Gitea API 回應而定)。 * 依 Gitea API 回應而定)。
* @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立 issue 後, * @remarks 使用情境:建問題模式(input: create-issue)下,
* 把工具/diff/角色情境留言與 `review.postSevereToIssue` 的嚴重問題明細留言到該 issue * `createIssueWithFindings` 建立 issue 後,逐條把 finding 明細留言到該 issue
* 另外 `createIssueComment` 也委派本函式對 `ctx.prNumber` 留言。 * 另外 `createIssueComment` 也委派本函式對 `ctx.prNumber` 留言。
*/ */
function createCommentOnIssue(ctx, issueNumber, body) { function createCommentOnIssue(ctx, issueNumber, body) {
@@ -134,9 +134,9 @@ function createIssueComment(ctx, body) {
* @returns {Promise<object[]>} 標籤物件陣列(每筆含 `id`、`name`、`color` 等欄位, * @returns {Promise<object[]>} 標籤物件陣列(每筆含 `id`、`name`、`color` 等欄位,
* 依 Gitea API 回應而定);存取庫無標籤時為空陣列。 * 依 Gitea API 回應而定);存取庫無標籤時為空陣列。
* @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 於確定有保留問題後 * @remarks 使用情境:建問題模式(input: create-issue)下,
* 先以本函式取得可用標籤,再交給 `review.selectLabels` 讓 AI 挑出適合的標籤子集合, * `createIssueWithFindings` 先以本函式取得可用標籤,再交給 `selectLabels`
* 最後於建立追蹤 issue 時(`createIssue`)一次帶入這些標籤 * 讓 AI 從中挑選適合掛在新 issue 上的標籤子集合
*/ */
function listLabels(ctx) { function listLabels(ctx) {
return listAll(ctx, `/repos/${ctx.owner}/${ctx.repo}/labels`); return listAll(ctx, `/repos/${ctx.owner}/${ctx.repo}/labels`);
@@ -156,9 +156,10 @@ function listLabels(ctx) {
* @returns {Promise<object>} 建立成功的 issue 物件(含 `number`、`title`、 * @returns {Promise<object>} 建立成功的 issue 物件(含 `number`、`title`、
* `html_url` 等欄位,依 Gitea API 回應而定)。 * `html_url` 等欄位,依 Gitea API 回應而定)。
* @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 的 `ensureIssueCreated` * @remarks 使用情境:建問題模式(input: create-issue)下,
* 以 PR 標題/描述為 issue 標題與本文,並帶入 `review.selectLabels` 事先挑好的標籤 id * `createIssueWithFindings` 以 PR 標題/描述為 issue 標題與本文、
* 呼叫本函式一次建立追蹤問題的 issue(連同標籤),之後再把審查內容逐條留言到該 issue。 * 配上 `selectLabels` 挑出的標籤 id呼叫本函式建立追蹤問題的 issue
* 再逐條把 finding 明細留言到該 issue。
*/ */
function createIssue(ctx, { title, body, labels }) { function createIssue(ctx, { title, body, labels }) {
return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues`, { return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues`, {
@@ -168,33 +169,6 @@ function createIssue(ctx, { title, body, labels }) {
}); });
} }
/**
* 建立「問題相依」關係:讓 URL 上的 issue/PR 相依於(被阻擋於)表單指定的 issue。
* 對應 endpoint`POST /repos/{owner}/{repo}/issues/{issueNumber}/dependencies`
* body 為 IssueMeta`{index, owner, repo}`)。
*
* 語義:URL 的 issue`issueNumber`)相依於 body 的 issue`dependency`)——
* 在 `dependency` 關閉前,`issueNumber` 無法合併/關閉。本 endpoint 需 repo 啟用
* 「問題相依(issue dependencies)」功能,屬版本/設定相依;未啟用或不支援時 API 會回非 2xx。
*
* @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、
* `owner`repo 擁有者)、`repo`repo 名稱)。
* @param {number|string} issueNumber - 要被阻擋的 issuePR 編號(相依方)。
* @param {number} dependency - 作為阻擋來源的 issue 編號(同一 repo)。
* @returns {Promise<object>} 建立成功的相依關係物件(依 Gitea API 回應而定)。
* @throws {Error} 請求失敗(非 2xx,例如未啟用問題相依功能)由底層 `api` 丟出,錯誤附 `status`、`data`。
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立追蹤 issue 後,
* 以本函式把「PR`ctx.prNumber`)相依於追蹤 issue」,讓 issue 完成/關閉前 PR 無法合併;
* 呼叫端以 try/catch 降級(功能未啟用時記 WRN、不阻斷流程)。
*/
function addIssueDependency(ctx, issueNumber, dependency) {
return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {
index: dependency,
owner: ctx.owner,
repo: ctx.repo,
});
}
/** /**
* 列出 PR 上的全部一般留言(自動分頁撈取,每頁 50 筆直到取完)。 * 列出 PR 上的全部一般留言(自動分頁撈取,每頁 50 筆直到取完)。
* 對應 endpoint`GET /repos/{owner}/{repo}/issues/{prNumber}/comments`。 * 對應 endpoint`GET /repos/{owner}/{repo}/issues/{prNumber}/comments`。
@@ -204,7 +178,7 @@ function addIssueDependency(ctx, issueNumber, dependency) {
* @returns {Promise<Array<object>>} 留言物件陣列(含 `id`、`body`、`user` 等欄位); * @returns {Promise<Array<object>>} 留言物件陣列(含 `id`、`body`、`user` 等欄位);
* 無留言時為空陣列。 * 無留言時為空陣列。
* @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
* @remarks 使用情境:步驟 2 重跑 review 前,先撈出 PR 全部留言並搭配 `whoAmI` * @remarks 使用情境:步驟 8 重跑 review 前,先撈出 PR 全部留言並搭配 `whoAmI`
* 比對作者,找出本 action(bot)先前發過的留言,以便編輯標註為已過時。 * 比對作者,找出本 action(bot)先前發過的留言,以便編輯標註為已過時。
*/ */
function listIssueComments(ctx) { function listIssueComments(ctx) {
@@ -263,7 +237,7 @@ function createReview(ctx, body, comments) {
* @returns {Promise<Array<object>>} review 物件陣列(含 `id`、`user`、`body` 等欄位); * @returns {Promise<Array<object>>} review 物件陣列(含 `id`、`user`、`body` 等欄位);
* 無 review 時為空陣列。 * 無 review 時為空陣列。
* @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
* @remarks 使用情境:步驟 2 重跑 review 前,先找出 PR 上既有 review, * @remarks 使用情境:步驟 8 重跑 review 前,先找出 PR 上既有 review,
* 再以 `listReviewComments` 取出其行內留言做後續解決標記。 * 再以 `listReviewComments` 取出其行內留言做後續解決標記。
*/ */
function listReviews(ctx) { function listReviews(ctx) {
@@ -306,7 +280,7 @@ function listReviewComments(ctx, reviewId) {
* @param {number|string} commentId - 要標記為已解決的行內留言 id。 * @param {number|string} commentId - 要標記為已解決的行內留言 id。
* @returns {Promise<boolean>} 標記成功回傳 `true`;任何失敗 * @returns {Promise<boolean>} 標記成功回傳 `true`;任何失敗
* (版本不支援、權限不足、留言不存在等)一律回傳 `false`,不丟出例外。 * (版本不支援、權限不足、留言不存在等)一律回傳 `false`,不丟出例外。
* @remarks 使用情境:步驟 2 嘗試把舊回合的行內留言標記為已解決;若回傳 `false` * @remarks 使用情境:步驟 8 嘗試把舊回合的行內留言標記為已解決;若回傳 `false`
* (例如目標 Gitea 版本無此 API),呼叫端應停止嘗試並記 WRN * (例如目標 Gitea 版本無此 API),呼叫端應停止嘗試並記 WRN
* (由 `resolveOldComments` 實作此降級)。 * (由 `resolveOldComments` 實作此降級)。
*/ */
@@ -329,7 +303,6 @@ module.exports = {
createCommentOnIssue, createCommentOnIssue,
listLabels, listLabels,
createIssue, createIssue,
addIssueDependency,
listIssueComments, listIssueComments,
editIssueComment, editIssueComment,
createReview, createReview,
+21 -123
View File
@@ -44,25 +44,6 @@ function gitTrim(cwd, ...args) {
return git(cwd, ...args).trim(); return git(cwd, ...args).trim();
} }
/**
* 嘗試同步執行 git 指令,失敗時回傳 false,成功時回傳 true。
*
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
* @param {...string} args - 傳給 git 的參數。
* @returns {boolean} git 指令是否成功結束。
* @remarks
* 使用情境:修復淺層 checkout 的歷史不足時,部分 fetch 策略可能因 runner
* 或遠端版本不同而失敗;呼叫端可依序嘗試多種策略,不讓第一個失敗中斷流程。
*/
function tryGit(cwd, ...args) {
try {
git(cwd, ...args);
return true;
} catch {
return false;
}
}
/** /**
* 取得目前 HEAD 最新一筆 commit 的訊息標題(commit message 第一行)。 * 取得目前 HEAD 最新一筆 commit 的訊息標題(commit message 第一行)。
* *
@@ -83,18 +64,14 @@ function latestCommitSubject(cwd) {
/** /**
* 解析 PR base 分支與目前 HEAD 的 merge-base commit SHA。 * 解析 PR base 分支與目前 HEAD 的 merge-base commit SHA。
* *
* 先以 refspec 明確更新 `origin/<baseRef>`,再以 `git merge-base origin/<baseRef> HEAD` * 先嘗試 `git fetch origin <baseRef>` 更新 base 分支資料(失敗時靜默忽略,
* 取得共同祖先。若 checkout 是淺層歷史而導致 merge-base 失敗,會依序補抓更完整的 * 因 fetch-depth: 0 的 checkout 通常已含 base 分支,可直接沿用本地資料),
* base/head 歷史;**每個補抓策略成功後立即重試 merge-base,一成功即回傳** * 再以 `git merge-base origin/<baseRef> HEAD` 取得共同祖先。
* 避免在已補到足夠歷史後仍多做無謂的 fetch 往返(例如 `--unshallow` 成功就不再 deepen)。
* 各策略採資料驅動依序執行;全部用盡仍失敗時,丟出彙整了「哪個策略成功/失敗」診斷的錯誤,
* 方便維護者判斷是哪一步補抓不足(診斷僅含策略名與成敗,不含 git 原始輸出以免洩漏遠端資訊)。
* *
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
* @param {string} baseRef - PR 目標(base)分支名稱,例如 'master' 或 'develop';不含 'origin/' 前綴。 * @param {string} baseRef - PR 目標(base)分支名稱,例如 'master' 或 'develop';不含 'origin/' 前綴。
* @returns {string} merge-base 的 commit SHA40 碼十六進位字串)。 * @returns {string} merge-base 的 commit SHA40 碼十六進位字串)。
* @throws {Error} 補抓歷史後仍無法取得共同祖先時,丟出含 baseRef 與各策略診斷的明確錯誤 * @throws {Error} 本地不存在 origin/<baseRef>、或兩者無共同祖先時,`git merge-base` 失敗並拋出(fetch 失敗不會拋出)。
* `error.cause` 保留首次 merge-base 失敗的原始錯誤)。
* @remarks * @remarks
* 使用情境:AI code review 以此結果作為 diff 比較基準—— * 使用情境:AI code review 以此結果作為 diff 比較基準——
* 先 `resolveMergeBase(cwd, pr.base.ref)` 取得基準 SHA * 先 `resolveMergeBase(cwd, pr.base.ref)` 取得基準 SHA
@@ -102,57 +79,12 @@ function latestCommitSubject(cwd) {
* 避免把 base 分支後續演進誤算進 diff。 * 避免把 base 分支後續演進誤算進 diff。
*/ */
function resolveMergeBase(cwd, baseRef) { function resolveMergeBase(cwd, baseRef) {
const remoteBase = `origin/${baseRef}`;
const diagnostics = [];
// 執行一個 fetch 策略並記錄成敗(只記策略名與成敗,不含 git 原始輸出,避免洩漏遠端資訊)。
const runFetch = (label, ...args) => {
const ok = tryGit(cwd, ...args);
diagnostics.push(`${label}${ok ? '成功' : '失敗'}`);
return ok;
};
// 每個補抓策略後重試 merge-base:成功回傳 SHA,失敗記診斷並回傳 null。
const tryMergeBase = (label) => {
try { try {
return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD'); git(cwd, 'fetch', 'origin', baseRef);
} catch { } catch {
diagnostics.push(`merge-base${label}):失敗`); // fetch-depth: 0 的 checkout 通常已含 base 分支,抓不到時直接沿用本地資料。
return null;
} }
}; return gitTrim(cwd, 'merge-base', `origin/${baseRef}`, 'HEAD');
// 先明確更新 origin/<baseRef>,再嘗試 merge-base。
runFetch(`fetch base(${baseRef})`, 'fetch', '--no-tags', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`);
let firstError;
try {
return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD');
} catch (err) {
firstError = err;
diagnostics.push('merge-base(首次):失敗');
}
// 資料驅動的補抓策略:淺層才 unshallow;其後依序 deepen base 與 HEAD。
// 每個策略成功後立即重試 merge-base,成功即回傳,避免多餘往返。
const strategies = [];
if (gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true') {
strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']);
}
strategies.push([
'deepen base',
'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`,
]);
strategies.push(['deepen HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD']);
for (const [label, ...args] of strategies) {
if (!runFetch(label, ...args)) continue; // fetch 失敗就換下一個策略。
const sha = tryMergeBase(`${label}`);
if (sha) return sha;
}
const error = new Error(
`無法解析 origin/${baseRef} 與 HEAD 的 merge-base;請確認 checkout 有足夠歷史,或設定 checkout fetch-depth: 0。診斷:${diagnostics.join('')}`,
);
error.cause = firstError;
throw error;
} }
/** /**
@@ -223,9 +155,7 @@ function fileLastUpdatedIso(cwd, file) {
* 若目前 HEAD 不在 PR head commit(例如 checkout 停在 merge commit), * 若目前 HEAD 不在 PR head commit(例如 checkout 停在 merge commit),
* 會先 `git checkout --detach <headSha>` 站上 head,避免把 merge 內容推回來源分支。 * 會先 `git checkout --detach <headSha>` 站上 head,避免把 merge 內容推回來源分支。
* commit 以 `-c` 臨時覆寫 user.name / user.email,不改動 repo 的 git 設定。 * commit 以 `-c` 臨時覆寫 user.name / user.email,不改動 repo 的 git 設定。
* push 策略:一律以 `token` 的身分明確認證推送({@link pushWithCredential},不走 runner 的 * push 先走 origin;失敗(遠端未帶認證)時改用帶 token 的 URL 重試。
* origin 自動 token)——origin 帶的自動 tokengitea.token / GITHUB_TOKEN)推送不會再觸發 CI
* 改以呼叫端提供的 `token`(建議為 PAT)身分推送,才會讓 PR 的 synchronize 事件再觸發 CI。
* *
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
* @param {object} options - 提交與推送設定。 * @param {object} options - 提交與推送設定。
@@ -233,7 +163,7 @@ function fileLastUpdatedIso(cwd, file) {
* @param {string} [options.headSha] - PR head 的 commit SHA;有提供且與目前 HEAD 不同時會先 detach 到此 commit。可省略(falsy 時不 detach,直接於目前 HEAD 上 commit)。 * @param {string} [options.headSha] - PR head 的 commit SHA;有提供且與目前 HEAD 不同時會先 detach 到此 commit。可省略(falsy 時不 detach,直接於目前 HEAD 上 commit)。
* @param {string} options.message - commit 訊息。 * @param {string} options.message - commit 訊息。
* @param {string[]} options.files - 要加入 commit 的檔案路徑清單(相對 repo 根目錄);全數無實際變更時不 commit、回傳 false。 * @param {string[]} options.files - 要加入 commit 的檔案路徑清單(相對 repo 根目錄);全數無實際變更時不 commit、回傳 false。
* @param {string} options.token - 具該 repo push 權限的 Gitea access token(建議為能觸發 CI 的 PAT);用於 findings commit 的認證推送 * @param {string} options.token - 具該 repo push 權限的 Gitea access token;僅在 origin push 失敗時用於組出帶認證的重試 URL
* @param {string} options.serverUrl - Gitea 伺服器根網址(例如 https://gitea.example.com),須為合法 URL。 * @param {string} options.serverUrl - Gitea 伺服器根網址(例如 https://gitea.example.com),須為合法 URL。
* @param {string} options.repository - repo 完整名稱(owner/repo 格式),與 serverUrl 組成 clone URL。 * @param {string} options.repository - repo 完整名稱(owner/repo 格式),與 serverUrl 組成 clone URL。
* @returns {boolean} true=有變更且已 commit 並 push 到來源分支;false=暫存區與 HEAD 無差異,略過 commit/push。 * @returns {boolean} true=有變更且已 commit 並 push 到來源分支;false=暫存區與 HEAD 無差異,略過 commit/push。
@@ -243,10 +173,10 @@ function fileLastUpdatedIso(cwd, file) {
* findings 檔與 `.gitea/ai-review/exclusions.json` 等結果檔提交回 PR 來源分支, * findings 檔與 `.gitea/ai-review/exclusions.json` 等結果檔提交回 PR 來源分支,
* 並依回傳值記錄「已 commit/push」或「無實際變更、略過」的不同日誌。 * 並依回傳值記錄「已 commit/push」或「無實際變更、略過」的不同日誌。
* *
* 安全注意:帶認證的推送一律透過 {@link pushWithCredential} 進行——認證只以 * 安全注意:push 重試時組出的 URL 內含 token(形如
* 環境變數(`http.<url>.extraheader` 的 base64 Basic)傳入,**不進 argv** * `https://ai-review-bot:<token>@host/owner/repo.git`
* 推送目標 URL 本身不含帳密;且 push 失敗時改拋固定訊息,避免 `execFileSync` * 絕對不得將此 URL 輸出到日誌、錯誤訊息或任何 action 輸出,以免洩漏 token
* 例外把命令列(含 token)回顯到 CI log 或程序清單 * 若需記錄重試行為,只能記載「改用帶認證 URL 重試」而不得包含 URL 本身
*/ */
function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, serverUrl, repository }) { function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, serverUrl, repository }) {
const current = gitTrim(cwd, 'rev-parse', 'HEAD'); const current = gitTrim(cwd, 'rev-parse', 'HEAD');
@@ -266,49 +196,17 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
'-c', 'user.email=ai-review-bot@noreply.gitea', '-c', 'user.email=ai-review-bot@noreply.gitea',
'commit', '-m', message, 'commit', '-m', message,
); );
const refspec = `HEAD:refs/heads/${headRef}`;
const remoteUrl = `${serverUrl}/${repository}.git`;
// 一律以 token 的身分明確認證推送(不走 origin 的自動 token)——只要 token 是能觸發 CI 的 PAT
// 結果 commit 就會讓 PR 的 synchronize 事件再觸發 CI,由步驟 1 快速回報把結果蓋到新 head。
pushWithCredential(cwd, remoteUrl, token, refspec);
return true;
}
/**
* 以帶認證的方式推送到指定遠端,認證資訊只經環境變數傳入、不進命令列 argv。
*
* 認證方式:等同 `https://ai-review-bot:<secret>@host/...` 的 HTTP Basicgit 會把
* URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的
* `GIT_CONFIG_*` 環境變數注入 `http.<url>.extraheader`,使 base64 憑證**不出現在 argv**
* (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。
* 推送失敗時**不重拋原始例外**(其 message 會含命令列與遠端 URL),改拋固定訊息。
*
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
* @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。
* @param {string} secret - 具 push 權限的 tokenPAT(作為 Basic 認證的密碼)。
* @param {string} refspec - push 的 refspec(形如 `HEAD:refs/heads/<branch>`)。
* @returns {void} 成功即返回;失敗拋出不含 URL/argv/token 的固定錯誤。
* @throws {Error} 推送失敗時拋出固定訊息(已隱藏遠端 URL 與認證資訊)。
* @remarks 本函式未匯出,僅供 {@link commitAndPushFindings} 使用。
*/
function pushWithCredential(cwd, remoteUrl, secret, refspec) {
const basic = Buffer.from(`ai-review-bot:${secret}`).toString('base64');
try { try {
execFileSync('git', ['push', remoteUrl, refspec], { git(cwd, 'push', 'origin', `HEAD:refs/heads/${headRef}`);
cwd,
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
env: {
...process.env,
GIT_TERMINAL_PROMPT: '0',
GIT_CONFIG_COUNT: '1',
GIT_CONFIG_KEY_0: `http.${remoteUrl}.extraheader`,
GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`,
},
});
} catch { } catch {
throw new Error('推送審查結果 commit 失敗(已隱藏遠端 URL 與認證資訊)。'); // 遠端未帶認證(checkout 未保留 credentials)時,改用帶 token 的 URL 重試。
// 注意:不得把這個 URL 輸出到日誌,避免洩漏 token。
const url = new URL(`${serverUrl}/${repository}.git`);
url.username = 'ai-review-bot';
url.password = token;
git(cwd, 'push', url.toString(), `HEAD:refs/heads/${headRef}`);
} }
return true;
} }
module.exports = { module.exports = {
+122 -163
View File
@@ -13,72 +13,6 @@ const templates = require('./templates');
const PER_FILE_DIFF_LIMIT = 16_000; const PER_FILE_DIFF_LIMIT = 16_000;
const TOTAL_DIFF_LIMIT = 160_000; const TOTAL_DIFF_LIMIT = 160_000;
/**
* 遮罩單行診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。
*
* 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽
* `Authorization` 標頭、`token=``token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/
* 長 hex`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出
* CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。
*
* @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。
* @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。
* @remarks 本函式未匯出,僅供模組內部使用。
*/
function redactSecrets(text) {
return String(text ?? '')
.replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ')
.replace(/(authorization\s*[:=]\s*)\S+/gi, '$1***')
.replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***')
.replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@')
.replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***')
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***')
.trim();
}
/**
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
*
* 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或
* 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。權衡「可除錯性」後:本函式
* 於失敗時**預設**附上經 {@link redactSecrets} 遮罩且去除控制字元的 **stderr 與 stdout**
* 片段(各先截去過長輸入再取前 500 字)——只印 exit code 幾乎無從判斷 CLI 為何失敗,
* 且部分 CLI(如 claude-code 的 `-p` 模式)將錯誤寫到 stdout 而非 stderr。純函式、不拋例外。
*
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
* `runAgent` 的回傳物件。
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
* @remarks
* 使用情境:{@link runAttackers}{@link runDefenders}{@link fillPurposes}{@link selectLabels}
* 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗;
* 需要原始輸出診斷時,於 workflow 設定 secret `ACTIONS_STEP_DEBUG=true` 再重跑。
* 本函式未匯出,僅供模組內部使用。
*/
function agentFailureDetail(res) {
const parts = [];
const err = res && res.error;
if (err) {
if (err.killed) parts.push('已逾時終止');
if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
else if (err.code) parts.push(`code ${err.code}`);
else if (err.signal) parts.push(`signal ${err.signal}`);
}
// 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描;
// 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。
const INPUT_LIMIT = 2_000;
// 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 與 stdout 片段——CLI 失敗時
// 只印 exit code 幾乎無從除錯(見 test-claude 秒失敗案例);且部分 CLI(如 claude-code 的
// -p 模式)會把錯誤寫到 stdout 而非 stderr,故兩者都輸出。redactSecrets 為盡力防線。
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT));
if (stderr) parts.push(`stderr${stderr.slice(0, 500)}`);
const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT));
if (stdout) parts.push(`stdout${stdout.slice(0, 500)}`);
if (parts.length === 0) {
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
}
return parts.join('');
}
/** /**
* 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。 * 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。
* *
@@ -88,7 +22,7 @@ function agentFailureDetail(res) {
* @param {string} workspace - 工作目錄絕對路徑(`.reviewignore` 所在的 repo 根目錄)。 * @param {string} workspace - 工作目錄絕對路徑(`.reviewignore` 所在的 repo 根目錄)。
* @returns {string[]} 忽略用的路徑前綴陣列;檔案不存在時為空陣列。 * @returns {string[]} 忽略用的路徑前綴陣列;檔案不存在時為空陣列。
* @remarks * @remarks
* 使用情境:審查流程「步驟 4」開頭由 `src/index.js` 呼叫, * 使用情境:審查流程「步驟 3」開頭由 `src/index.js` 呼叫,
* 取得前綴清單後搭配 {@link isIgnored} 過濾 `gitrepo.changedFiles` 的結果, * 取得前綴清單後搭配 {@link isIgnored} 過濾 `gitrepo.changedFiles` 的結果,
* 決定哪些變更檔案要納入送審。 * 決定哪些變更檔案要納入送審。
*/ */
@@ -112,7 +46,7 @@ function loadReviewIgnore(workspace) {
* @param {string[]} prefixes - 忽略路徑前綴清單(通常來自 {@link loadReviewIgnore})。 * @param {string[]} prefixes - 忽略路徑前綴清單(通常來自 {@link loadReviewIgnore})。
* @returns {boolean} `true` 表示忽略、不納入審查;`false` 表示送審。 * @returns {boolean} `true` 表示忽略、不納入審查;`false` 表示送審。
* @remarks * @remarks
* 使用情境:審查流程「步驟 4」中,`src/index.js` 以 * 使用情境:審查流程「步驟 3」中,`src/index.js` 以
* `allFiles.filter((file) => !review.isIgnored(file, ignores))` * `allFiles.filter((file) => !review.isIgnored(file, ignores))`
* 過濾變更檔案清單,被排除的檔案數量會反映在變更摘要留言的排除統計。 * 過濾變更檔案清單,被排除的檔案數量會反映在變更摘要留言的排除統計。
*/ */
@@ -136,7 +70,7 @@ function isIgnored(file, prefixes) {
* @returns {Array<{file: string, purpose: string, lines: number, chars: number, truncated: boolean, lastUpdated: string, diffForPrompt: string}>} * @returns {Array<{file: string, purpose: string, lines: number, chars: number, truncated: boolean, lastUpdated: string, diffForPrompt: string}>}
* 每檔一列的 diff 資料列;`purpose` 初始為「—」,由 {@link fillPurposes} 補齊。 * 每檔一列的 diff 資料列;`purpose` 初始為「—」,由 {@link fillPurposes} 補齊。
* @remarks * @remarks
* 使用情境:審查流程「步驟 4」由 `src/index.js` 呼叫,產出的 rows 同時餵給 * 使用情境:審查流程「步驟 3」由 `src/index.js` 呼叫,產出的 rows 同時餵給
* {@link fillPurposes}(補用途)、`templates.diffComment`(變更摘要留言)與 * {@link fillPurposes}(補用途)、`templates.diffComment`(變更摘要留言)與
* {@link buildAttackPrompt}(攻擊方提示的變更內容區塊)。 * {@link buildAttackPrompt}(攻擊方提示的變更內容區塊)。
*/ */
@@ -152,12 +86,12 @@ function collectDiffRows({ cwd, files, base, gitrepo }) {
if (diffForPrompt.length > PER_FILE_DIFF_LIMIT) { if (diffForPrompt.length > PER_FILE_DIFF_LIMIT) {
diffForPrompt = `${diffForPrompt.slice(0, PER_FILE_DIFF_LIMIT)}\n...(diff 過長,其餘截斷未送審)`; diffForPrompt = `${diffForPrompt.slice(0, PER_FILE_DIFF_LIMIT)}\n...(diff 過長,其餘截斷未送審)`;
truncated = true; truncated = true;
log('步驟4', 'WRN', `${file} 的 diff 超過單檔上限(${chars} 字元),已截斷送審。`); log('步驟3', 'WRN', `${file} 的 diff 超過單檔上限(${chars} 字元),已截斷送審。`);
} }
if (totalChars + diffForPrompt.length > TOTAL_DIFF_LIMIT) { if (totalChars + diffForPrompt.length > TOTAL_DIFF_LIMIT) {
diffForPrompt = '(全部 diff 總量超過送審上限,本檔內容未送審,僅列出檔名)'; diffForPrompt = '(全部 diff 總量超過送審上限,本檔內容未送審,僅列出檔名)';
truncated = true; truncated = true;
log('步驟4', 'WRN', `${file} 因總量上限未送審 diff 內容。`); log('步驟3', 'WRN', `${file} 因總量上限未送審 diff 內容。`);
} else { } else {
totalChars += diffForPrompt.length; totalChars += diffForPrompt.length;
} }
@@ -187,7 +121,7 @@ function collectDiffRows({ cwd, files, base, gitrepo }) {
* @param {Array<Object>} params.diffRows - {@link collectDiffRows} 產出的資料列;本函式會就地更新其 `purpose` 欄位。 * @param {Array<Object>} params.diffRows - {@link collectDiffRows} 產出的資料列;本函式會就地更新其 `purpose` 欄位。
* @returns {Promise<void>} 無回傳值;結果反映在 `diffRows` 的 `purpose` 欄位。 * @returns {Promise<void>} 無回傳值;結果反映在 `diffRows` 的 `purpose` 欄位。
* @remarks * @remarks
* 使用情境:審查流程「步驟 4」在 `collectDiffRows` 之後、發布 * 使用情境:審查流程「步驟 3」在 `collectDiffRows` 之後、發布
* `templates.diffComment` 變更摘要留言之前呼叫,讓摘要表格的「用途」欄有內容。 * `templates.diffComment` 變更摘要留言之前呼叫,讓摘要表格的「用途」欄有內容。
*/ */
async function fillPurposes({ tool, model, cwd, diffRows }) { async function fillPurposes({ tool, model, cwd, diffRows }) {
@@ -206,12 +140,12 @@ ${sections}
- 不得輸出個資(PII)。`; - 不得輸出個資(PII)。`;
const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 }); const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 });
if (!res.ok) { if (!res.ok) {
log('步驟4', 'WRN', `檔案用途摘要產生失敗,以「—」代替${agentFailureDetail(res)}`); log('步驟3', 'WRN', '檔案用途摘要產生失敗,以「—」代替。');
return; return;
} }
const parsed = extractJson(res.output); const parsed = extractJson(res.output);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
log('步驟4', 'WRN', '檔案用途摘要回覆無法解析,以「—」代替。'); log('步驟3', 'WRN', '檔案用途摘要回覆無法解析,以「—」代替。');
return; return;
} }
for (const row of diffRows) { for (const row of diffRows) {
@@ -229,7 +163,7 @@ ${sections}
* @param {*} value - 攻擊方回覆的 severity 原始值(可能是任何型別;非字串會先轉字串)。 * @param {*} value - 攻擊方回覆的 severity 原始值(可能是任何型別;非字串會先轉字串)。
* @returns {'嚴重'|'警告'|'建議'} 收斂後的等級字串。 * @returns {'嚴重'|'警告'|'建議'} 收斂後的等級字串。
* @remarks * @remarks
* 使用情境:審查流程「步驟 6」中 {@link normalizeFinding} 檢核每條 finding 時呼叫, * 使用情境:審查流程「步驟 5」中 {@link normalizeFinding} 檢核每條 finding 時呼叫,
* 確保後續 {@link sortFindings} 的 `templates.SEVERITY_ORDER` 排序、 * 確保後續 {@link sortFindings} 的 `templates.SEVERITY_ORDER` 排序、
* 「嚴重」分組(步驟 9 逐條留言 vs 步驟 10 彙整表格)都能以固定用詞比對。 * 「嚴重」分組(步驟 9 逐條留言 vs 步驟 10 彙整表格)都能以固定用詞比對。
* 本函式未匯出,僅供模組內部使用。 * 本函式未匯出,僅供模組內部使用。
@@ -254,7 +188,7 @@ function normalizeSeverity(value) {
* @param {Array<Object>} diffRows - {@link collectDiffRows} 產出的送審資料列(filepurposelastUpdateddiffForPrompt)。 * @param {Array<Object>} diffRows - {@link collectDiffRows} 產出的送審資料列(filepurposelastUpdateddiffForPrompt)。
* @returns {string} 可直接餵給 `runAgent` stdin 的完整提示字串。 * @returns {string} 可直接餵給 `runAgent` stdin 的完整提示字串。
* @remarks * @remarks
* 使用情境:審查流程「步驟 6」{@link runAttackers} 為每個攻擊方角色各組一份提示, * 使用情境:審查流程「步驟 5」{@link runAttackers} 為每個攻擊方角色各組一份提示,
* 並行送入 sub agent 找問題。本函式未匯出,僅供模組內部使用。 * 並行送入 sub agent 找問題。本函式未匯出,僅供模組內部使用。
*/ */
function buildAttackPrompt(role, diffRows) { function buildAttackPrompt(role, diffRows) {
@@ -299,7 +233,7 @@ ${sections}
* @returns {?{reviewer: string, focus: string, badge: string, severity: string, file: string, startLine: number, endLine: number, problem: string, suggestion: string, suggestedCode: string}} * @returns {?{reviewer: string, focus: string, badge: string, severity: string, file: string, startLine: number, endLine: number, problem: string, suggestion: string, suggestedCode: string}}
* 標準化後的 finding;輸入不合格時為 `null`。 * 標準化後的 finding;輸入不合格時為 `null`。
* @remarks * @remarks
* 使用情境:審查流程「步驟 6」{@link runAttackers} 解析每個攻擊方的 JSON 回覆後, * 使用情境:審查流程「步驟 5」{@link runAttackers} 解析每個攻擊方的 JSON 回覆後,
* 逐條經本函式檢核,通過者才進入合併列表並編派 id,供防守方裁決與留言使用。 * 逐條經本函式檢核,通過者才進入合併列表並編派 id,供防守方裁決與留言使用。
* 本函式未匯出,僅供模組內部使用。 * 本函式未匯出,僅供模組內部使用。
*/ */
@@ -322,7 +256,7 @@ function normalizeFinding(fromAgent, role) {
} }
/** /**
* 步驟 6:每個攻擊方角色一個 sub agent 並行分析送審 diff,合併為單一問題列表並編派 id。 * 步驟 5:每個攻擊方角色一個 sub agent 並行分析送審 diff,合併為單一問題列表並編派 id。
* *
* 單一角色失敗(執行失敗或回覆無法解析為 JSON 陣列)只記 WRN 並以空結果代替, * 單一角色失敗(執行失敗或回覆無法解析為 JSON 陣列)只記 WRN 並以空結果代替,
* 不阻斷其他角色(失敗降級行為);每條回覆先經 {@link normalizeFinding} 檢核, * 不阻斷其他角色(失敗降級行為);每條回覆先經 {@link normalizeFinding} 檢核,
@@ -336,25 +270,25 @@ function normalizeFinding(fromAgent, role) {
* @param {Array<Object>} params.diffRows - {@link collectDiffRows} 產出的送審資料列。 * @param {Array<Object>} params.diffRows - {@link collectDiffRows} 產出的送審資料列。
* @returns {Promise<Array<Object>>} 合併後的標準化 finding 列表(每條含 `id`);全部失敗或無問題時為空陣列。 * @returns {Promise<Array<Object>>} 合併後的標準化 finding 列表(每條含 `id`);全部失敗或無問題時為空陣列。
* @remarks * @remarks
* 使用情境:審查流程「步驟 6」由 `src/index.js` 在攻擊方登場留言後呼叫, * 使用情境:審查流程「步驟 5」由 `src/index.js` 在攻擊方登場留言後呼叫,
* 結果直接交給步驟 8 的 {@link runDefenders} 裁決。 * 結果直接交給步驟 7 的 {@link runDefenders} 裁決。
*/ */
async function runAttackers({ tool, model, cwd, attackers, diffRows }) { async function runAttackers({ tool, model, cwd, attackers, diffRows }) {
const results = await Promise.all( const results = await Promise.all(
attackers.map(async (role) => { attackers.map(async (role) => {
log('步驟6', 'INF', `攻擊方 ${role.meta.name} 開始分析。`); log('步驟5', 'INF', `攻擊方 ${role.meta.name} 開始分析。`);
const res = await runAgent(tool, { model, prompt: buildAttackPrompt(role, diffRows), cwd }); const res = await runAgent(tool, { model, prompt: buildAttackPrompt(role, diffRows), cwd });
if (!res.ok) { if (!res.ok) {
log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${agentFailureDetail(res)}`); log('步驟5', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${(res.error && res.error.message) || '未知錯誤'}`);
return []; return [];
} }
const parsed = extractJson(res.output); const parsed = extractJson(res.output);
if (!Array.isArray(parsed)) { if (!Array.isArray(parsed)) {
log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 回覆無法解析為 JSON 陣列,略過該角色結果。`); log('步驟5', 'WRN', `攻擊方 ${role.meta.name} 回覆無法解析為 JSON 陣列,略過該角色結果。`);
return []; return [];
} }
const list = parsed.map((f) => normalizeFinding(f, role)).filter(Boolean); const list = parsed.map((f) => normalizeFinding(f, role)).filter(Boolean);
log('步驟6', 'INF', `攻擊方 ${role.meta.name} 完成:${list.length} 條問題。`); log('步驟5', 'INF', `攻擊方 ${role.meta.name} 完成:${list.length} 條問題。`);
return list; return list;
}), }),
); );
@@ -362,7 +296,7 @@ async function runAttackers({ tool, model, cwd, attackers, diffRows }) {
merged.forEach((finding, index) => { merged.forEach((finding, index) => {
finding.id = `F${String(index + 1).padStart(3, '0')}`; finding.id = `F${String(index + 1).padStart(3, '0')}`;
}); });
log('步驟6', 'INF', `全部攻擊方完成,合併後共 ${merged.length} 條問題。`); log('步驟5', 'INF', `全部攻擊方完成,合併後共 ${merged.length} 條問題。`);
return merged; return merged;
} }
@@ -375,7 +309,7 @@ async function runAttackers({ tool, model, cwd, attackers, diffRows }) {
* @param {number} limit - 保留的最大字元數(超過即截斷)。 * @param {number} limit - 保留的最大字元數(超過即截斷)。
* @returns {string} 截斷後的檔案內容;檔案不存在時為空字串。 * @returns {string} 截斷後的檔案內容;檔案不存在時為空字串。
* @remarks * @remarks
* 使用情境:審查流程「步驟 8」{@link runDefenders} 以 * 使用情境:審查流程「步驟 7」{@link runDefenders} 以
* `readCapped(<cwd>/.gitea/ai-review/exclusions.json, 20_000)` * `readCapped(<cwd>/.gitea/ai-review/exclusions.json, 20_000)`
* 讀取已知排除事項,嵌入 {@link buildDefendPrompt} 的防守方提示, * 讀取已知排除事項,嵌入 {@link buildDefendPrompt} 的防守方提示,
* 避免排除清單過長撐爆提示。本函式未匯出,僅供模組內部使用。 * 避免排除清單過長撐爆提示。本函式未匯出,僅供模組內部使用。
@@ -397,7 +331,7 @@ function readCapped(filePath, limit) {
* @param {string} cwd - 工作目錄(repo 根目錄,findings 目錄位於其下 `.gitea/ai-review/findings`)。 * @param {string} cwd - 工作目錄(repo 根目錄,findings 目錄位於其下 `.gitea/ai-review/findings`)。
* @returns {string} 歷史 findings 摘要文字(Markdown 區段 + JSON);無歷史時為空字串。 * @returns {string} 歷史 findings 摘要文字(Markdown 區段 + JSON);無歷史時為空字串。
* @remarks * @remarks
* 使用情境:審查流程「步驟 8」{@link runDefenders} 呼叫本函式取得歷史摘要, * 使用情境:審查流程「步驟 7」{@link runDefenders} 呼叫本函式取得歷史摘要,
* 嵌入 {@link buildDefendPrompt},讓防守方能以「與歷史 findings 重複」為由裁決排除。 * 嵌入 {@link buildDefendPrompt},讓防守方能以「與歷史 findings 重複」為由裁決排除。
* 本函式未匯出,僅供模組內部使用。 * 本函式未匯出,僅供模組內部使用。
*/ */
@@ -445,7 +379,7 @@ function loadHistory(cwd) {
* @param {string} historyText - {@link loadHistory} 產出的歷史 findings 摘要;空字串時提示顯示「(無)」。 * @param {string} historyText - {@link loadHistory} 產出的歷史 findings 摘要;空字串時提示顯示「(無)」。
* @returns {string} 可直接餵給 `runAgent` stdin 的完整裁決提示字串。 * @returns {string} 可直接餵給 `runAgent` stdin 的完整裁決提示字串。
* @remarks * @remarks
* 使用情境:審查流程「步驟 8」{@link runDefenders} 為每個防守方角色各組一份提示, * 使用情境:審查流程「步驟 7」{@link runDefenders} 為每個防守方角色各組一份提示,
* 並行送入 sub agent 逐條裁決是否可排除(重複或誤判)。本函式未匯出,僅供模組內部使用。 * 並行送入 sub agent 逐條裁決是否可排除(重複或誤判)。本函式未匯出,僅供模組內部使用。
*/ */
function buildDefendPrompt(role, findings, exclusionsText, historyText) { function buildDefendPrompt(role, findings, exclusionsText, historyText) {
@@ -490,7 +424,7 @@ ${JSON.stringify(minimal, null, 2)}
} }
/** /**
* 步驟 8:每個防守方角色一個 sub agent 並行裁決 findings * 步驟 7:每個防守方角色一個 sub agent 並行裁決 findings
* 「全部防守方都判可排除」才移除該條,其餘一律保留(保守原則)。 * 「全部防守方都判可排除」才移除該條,其餘一律保留(保守原則)。
* *
* 失敗降級:某防守方執行失敗或回覆無法解析 → 該角色視為全部保留; * 失敗降級:某防守方執行失敗或回覆無法解析 → 該角色視為全部保留;
@@ -507,7 +441,7 @@ ${JSON.stringify(minimal, null, 2)}
* `kept`=保留(至少一位防守方不同意排除)、`excluded`=移除(全數防守方判可排除); * `kept`=保留(至少一位防守方不同意排除)、`excluded`=移除(全數防守方判可排除);
* 兩邊元素都已附 `verdicts`。 * 兩邊元素都已附 `verdicts`。
* @remarks * @remarks
* 使用情境:審查流程「步驟 8」由 `src/index.js` 呼叫;`kept` 隨後經 * 使用情境:審查流程「步驟 7」由 `src/index.js` 呼叫;`kept` 隨後經
* {@link sortFindings} 排序、依「嚴重」分組發留言(步驟 9/10), * {@link sortFindings} 排序、依「嚴重」分組發留言(步驟 9/10),
* `kept` 與 `excluded` 一併保存進 `.gitea/ai-review/findings/*.json`。 * `kept` 與 `excluded` 一併保存進 `.gitea/ai-review/findings/*.json`。
*/ */
@@ -517,7 +451,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) {
const historyText = loadHistory(cwd); const historyText = loadHistory(cwd);
const verdictsPerDefender = await Promise.all( const verdictsPerDefender = await Promise.all(
defenders.map(async (role) => { defenders.map(async (role) => {
log('步驟8', 'INF', `防守方 ${role.meta.name} 開始裁決。`); log('步驟7', 'INF', `防守方 ${role.meta.name} 開始裁決。`);
const res = await runAgent(tool, { const res = await runAgent(tool, {
model, model,
prompt: buildDefendPrompt(role, findings, exclusionsText, historyText), prompt: buildDefendPrompt(role, findings, exclusionsText, historyText),
@@ -525,7 +459,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) {
}); });
const verdicts = new Map(); const verdicts = new Map();
if (!res.ok) { if (!res.ok) {
log('步驟8', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留${agentFailureDetail(res)}`); log('步驟7', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留。`);
return { role: role.meta.name, verdicts }; return { role: role.meta.name, verdicts };
} }
const parsed = extractJson(res.output); const parsed = extractJson(res.output);
@@ -539,9 +473,9 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) {
} }
} }
} else { } else {
log('步驟8', 'WRN', `防守方 ${role.meta.name} 回覆無法解析,該角色視為全部保留。`); log('步驟7', 'WRN', `防守方 ${role.meta.name} 回覆無法解析,該角色視為全部保留。`);
} }
log('步驟8', 'INF', `防守方 ${role.meta.name} 完成裁決。`); log('步驟7', 'INF', `防守方 ${role.meta.name} 完成裁決。`);
return { role: role.meta.name, verdicts }; return { role: role.meta.name, verdicts };
}), }),
); );
@@ -559,7 +493,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) {
finding.verdicts = verdicts; finding.verdicts = verdicts;
(allExclude ? excluded : kept).push(finding); (allExclude ? excluded : kept).push(finding);
} }
log('步驟8', 'INF', `裁決完成:保留 ${kept.length} 條、排除 ${excluded.length} 條。`); log('步驟7', 'INF', `裁決完成:保留 ${kept.length} 條、排除 ${excluded.length} 條。`);
return { kept, excluded }; return { kept, excluded };
} }
@@ -580,7 +514,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) {
* false=無排除問題、或既有檔案壞損/非陣列而略過寫入。 * false=無排除問題、或既有檔案壞損/非陣列而略過寫入。
* @throws {Error} 檔案系統寫入失敗(如權限不足)時由 fs 拋出,未攔截。 * @throws {Error} 檔案系統寫入失敗(如權限不足)時由 fs 拋出,未攔截。
* @remarks * @remarks
* 使用情境:`main()`src/index.js)於步驟 8 防守方裁決後呼叫本函式, * 使用情境:`main()`src/index.js)於步驟 7 防守方裁決後呼叫本函式,
* 並以回傳值決定收尾時是否把 exclusions.json 一併 commit * 並以回傳值決定收尾時是否把 exclusions.json 一併 commit
* (一般模式:findingsexclusions.json;建問題模式:只 commit exclusions.json)。 * (一般模式:findingsexclusions.json;建問題模式:只 commit exclusions.json)。
*/ */
@@ -593,11 +527,11 @@ function appendExclusions({ cwd, excluded, prNumber }) {
try { try {
entries = JSON.parse(fs.readFileSync(filePath, 'utf8')); entries = JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch { } catch {
log('步驟8', 'WRN', 'exclusions.json 無法解析,為避免破壞既有內容不附加誤判紀錄(需人工確認)。'); log('步驟7', 'WRN', 'exclusions.json 無法解析,為避免破壞既有內容不附加誤判紀錄(需人工確認)。');
return false; return false;
} }
if (!Array.isArray(entries)) { if (!Array.isArray(entries)) {
log('步驟8', 'WRN', 'exclusions.json 非 JSON 陣列,為避免破壞既有內容不附加誤判紀錄(需人工確認)。'); log('步驟7', 'WRN', 'exclusions.json 非 JSON 陣列,為避免破壞既有內容不附加誤判紀錄(需人工確認)。');
return false; return false;
} }
} }
@@ -618,7 +552,7 @@ function appendExclusions({ cwd, excluded, prNumber }) {
} }
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(entries, null, 2)}\n`, 'utf8'); fs.writeFileSync(filePath, `${JSON.stringify(entries, null, 2)}\n`, 'utf8');
log('步驟8', 'INF', `已將 ${excluded.length} 條誤判/重複問題附加到 exclusions.json。`); log('步驟7', 'INF', `已將 ${excluded.length} 條誤判/重複問題附加到 exclusions.json。`);
return true; return true;
} }
@@ -631,7 +565,7 @@ function appendExclusions({ cwd, excluded, prNumber }) {
* @param {Array<{severity: string, file: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為 {@link runDefenders} 回傳的 `kept`)。 * @param {Array<{severity: string, file: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為 {@link runDefenders} 回傳的 `kept`)。
* @returns {void} 無回傳值;排序結果反映在傳入陣列本身。 * @returns {void} 無回傳值;排序結果反映在傳入陣列本身。
* @remarks * @remarks
* 使用情境:審查流程「步驟 8」裁決完成後、保存 findings 與分組發留言之前, * 使用情境:審查流程「步驟 7」裁決完成後、保存 findings 與分組發留言之前,
* `src/index.js` 對 `kept` 呼叫本函式,確保步驟 9 逐條留言與步驟 10 彙整表格 * `src/index.js` 對 `kept` 呼叫本函式,確保步驟 9 逐條留言與步驟 10 彙整表格
* 都以「嚴重度優先、同檔集中、行號遞增」的穩定順序呈現。 * 都以「嚴重度優先、同檔集中、行號遞增」的穩定順序呈現。
*/ */
@@ -644,6 +578,30 @@ function sortFindings(findings) {
); );
} }
/**
* 建問題模式的就地排序:依檔案路徑、再依嚴重等級(嚴重→警告→建議)、再依起始行遞增。
*
* 與 {@link sortFindings}(嚴重度優先)不同,本排序以檔案路徑為第一鍵,
* 讓 issue 上逐條留言的問題「同檔集中」,便於開發者逐檔處理。
* 嚴重等級權重取自 `templates.SEVERITY_ORDER`;未知等級排最後。
* 注意:直接修改傳入陣列(in-place),無回傳值。
*
* @param {Array<{file: string, severity: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為保留問題 `kept` 的複本)。
* @returns {void} 無回傳值;排序結果反映在傳入陣列本身。
* @remarks
* 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings}
* 先以 `[...findings]` 複製保留問題(不動原陣列的嚴重度排序),
* 再對複本呼叫本函式,依「檔案→嚴重度→行號」的順序逐條留言到新 issue。
*/
function sortFindingsForIssue(findings) {
findings.sort(
(a, b) =>
a.file.localeCompare(b.file) ||
(templates.SEVERITY_ORDER[a.severity] ?? 9) - (templates.SEVERITY_ORDER[b.severity] ?? 9) ||
a.startLine - b.startLine,
);
}
/** /**
* 建問題模式:以 AI 依 PR 標題/描述與問題列表摘要, * 建問題模式:以 AI 依 PR 標題/描述與問題列表摘要,
* 從存取庫可用標籤中挑選適合掛在追蹤 issue 上的標籤子集合。 * 從存取庫可用標籤中挑選適合掛在追蹤 issue 上的標籤子集合。
@@ -662,9 +620,9 @@ function sortFindings(findings) {
* @param {Array<Object>} params.findings - 保留的問題列表;每條取 severityfocusfile 與截斷 120 字的 problem 作為挑選依據。 * @param {Array<Object>} params.findings - 保留的問題列表;每條取 severityfocusfile 與截斷 120 字的 problem 作為挑選依據。
* @returns {Promise<number[]>} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。 * @returns {Promise<number[]>} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。
* @remarks * @remarks
* 使用情境:建問題模式(input: create-issue)下,`main()`src/index.js * 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings}
* 於確定有保留問題後、建立追蹤 issue 前先呼叫 `gitea.listLabels` 取得可用標籤, * 先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式取得標籤 id 子集合,
* 再以本函式依保留問題挑出標籤 id 子集合,於 `gitea.createIssue` 建立 issue 時一次帶入 * 傳給 `gitea.createIssue` 讓新 issue 自動掛上合適標籤
*/ */
async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) { async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) {
if (labels.length === 0) return []; if (labels.length === 0) return [];
@@ -699,7 +657,7 @@ ${JSON.stringify(brief)}
- 沒有適合的標籤時輸出 []。`; - 沒有適合的標籤時輸出 []。`;
const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 }); const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 });
if (!res.ok) { if (!res.ok) {
log('建問題', 'WRN', `標籤挑選失敗,issue 不掛標籤${agentFailureDetail(res)}`); log('建問題', 'WRN', '標籤挑選失敗,issue 不掛標籤。');
return []; return [];
} }
const parsed = extractJson(res.output); const parsed = extractJson(res.output);
@@ -712,57 +670,61 @@ ${JSON.stringify(brief)}
} }
/** /**
* 建問題模式:把嚴重 findings 逐條以一般留言發到追蹤 issue * 建問題模式:把保留的審查問題建成存取庫的追蹤 issue 並逐條留言明細
* *
* issue 無法把留言掛在程式碼行上(沒有 diff 定位),故改以 * 流程:AI 挑標籤(`listLabels` {@link selectLabels},失敗不掛標籤)
* {@link templates.issueFindingComment} 在內文標明位置逐條發布—— * → 建立 issue(標題=PR 標題、本文=PR 描述加追溯資訊;失敗記 ERR 並回傳 null 不阻斷主流程)
* 等同一般模式 PR 步驟 9 的嚴重問題,改以 issue 留言呈現。 * → 複製 findings 依「檔案路徑→嚴重等級→起始行」排序({@link sortFindingsForIssue}
* findings 由呼叫端事先以 {@link sortFindings} 排序(嚴重度→檔案→行號),本函式不再排序 * → 逐條以 `templates.issueFindingComment` 留言到 issue
* *
* @param {Object} params - 解構參數。 * @param {Object} params - 解構參數。
* @param {Object} params.ctx - 執行環境 context`loadContext()` 回傳); Gitea API 認證。 * @param {Object} params.ctx - 執行環境 context`loadContext()` 回傳);使用 `prNumber`、`prTitle`、`prBody` 及 Gitea API 認證欄位
* @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `createCommentOnIssue`。 * @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `listLabels`、`createIssue`、`createCommentOnIssue`。
* @param {number} params.issueNumber - 目標追蹤 issue 的編號 * @param {Object} params.tool - `detectTool()` 偵測到的 AI CLI 工具描述物件(挑標籤用)
* @param {Array<Object>} params.severe - severity 為「嚴重」的 finding 列表(已排序;呼叫端保證非空) * @param {string} params.model - 指定 AI 模型名稱;空字串=工具預設
* @returns {Promise<void>} 無回傳值;結果反映在 issue 留言與日誌 * @param {string} params.cwd - agent 的工作目錄(repo 根目錄)
* @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出,由主流程頂層 catch 收斂 * @param {Array<Object>} params.findings - 要寫進 issue 的問題列表(通常為防守方裁決後保留的 `kept`);本函式以複本排序,不改動原陣列順序
* @returns {Promise<object|null>} 建立成功的 Gitea issue 物件(含 `number` 等欄位);建立 issue 失敗時為 null。
* @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出;列標籤與建 issue 的失敗則已於函式內降級處理。
* @remarks * @remarks
* 使用情境:建問題模式(input: create-issue)下,`main()`src/index.js於防守方裁決後 * 使用情境:`main()`src/index.js在步驟 10 之後、收尾之前,
* 建立追蹤 issue、寫入情境留言,再以本函式把嚴重問題逐條留言到該 issue * 於 `ctx.createIssue` 為 true 且 `kept.length > 0` 時呼叫本函式
* 警告+建議則由 {@link postOthersToIssue} 同樣逐條留言到同一 issue(皆可個別回覆), * 此模式下問題明細已保存在 issue 留言,收尾只 commit exclusions.json、findings 檔不進版控。
* 不使用 {@link templates.othersComment} 的單一表格——表格僅用於一般模式(PR)。
*/ */
async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { async function createIssueWithFindings({ ctx, gitea, tool, model, cwd, findings }) {
for (const finding of severe) { let labelIds = [];
await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding)); try {
const labels = await gitea.listLabels(ctx);
labelIds = await selectLabels({
tool,
model,
cwd,
labels,
prTitle: ctx.prTitle,
prBody: ctx.prBody,
findings,
});
} catch (err) {
log('建問題', 'WRN', `取得存取庫標籤失敗(${err.message}),issue 不掛標籤。`);
} }
log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}`); let issue;
} try {
issue = await gitea.createIssue(ctx, {
/** title: ctx.prTitle || `AI Code ReviewPR #${ctx.prNumber}`,
* 建問題模式:把警告+建議(非嚴重)findings 逐條以獨立留言發到追蹤 issue。 body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
* labels: labelIds,
* 與一般模式(PR)把警告+建議彙整成單一表格({@link templates.othersComment})不同: });
* issue 內每條問題各發一則留言({@link templates.issueFindingComment},內文標明位置), } catch (err) {
* 讓開發者能針對「單一問題」直接回覆討論,而非只能回覆一整張表格。 log('建問題', 'ERR', `建立 issue 失敗:${err.message}`);
* findings 由呼叫端事先以 {@link sortFindings} 排序(嚴重度→檔案→行號),本函式不再排序。 return null;
*
* @param {Object} params - 解構參數。
* @param {Object} params.ctx - 執行環境 context`loadContext()` 回傳);供 Gitea API 認證。
* @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `createCommentOnIssue`。
* @param {number} params.issueNumber - 目標追蹤 issue 的編號。
* @param {Array<Object>} params.others - severity 非「嚴重」(警告+建議)的 finding 列表(已排序;呼叫端保證非空)。
* @returns {Promise<void>} 無回傳值;結果反映在 issue 留言與日誌。
* @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出,由主流程頂層 catch 收斂。
* @remarks
* 使用情境:建問題模式(input: create-issue)下,`main()`src/index.js)步驟 10
* 以本函式把警告+建議逐條留言到追蹤 issue,確保 issue 上每條問題都是可個別回覆的留言。
*/
async function postOthersToIssue({ ctx, gitea, issueNumber, others }) {
for (const finding of others) {
await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding));
} }
log('步驟10', 'INF', `已將 ${others.length} 條警告+建議逐條留言到 issue #${issueNumber}`); const sorted = [...findings];
sortFindingsForIssue(sorted);
for (const finding of sorted) {
await gitea.createCommentOnIssue(ctx, issue.number, templates.issueFindingComment(finding));
}
log('建問題', 'INF', `issue #${issue.number} 已建立並逐條留言 ${sorted.length} 條問題。`);
return issue;
} }
/** /**
@@ -796,7 +758,7 @@ function readSnippet(cwd, finding) {
} }
/** /**
* 步驟 2:將 PR 既有的 bot 留言標記為已解決,本回合剛發的留言除外。 * 步驟 8:將 PR 既有的 bot 留言標記為已解決,本回合剛發的留言除外。
* *
* 兩類處理: * 兩類處理:
* - 一般留言(bot 發、含隱藏標記、非本回合、尚未標註)→ 編輯加上「〔已過時〕」前綴。 * - 一般留言(bot 發、含隱藏標記、非本回合、尚未標註)→ 編輯加上「〔已過時〕」前綴。
@@ -807,21 +769,18 @@ function readSnippet(cwd, finding) {
* @param {Object} params - 解構參數。 * @param {Object} params - 解構參數。
* @param {Object} params.ctx - 執行環境 context`loadContext()` 產出,含 repoPR 編號/token 等 API 呼叫所需資訊)。 * @param {Object} params.ctx - 執行環境 context`loadContext()` 產出,含 repoPR 編號/token 等 API 呼叫所需資訊)。
* @param {Object} params.gitea - Gitea API 模組(`src/lib/gitea.js`),需提供 `whoAmI``listIssueComments``editIssueComment``listReviews``listReviewComments``tryResolveReviewComment`;以參數注入便於測試替換。 * @param {Object} params.gitea - Gitea API 模組(`src/lib/gitea.js`),需提供 `whoAmI``listIssueComments``editIssueComment``listReviews``listReviewComments``tryResolveReviewComment`;以參數注入便於測試替換。
* @param {Set<number>} params.currentRunCommentIds - 本回合發出的一般留言 id 集合;這些留言不標註過時。 * @param {Set<number>} params.currentRunCommentIds - 本回合發出的一般留言 id 集合;這些留言不標註過時。
* @returns {Promise<void>} 無回傳值;結果反映在 PR 留言狀態與日誌。 * @returns {Promise<void>} 無回傳值;結果反映在 PR 留言狀態與日誌。
* @remarks * @remarks
* 使用情境:一般模式下,`src/index.js` 於「確定本回合審查已成功產生結果後、發布嚴重/ * 使用情境:審查流程「步驟 8」在防守方裁決、保存 findings 之後、
* 其他問題留言之前呼叫(見 `main()`),刻意延後到工具偵測、diff 整理與攻防裁決都成功之後, * 發布本回合嚴重問題留言(步驟 9之前呼叫,確保 PR 上只有最新回合的審查結果醒目可見。
* 避免任一前置步驟失敗時舊結果已被清掉、PR 卻沒有新結果。此時本回合的工具/diff/角色留言
* 已發出並登錄於 `currentRunCommentIds`,本函式據此排除、不會把這些「新產生的留言」誤標為過時;
* 之後才發布的嚴重/其他問題留言更不受影響,確保 PR 上只有最新回合的審查結果醒目可見。
*/ */
async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) { async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
let botLogin = ''; let botLogin = '';
try { try {
botLogin = (await gitea.whoAmI(ctx)).login || ''; botLogin = (await gitea.whoAmI(ctx)).login || '';
} catch (err) { } catch (err) {
log('步驟2', 'WRN', `無法取得 bot 身分(${err.message}),略過留言解決。`); log('步驟8', 'WRN', `無法取得 bot 身分(${err.message}),略過留言解決。`);
return; return;
} }
@@ -838,9 +797,9 @@ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
await gitea.editIssueComment(ctx, comment.id, `${templates.OUTDATED_PREFIX}${comment.body}`); await gitea.editIssueComment(ctx, comment.id, `${templates.OUTDATED_PREFIX}${comment.body}`);
outdatedCount += 1; outdatedCount += 1;
} }
log('步驟2', 'INF', `一般留言已標註〔已過時〕:${outdatedCount} 則。`); log('步驟8', 'INF', `一般留言已標註〔已過時〕:${outdatedCount} 則。`);
} catch (err) { } catch (err) {
log('步驟2', 'WRN', `標註一般留言失敗:${err.message}`); log('步驟8', 'WRN', `標註一般留言失敗:${err.message}`);
} }
// review 程式碼留言:盡力 resolve;API 不支援(第一次就失敗)即停止嘗試。 // review 程式碼留言:盡力 resolve;API 不支援(第一次就失敗)即停止嘗試。
@@ -866,12 +825,12 @@ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
} }
} }
if (resolveSupported) { if (resolveSupported) {
log('步驟2', 'INF', `review 程式碼留言已解決:${resolvedCount} 則。`); log('步驟8', 'INF', `review 程式碼留言已解決:${resolvedCount} 則。`);
} else { } else {
log('步驟2', 'WRN', 'Gitea 版本不支援 resolve API,review 程式碼留言維持原狀(已解決 ' + resolvedCount + ' 則)。'); log('步驟8', 'WRN', 'Gitea 版本不支援 resolve API,review 程式碼留言維持原狀(已解決 ' + resolvedCount + ' 則)。');
} }
} catch (err) { } catch (err) {
log('步驟2', 'WRN', `解決 review 留言失敗:${err.message}`); log('步驟8', 'WRN', `解決 review 留言失敗:${err.message}`);
} }
} }
@@ -922,9 +881,9 @@ module.exports = {
runDefenders, runDefenders,
sortFindings, sortFindings,
appendExclusions, appendExclusions,
sortFindingsForIssue,
selectLabels, selectLabels,
postSevereToIssue, createIssueWithFindings,
postOthersToIssue,
resolveOldComments, resolveOldComments,
postSevereComments, postSevereComments,
}; };
+1 -1
View File
@@ -23,7 +23,7 @@ const path = require('path');
* @throws {Error} 當 `rolesDir` 不存在、無法讀取,或個別檔案讀取失敗時, * @throws {Error} 當 `rolesDir` 不存在、無法讀取,或個別檔案讀取失敗時,
* 由 `fs.readdirSync` / `fs.readFileSync` 直接拋出(未在函式內捕捉)。 * 由 `fs.readdirSync` / `fs.readFileSync` 直接拋出(未在函式內捕捉)。
* @remarks * @remarks
* 使用情境:`src/index.js` 於審查流程步驟 5 呼叫 * 使用情境:`src/index.js` 於審查流程步驟 4 呼叫
* `loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'))` 載入全部角色, * `loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'))` 載入全部角色,
* 再以 {@link attackersOf} / {@link defendersOf} 依 frontmatter 的 `side` 欄位 * 再以 {@link attackersOf} / {@link defendersOf} 依 frontmatter 的 `side` 欄位
* 分出攻擊方(Mage/Assassin/Rogue/Bard/Leo/Maya)與防守方(Paladin), * 分出攻擊方(Mage/Assassin/Rogue/Bard/Leo/Maya)與防守方(Paladin),
+17 -42
View File
@@ -2,10 +2,10 @@
// 固定留言模板:本 action 發到 PR 的留言一律由此產生(繁體中文、UTF-8、表格優先)。 // 固定留言模板:本 action 發到 PR 的留言一律由此產生(繁體中文、UTF-8、表格優先)。
// 隱藏標記:辨識哪些留言是本 action 發的(步驟 2 標註過時時使用)。 // 隱藏標記:辨識哪些留言是本 action 發的(步驟 8 標註過時時使用)。
const MARK = '<!-- ai-code-review -->'; const MARK = '<!-- ai-code-review -->';
// 舊留言標註前綴(步驟 2 的降級做法:無 resolve API 時編輯加註)。 // 舊留言標註前綴(步驟 8 的降級做法:無 resolve API 時編輯加註)。
const OUTDATED_PREFIX = '> 〔已過時〕本留言屬於較舊的審查回合。\n\n'; const OUTDATED_PREFIX = '> 〔已過時〕本留言屬於較舊的審查回合。\n\n';
// 嚴重等級對應的 emoji 與排序權重。 // 嚴重等級對應的 emoji 與排序權重。
@@ -33,7 +33,7 @@ const FOCUS_LABEL = {
* @param {*} text - 任意待處理內容;非字串會先以 `String()` 轉型,nullundefined 視為空字串。 * @param {*} text - 任意待處理內容;非字串會先以 `String()` 轉型,nullundefined 視為空字串。
* @returns {string} 已逸出、單行化的儲存格內容;若結果為空則回傳 `'—'`。 * @returns {string} 已逸出、單行化的儲存格內容;若結果為空則回傳 `'—'`。
* @remarks * @remarks
* 使用情境:審查流程中所有表格型留言的共用防呆——例如步驟 4 * 使用情境:審查流程中所有表格型留言的共用防呆——例如步驟 3
* `diffComment()` 產生變更摘要表格時,檔名與用途欄位都經本函式處理, * `diffComment()` 產生變更摘要表格時,檔名與用途欄位都經本函式處理,
* 避免檔名或 AI 產生的描述含 `|` 或換行而撐破 Markdown 表格。 * 避免檔名或 AI 產生的描述含 `|` 或換行而撐破 Markdown 表格。
* 本函式未匯出,僅供模組內部使用。 * 本函式未匯出,僅供模組內部使用。
@@ -55,7 +55,7 @@ function cell(text) {
* @param {string} focus - 審查面向代碼(例如 `'logic'`、`'security'`);可為 undefined。 * @param {string} focus - 審查面向代碼(例如 `'logic'`、`'security'`);可為 undefined。
* @returns {string} 顯示字串:命中時如 `'邏輯(logic'`;未命中時原樣回傳 `focus`falsy 時回傳 `'—'`。 * @returns {string} 顯示字串:命中時如 `'邏輯(logic'`;未命中時原樣回傳 `focus`falsy 時回傳 `'—'`。
* @remarks * @remarks
* 使用情境:審查流程步驟 57 的角色登場留言——`rolesComment()` * 使用情境:審查流程步驟 46 的角色登場留言——`rolesComment()`
* 產生「角色|面向|個性」表格時,以本函式把每位審查員 * 產生「角色|面向|個性」表格時,以本函式把每位審查員
* (攻擊方/防守方)的 focus 代碼轉成中英並列的面向欄位內容。 * (攻擊方/防守方)的 focus 代碼轉成中英並列的面向欄位內容。
* 本函式未匯出,僅供模組內部使用。 * 本函式未匯出,僅供模組內部使用。
@@ -66,7 +66,7 @@ function focusLabel(focus) {
} }
/** /**
* 產生審查流程步驟 3 的「審查工具」PR 留言內容。 * 產生審查流程步驟 2 的「審查工具」PR 留言內容。
* *
* 留言以隱藏標記 `MARK` 開頭,包含工具資訊表格(工具/版本/模型/ * 留言以隱藏標記 `MARK` 開頭,包含工具資訊表格(工具/版本/模型/
* 審查 commitRun Job 連結)與一張 mermaid 流程圖,說明整條審查管線 * 審查 commitRun Job 連結)與一張 mermaid 流程圖,說明整條審查管線
@@ -81,9 +81,9 @@ function focusLabel(focus) {
* @param {string} params.runLink - CI run 的網址,直接內插為 Markdown 連結目標。 * @param {string} params.runLink - CI run 的網址,直接內插為 Markdown 連結目標。
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記,結尾帶換行)。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記,結尾帶換行)。
* @remarks * @remarks
* 使用情境:審查流程步驟 3——每回合審查開始時,先把工具身分與 * 使用情境:審查流程步驟 2——每回合審查開始時,先把工具身分與
* 管線流程圖留言到 PR,讓開發者知道這回合由哪個版本/模型執行; * 管線流程圖留言到 PR,讓開發者知道這回合由哪個版本/模型執行;
* 留言開頭的 MARK 讓步驟 2 能辨識並將舊回合留言標註為過時。 * 留言開頭的 MARK 讓步驟 8 能辨識並將舊回合留言標註為過時。
*/ */
function toolComment({ toolName, version, model, sha, runNumber, runLink }) { function toolComment({ toolName, version, model, sha, runNumber, runLink }) {
return `${MARK} return `${MARK}
@@ -108,7 +108,7 @@ flowchart LR
} }
/** /**
* 產生審查流程步驟 4 的「變更摘要(送審 git diff)」PR 留言內容。 * 產生審查流程步驟 3 的「變更摘要(送審 git diff)」PR 留言內容。
* *
* 以四欄表格(檔案/用途/git diff 長度/最後更新時間)列出本回合 * 以四欄表格(檔案/用途/git diff 長度/最後更新時間)列出本回合
* 送審的每個檔案;diff 過長被截斷送審的檔案會加註「(過長截斷送審)」, * 送審的每個檔案;diff 過長被截斷送審的檔案會加註「(過長截斷送審)」,
@@ -125,7 +125,7 @@ flowchart LR
* @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示排除註記。 * @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示排除註記。
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
* @remarks * @remarks
* 使用情境:審查流程步驟 4——整理完 git diff 後,把「哪些檔案、多長、 * 使用情境:審查流程步驟 3——整理完 git diff 後,把「哪些檔案、多長、
* 是否截斷、哪些被 .reviewignore 排除」留言到 PR,讓開發者確認送審範圍 * 是否截斷、哪些被 .reviewignore 排除」留言到 PR,讓開發者確認送審範圍
* 與 AI 實際看到的內容一致。 * 與 AI 實際看到的內容一致。
*/ */
@@ -150,10 +150,10 @@ function diffComment(rows, ignoredCount) {
} }
/** /**
* 產生審查流程步驟 57 共用的「角色登場」PR 留言內容。 * 產生審查流程步驟 46 共用的「角色登場」PR 留言內容。
* *
* 以三欄表格(角色/面向/個性)列出本回合登場的審查員; * 以三欄表格(角色/面向/個性)列出本回合登場的審查員;
* 攻擊方(步驟 5)與防守方(步驟 7)共用本模板,僅標題不同。 * 攻擊方(步驟 4)與防守方(步驟 6)共用本模板,僅標題不同。
* 面向欄位經 focusLabel() 轉成「中文(原文)」並列格式。 * 面向欄位經 focusLabel() 轉成「中文(原文)」並列格式。
* *
* @param {Object} params - 留言內容(解構參數)。 * @param {Object} params - 留言內容(解構參數)。
@@ -166,7 +166,7 @@ function diffComment(rows, ignoredCount) {
* @param {string} params.roles[].meta.personality - 角色個性描述;經 cell() 防呆。 * @param {string} params.roles[].meta.personality - 角色個性描述;經 cell() 防呆。
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
* @remarks * @remarks
* 使用情境:審查流程步驟 5(攻擊方登場)與步驟 7(防守方登場)—— * 使用情境:審查流程步驟 4(攻擊方登場)與步驟 6(防守方登場)——
* 在各階段開始審查前,把該回合參與的審查員角色、負責面向與個性 * 在各階段開始審查前,把該回合參與的審查員角色、負責面向與個性
* 留言到 PR,讓開發者理解後續 findings 是由哪些視角產出的。 * 留言到 PR,讓開發者理解後續 findings 是由哪些視角產出的。
*/ */
@@ -302,7 +302,7 @@ function othersComment(findings) {
* @param {string} [params.prBody] - PR 描述原文;nullish 或 trim 後為空時輸出佔位文字。 * @param {string} [params.prBody] - PR 描述原文;nullish 或 trim 後為空時輸出佔位文字。
* @returns {string} 完整 issue 本文 Markdown 字串(含 MARK 隱藏標記)。 * @returns {string} 完整 issue 本文 Markdown 字串(含 MARK 隱藏標記)。
* @remarks * @remarks
* 使用情境:建問題模式下 `main()`src/index.js)的 `ensureIssueCreated` 建立 issue 時, * 使用情境:建問題模式下 `createIssueWithFindings`src/lib/review.js建立 issue 時,
* 以「標題=PR 標題、本文=本函式輸出」呼叫 `gitea.createIssue` * 以「標題=PR 標題、本文=本函式輸出」呼叫 `gitea.createIssue`
* 讓 issue 讀者能從本文回溯到觸發審查的 PR,再從下方留言逐條查看問題明細。 * 讓 issue 讀者能從本文回溯到觸發審查的 PR,再從下方留言逐條查看問題明細。
*/ */
@@ -334,9 +334,9 @@ ${body || 'PR 無描述)'}
* @param {string} [finding.suggestedCode] - 建議寫法程式碼;有值才輸出「建議寫法」區塊。 * @param {string} [finding.suggestedCode] - 建議寫法程式碼;有值才輸出「建議寫法」區塊。
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
* @remarks * @remarks
* 使用情境:建問題模式下 `review.postSevereToIssue`src/lib/review.js把每條嚴重 finding * 使用情境:建問題模式下 `createIssueWithFindings`src/lib/review.js建立 issue 後,
* 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上, * 把保留的 findings 依「檔案路徑→嚴重等級→起始行」排序,逐條以本函式產生留言內容、
* 作為問題明細的追蹤紀錄。 * 經 `gitea.createCommentOnIssue` 發布到新 issue 上,作為問題明細的追蹤紀錄。
*/ */
function issueFindingComment(finding) { function issueFindingComment(finding) {
const emoji = SEVERITY_EMOJI[finding.severity] || '🔵'; const emoji = SEVERITY_EMOJI[finding.severity] || '🔵';
@@ -371,7 +371,7 @@ function issueFindingComment(finding) {
* @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示「(N 個檔案被排除)」註記。 * @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示「(N 個檔案被排除)」註記。
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
* @remarks * @remarks
* 使用情境:審查流程步驟 4 的替代路徑——整理 git diff 時發現 * 使用情境:審查流程步驟 3 的替代路徑——整理 git diff 時發現
* 過濾後送審清單為空(例如整包變更都被 .reviewignore 排除), * 過濾後送審清單為空(例如整包變更都被 .reviewignore 排除),
* 直接以本留言告知開發者本回合視為審查通過,不再進入 * 直接以本留言告知開發者本回合視為審查通過,不再進入
* 攻擊方/防守方審查階段。 * 攻擊方/防守方審查階段。
@@ -383,30 +383,6 @@ function nothingToReviewComment(ignoredCount) {
本次 PR 套用 \`.reviewignore\` 後**沒有可審查的變更**${ignoredCount > 0 ? `${ignoredCount} 個檔案被排除)` : ''},視為審查通過。`; 本次 PR 套用 \`.reviewignore\` 後**沒有可審查的變更**${ignoredCount > 0 ? `${ignoredCount} 個檔案被排除)` : ''},視為審查通過。`;
} }
/**
* 產生建問題模式(input: create-issue)下,回貼到「PR」的追蹤問題連結留言。
*
* 建問題模式把審查內容全部發到 issue、不留在 PR;本留言是 PR 上唯一的一則審查留言,
* 提供 issue 連結與問題數量統計,讓 PR 讀者一眼看到「本次審查結果在哪個 issue」。
*
* @param {Object} params - 解構參數。
* @param {number} params.issueNumber - 追蹤問題的 issue 編號;內插為 Markdown 連結文字。
* @param {string} params.issueUrl - 追蹤問題的 issue 網址;作為 Markdown 連結目標。
* @param {number} params.severeCount - 嚴重問題條數,顯示在統計。
* @param {number} params.otherCount - 警告+建議問題條數,顯示在統計。
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
* @remarks
* 使用情境:建問題模式下 `main()`src/index.js)在 issue 建立並寫入全部審查內容後,
* 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以
* {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。
*/
function issueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {
return `${MARK}
## 🔍 AI Code Review|已建立追蹤問題
本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`;
}
module.exports = { module.exports = {
MARK, MARK,
OUTDATED_PREFIX, OUTDATED_PREFIX,
@@ -421,5 +397,4 @@ module.exports = {
issueBody, issueBody,
issueFindingComment, issueFindingComment,
nothingToReviewComment, nothingToReviewComment,
issueLinkComment,
}; };