Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b60f50f2b4 | ||
|
|
43ad2b56d2 | ||
|
|
8e18bbacc2 | ||
|
|
1f012c6cfb | ||
|
|
76617dd9dc | ||
|
|
216bc39255 | ||
|
|
75ca8cdb83 | ||
|
|
aaf2e484d5 | ||
|
|
55da29a86f | ||
|
|
d424447d15 | ||
|
|
9d05af647b |
@@ -1791,5 +1791,313 @@
|
|||||||
"endLine": 32,
|
"endLine": 32,
|
||||||
"problem": "`redactSecrets` 新增多個遮罩規則與控制字元單行化,但測試只透過 `agentFailureDetail` 間接覆蓋 Authorization 與 token 兩種格式。URL 內嵌帳密、長 token、GitHub token 樣式、控制字元注入與 null/undefined 邊界都還沒被直接驗證。",
|
"problem": "`redactSecrets` 新增多個遮罩規則與控制字元單行化,但測試只透過 `agentFailureDetail` 間接覆蓋 Authorization 與 token 兩種格式。URL 內嵌帳密、長 token、GitHub token 樣式、控制字元注入與 null/undefined 邊界都還沒被直接驗證。",
|
||||||
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄失敗診斷與 redactSecrets 對 Authorization、URL 帳密、token 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。"
|
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄失敗診斷與 redactSecrets 對 Authorization、URL 帳密、token 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 14:42:39",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 179,
|
||||||
|
"endLine": 212,
|
||||||
|
"problem": "`queueOrPostComment` 與 `createIssueAndFlushBufferedComments` 這兩個閉包名稱像兩段不同旋律:一個強調「排隊或發布」,另一個卻把「建立 issue、flush 暫存留言、設定狀態」全塞進名稱與實作。讀者要來回追 `trackingIssue`、`pendingIssueCommentBodies`,節奏偏長且語意負擔重。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 main() 內新增閉包與 issue/comment 路由細節讓主流程閱讀負擔加重;本條只是改以目前函式名稱描述同一維護性問題。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 14:42:39",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 165,
|
||||||
|
"endLine": 213,
|
||||||
|
"problem": "`main()` 這次把「留言目的地切換、issue 建立、暫存留言 flush、標籤挑選前置狀態」都塞進閉包與可變狀態(`pendingIssueCommentBodies`、`trackingIssue`、`currentRunCommentIds`)。半年後要改建問題模式時,維護者得同時追蹤主流程步驟、閉包副作用與留言落點,任何新增留言點都可能忘記處理 queue/flush 或 PR 排除清單,長期會讓流程編排變得很脆弱。",
|
||||||
|
"reason": "Paladin:可排除(命中已知排除事項且重複)。已知與歷史 findings 已涵蓋 main() 內留言目的地、issue 狀態、緩衝佇列與可變閉包耦合,及應抽出發布器/sink 的方向。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 14:42:39",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 302,
|
||||||
|
"endLine": 353,
|
||||||
|
"problem": "建問題模式的收束邏輯把「挑標籤、建立 issue、發 finding、回貼 PR、設定 issue dependency、決定是否阻擋合併」全部展開在 `main()`。這段和前面的 `createIssueAndFlushBufferedComments` 共同組成一條隱性的 issue workflow,但邊界沒有被封裝;未來要新增 issue 模板、改排序、改阻擋條件或重試策略時,會在主流程裡到處補條件,維護成本會快速上升。",
|
||||||
|
"reason": "Paladin:可排除(重複)。此條與 F006 及歷史 findings 同樣指向建問題模式在 main() 中承擔 issue workflow、發布分流與狀態管理,屬同一維護性問題的延伸描述。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 14:42:39",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "readme.md",
|
||||||
|
"startLine": 56,
|
||||||
|
"endLine": 132,
|
||||||
|
"problem": "README 內大量維護到原始碼行號的連結,這次 diff 只是分支與行號位移就需要同步改一整片表格。這種文件和程式碼行號的硬耦合很容易在後續修改時過期,讀者點到錯誤位置,維護者也要花時間做機械式同步。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 README 功能表硬編分支與行號連結,導致文件與原始碼高度耦合且容易失準。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 14:42:39",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Mage",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 251,
|
||||||
|
"endLine": 253,
|
||||||
|
"problem": "無可審查變更的分支在一般模式下會先把舊留言標為過時,之後才保存 findings 與 commit/push 結果。最小重現:`files.length === 0`、`nothingToReviewComment` 成功、`resolveOldComments` 成功,但接著 `saveFindings` 因檔案系統錯誤或 `commitFindings` 因 push 失敗拋例外。此時舊審查結果已被標過時,但沒有成功落地本回合的 success 結果 commit,狀態會停在半更新。",
|
||||||
|
"reason": "Paladin:可排除(命中已知排除事項)。維護者已裁示舊留言應刻意在本回合結果發布前標為過時;本條要求延後清理,落在同一已排除範圍。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 14:42:39",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 160,
|
||||||
|
"endLine": 353,
|
||||||
|
"problem": "建問題模式的主流程被大幅改寫,但目前測試只驗證了少數 helper,沒有驗證「留言先暫存、確定有 kept finding 才建 issue、無保留問題時靜默通過、嚴重問題才加 issue dependency、最後回貼 PR 連結」這些新增流程。這些行為一旦順序或條件寫錯,測試不會擋下來。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式 main() 層級流程缺測試,包括暫存留言、有 finding 才建 issue、無 finding 靜默、PR 回貼、嚴重與非嚴重分流及 dependency 失敗降級。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 14:42:39",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 307,
|
||||||
|
"endLine": 361,
|
||||||
|
"problem": "`pushWithCredential` 新增了關鍵認證行為,但沒有測試驗證它真的用 `GIT_CONFIG_*` 清掉 checkout 的 extraheader、再注入 PAT Authorization,也沒有測試遠端 URL 不符與 push 失敗時不洩漏 URL/token 的失敗路徑。這段是結果 commit 能否觸發下一輪 CI 的核心,沒有測試等於這個新契約還沒通過試煉。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄 pushWithCredential/pushToken 認證推送路徑缺少測試,包含 token 不進 argv、認證遮蔽、推送目標與失敗訊息不洩漏敏感資訊。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 14:42:39",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 130,
|
||||||
|
"endLine": 192,
|
||||||
|
"problem": "`resolveMergeBase` 新增多段 fetch fallback:先抓 base、再 deepen base、deepen PR HEAD、最後 shallow repo 才 unshallow,但現有測試只驗證不安全 baseRef 會被拒絕,沒有驗證淺層歷史補抓順序、每次 fetch 後會重試 merge-base、成功後會短路,也沒有覆蓋所有策略失敗時的診斷錯誤。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、成功短路、降級順序與最終失敗診斷缺測試提出相同問題。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 14:42:39",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/lib/diagnostics.js",
|
||||||
|
"startLine": 18,
|
||||||
|
"endLine": 69,
|
||||||
|
"problem": "`redactSecrets` 與 `agentFailureDetail` 是新加入的安全診斷防線,但測試只覆蓋 Authorization/token 的基本遮罩。控制字元單行化、URL 內嵌帳密、長 token/hex、輸入與輸出長度截斷、以及沒有 error code 時的 fallback 訊息都還沒被驗證。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 redactSecrets/agentFailureDetail 對控制字元、URL 帳密、token 格式、截斷與 fallback 診斷等邊界缺少測試。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:00:33",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 166,
|
||||||
|
"endLine": 223,
|
||||||
|
"problem": "`main()` 這段新添的建問題模式狀態、暫存佇列、三個閉包輔助函式,全塞在主流程開頭,讓主旋律還沒開始就先進入一大段插曲。`issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies` 彼此共享可變狀態,讀者必須在腦中追蹤閉包副作用,主流程的步驟節奏因此變得沉重。",
|
||||||
|
"reason": "Paladin:可排除(重複)。此條指涉 main() 內 issue 模式狀態、暫存佇列與閉包職責混雜,已由既有排除事項與歷史 findings 多次涵蓋。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:00:33",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 230,
|
||||||
|
"endLine": 239,
|
||||||
|
"problem": "註解標成「步驟 2:延後執行」,但實際位置夾在快速檢查與步驟 3 之前,後面又在步驟 8 後再次出現「步驟 2(延後執行)」。同一個步驟號在不同位置反覆變奏,雖然註解有解釋,閱讀節拍仍容易打結。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已指出流程步驟編號散落且實際順序變成 1、3~8、2、9~10,與本條「步驟 2 延後但仍用線性編號」相同。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:00:33",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 166,
|
||||||
|
"endLine": 235,
|
||||||
|
"problem": "`main()` 現在同時負責流程編排、PR 留言、issue 模式暫存、issue 建立、fallback 狀態切換與留言 flush。這段靠 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 多個閉包變數互相配合,半年後要改「留言要發去哪裡」或「建 issue 失敗怎麼降級」時,很容易漏掉某個狀態轉換,尤其後面收尾 commit、resolve 舊留言、嚴重/其他問題發布都還會讀這些狀態。",
|
||||||
|
"reason": "Paladin:可排除(命中已知排除事項且重複)。main() 同時承擔留言路由、issue 狀態、fallback 與 flush 的發布狀態管理,已由既有排除事項與歷史 findings 涵蓋。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:00:33",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 222,
|
||||||
|
"endLine": 230,
|
||||||
|
"problem": "註解與 log 名稱把「標記舊留言過時」稱為「步驟 2」,但實際執行點被延後到流程後段。這種非時間順序的步驟編號會讓維護者追 log 或對照 README 流程圖時產生認知落差:看到「步驟 2」不再代表第二個發生的動作,而是某個被延後的歷史步驟。",
|
||||||
|
"reason": "Paladin:可排除(重複)。與 F003 及歷史 findings 指涉同一個延後執行的「步驟 2」仍以線性步驟編號呈現所造成的認知落差。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:00:33",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "readme.md",
|
||||||
|
"startLine": 76,
|
||||||
|
"endLine": 131,
|
||||||
|
"problem": "README 內大量手動維護到 `src/branch/develop/...#Lxx` 的精確行號連結。這次變更已經因程式碼位移而更新一整片連結,未來每次插入函式或註解都會造成文件 churn;更糟的是漏改時文件會指到錯誤行,讀者以為文件可信,實際上卻被帶到過期位置。",
|
||||||
|
"reason": "Paladin:可排除(重複)。README 大量手動維護 develop 分支與 #Lxx 行號連結,歷史 findings 已多次記錄相同維護成本與連結漂移風險。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:00:33",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Mage",
|
||||||
|
"severity": "嚴重",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 117,
|
||||||
|
"endLine": 120,
|
||||||
|
"problem": "當 PR 審查出嚴重問題,但結果 commit/push 失敗時,`commitFindings` 只回傳 `false`。新版主流程又改成「本輪審查不因嚴重問題直接 exit 1,而是依賴下一輪讀到 `[failure]` commit 才失敗」。最小情境:`severe.length > 0`、token 權限不足或 push 競態導致 `commitAndPushFindings` 丟錯;此時沒有 `[failure]` commit、也不會有下一輪快速回報,但本輪仍可能以 0 結束,PR 檢查會在存在嚴重問題時通過。",
|
||||||
|
"reason": "Paladin:可排除(重複)。此條與 F001 指涉同一風險:嚴重 finding 存在時 failure 結果 commit/push 失敗,可能使本輪檢查仍以成功結束。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:00:33",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 163,
|
||||||
|
"endLine": 437,
|
||||||
|
"problem": "建問題模式被大幅改寫:情境留言先暫存、只有 kept finding 才建 issue、建立失敗會 fallback 回 PR、嚴重/非嚴重 finding 會改發到 issue、最後還要回貼追蹤 issue 連結與設定相依關係。但目前新增測試只覆蓋了少數 helper,沒有驗證 `main()` 在這些分支下的實際副作用。這些行為沒經過試煉,等於還不知道留言會不會發錯地方、fallback 後會不會漏清舊留言、或無 finding 時是否真的靜默通過。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、只在有 finding 時建 issue、fallback、嚴重/非嚴重分流、PR 回貼與靜默通過等核心分支缺少流程測試。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:00:33",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 298,
|
||||||
|
"endLine": 355,
|
||||||
|
"problem": "`commitAndPushFindings` 的推送行為改成一律走 `pushWithCredential`,並宣稱 token 不進 argv、會清掉 checkout 的 extraheader、只接受同一 Gitea origin 的 `.git` URL。這是安全與 CI 觸發都很關鍵的失敗路徑,但目前 `test/gitrepo.test.js` 只測了分支名稱檢查,沒有驗證推送時的 argv/env,也沒有驗證遠端 URL 不符時會停止。這段若改壞,測試不會提醒我們 token 可能出現在命令列或推送根本沒用 PAT 身分。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄 commitAndPushFindings/pushWithCredential 的認證推送路徑、argv/env、遠端目標檢查與失敗訊息遮蔽缺少測試。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:00:33",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 130,
|
||||||
|
"endLine": 188,
|
||||||
|
"problem": "`resolveMergeBase` 新增了多階段 fetch 補歷史流程:先 fetch base、首次 merge-base、再 deepen base、deepen PR HEAD、必要時 unshallow,且每個策略成功後要立即重試並短路返回。現有測試只驗證不安全 `baseRef` 會在 fetch 前被拒絕,沒有測到淺層 checkout、首次失敗後補抓成功、策略失敗繼續下一個、全部失敗時診斷訊息等邊界。這正是容易 off-by-one 或順序錯的流程,現在還沒有測試保護。",
|
||||||
|
"reason": "Paladin:可排除(重複)。resolveMergeBase 多階段 fetch、淺層與非淺層路徑、策略停止條件、全部失敗診斷與 cause 缺少測試,已由歷史 findings 涵蓋。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:26:12",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Assassin",
|
||||||
|
"severity": "嚴重",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 105,
|
||||||
|
"endLine": 116,
|
||||||
|
"problem": "這裡把審查結果 commit/push 失敗吞掉並回傳 `false`,而本次變更又把主流程改成「本輪審查不因嚴重問題直接 exit 1,靠下一輪讀到 `[failure]` commit 才失敗」。攻擊者只要讓結果 commit 推不上去,例如在 PR head 競態推送、讓 token 沒有 push 權限、或讓來源分支拒絕 bot push,就能讓嚴重安全 finding 已產生但沒有 failure commit、也沒有下一輪失敗檢查,等同把必要檢查繞過。",
|
||||||
|
"reason": "人工裁示排除:主流程已在 result === failure 且 commitFindings 回傳 false 時直接回傳 1;本 finding 針對舊位置的描述已由現有收尾防線涵蓋。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:30:12",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "readme.md",
|
||||||
|
"startLine": 48,
|
||||||
|
"endLine": 48,
|
||||||
|
"problem": "Mermaid 節點代號從 `N8` 跳回 `N2`,雖然流程語意是「步驟 2 延後」,但原始碼讀起來像旋律突然倒帶;維護者在對照圖與步驟時容易被代號順序絆住。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已指出流程步驟編號與實際順序、README 流程圖及文件對照會產生認知落差;本條 Mermaid 節點代號問題屬同一類指控。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:30:12",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 167,
|
||||||
|
"endLine": 235,
|
||||||
|
"problem": "`main()` 內新增了建 issue 模式狀態、暫存佇列與三個帶長篇 JSDoc 的閉包,讓主流程的旋律被大量伴奏蓋過。這段同時管理 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies` 與 PR 留言 id,可讀性負擔明顯升高。",
|
||||||
|
"reason": "Paladin:可排除(重複)。已知排除事項與歷史 findings 已涵蓋 main() 內留言路由、issue 狀態、暫存佇列與發布職責耦合造成可讀性與維護負擔。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:30:12",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 121,
|
||||||
|
"endLine": 128,
|
||||||
|
"problem": "`strategies` 以陣列位置承載語意,`[label, ...args]` 雖簡短,卻讓每個元素的第二欄之後全靠讀者猜是 git argv;這裡的可讀性像沒有小節線的樂譜。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 resolveMergeBase 內 strategies 與補救策略編排使閱讀節奏偏密提出問題;本條 tuple 可讀性屬同一段策略結構的細項。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:30:12",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 163,
|
||||||
|
"endLine": 231,
|
||||||
|
"problem": "`main()` 這次把一般模式/建問題模式/降級模式的留言去向都塞進閉包狀態:`issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 彼此耦合。半年後要新增一種落地方式或調整「何時清舊留言」時,維護者必須同時追多個可變狀態與後續多個 `if (issueModeActive)` 分支,很容易漏掉 flush、清空、登錄留言 id 或 fallback 後的收尾行為。",
|
||||||
|
"reason": "Paladin:可排除(重複)。與 F003 及既有排除事項相同,均指向 main() 以可變閉包狀態承擔留言目的地、issue 生命週期與 fallback 狀態機。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:30:12",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 232,
|
||||||
|
"endLine": 241,
|
||||||
|
"problem": "流程註解把「將舊留言標記為解決」稱為「步驟 2(延後執行)」,但實際執行點在審查結果產生後、接近原本步驟 9 前。這種「編號是 2、時間點是 8 後」的設計已經擴散到多個檔案的 JSDoc 與 log 描述,未來查 CI log 或維護流程圖時會產生認知落差:看到 `步驟2` 不代表它真的發生在第二步。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已明確記錄步驟編號散落於 log、JSDoc、README 且實際順序變成 1、3 到 8、2、9 到 10 的維護風險。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:30:12",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 103,
|
||||||
|
"endLine": 160,
|
||||||
|
"problem": "`resolveMergeBase()` 內同時負責驗證 base ref、fetch base、重試 merge-base、補抓淺層歷史、累積診斷與組錯誤訊息。這些都和「取得 merge-base」有關,但現在全部攤在同一個函式裡,策略陣列、診斷格式與 git 操作細節混在一起;未來要新增 fetch 策略或改診斷文字時,容易不小心改壞主流程判斷。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 resolveMergeBase 同時負責驗證、fetch 策略編排、診斷與錯誤包裝,難以分辨主流程與補救策略。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:30:12",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 164,
|
||||||
|
"endLine": 232,
|
||||||
|
"problem": "建問題模式新增了「先暫存情境留言、確定有保留 finding 才建 issue、建 issue 失敗時降級回 PR 留言」這一整段流程,但目前測試只覆蓋了部分 helper,沒有驗證主流程在 create-issue=true 下的分流行為。這代表像「無保留問題時不應建 issue/不應留言」、「建立 issue 失敗時應把 pending 留言補回 PR」、「trackingIssue 建立後後續留言應改送 issue」這些行為都還沒通過試煉。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式核心流程、暫存留言、issue 建立條件、發布分流、外部 API 狀態切換與降級路徑缺少測試。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:30:12",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 104,
|
||||||
|
"endLine": 150,
|
||||||
|
"problem": "resolveMergeBase 新增了多階段 fetch/deepen/unshallow fallback 與診斷錯誤,但測試只驗證不安全 baseRef 會被拒絕,沒有驗證任何成功或失敗 fallback 路徑。這段是 diff 基準的核心邏輯,若 shallow checkout 下 deepen PR HEAD 用錯 ref、策略順序跑錯,或全部失敗時診斷不正確,現有測試都抓不到。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、策略停止條件、降級與最終診斷缺少測試提出相同問題。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addedAt": "2026/07/21 16:30:12",
|
||||||
|
"prNumber": 6,
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 272,
|
||||||
|
"endLine": 322,
|
||||||
|
"problem": "commitAndPushFindings 改成一律透過 pushWithCredential 用 PAT extraheader 推送,且新增 headRef 安全檢查與固定錯誤遮蔽;但目前 gitrepo 測試沒有覆蓋 commitAndPushFindings/pushWithCredential 的推送行為。這讓「token 不進 argv」、「清掉 checkout 既有 extraheader」、「遠端 URL 不符時拒絕」、「push 失敗不洩漏 URL/token」這些失敗與安全邊界都沒有被驗證。",
|
||||||
|
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 commitAndPushFindings 與 pushWithCredential 的推送策略、認證遮蔽、失敗路徑與 token 不外洩等測試缺口。"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -59,19 +59,6 @@
|
|||||||
"problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)",
|
"problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)",
|
||||||
"suggestion": "mock git 執行器與 URL/env 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。",
|
"suggestion": "mock git 執行器與 URL/env 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。",
|
||||||
"suggestedCode": ""
|
"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: ''"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"excluded": []
|
"excluded": []
|
||||||
|
|||||||
@@ -120,20 +120,6 @@
|
|||||||
"suggestedCode": "",
|
"suggestedCode": "",
|
||||||
"sourceIssue": 15
|
"sourceIssue": 15
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "F015",
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/lib/gitea.js",
|
|
||||||
"startLine": 190,
|
|
||||||
"endLine": 197,
|
|
||||||
"problem": "`dependency` 作為參數名太薄,與 `issueNumber` 並列時看不出它也是 issue 編號。讀到 `addIssueDependency(ctx, ctx.prNumber, issue.number)` 時,語意要靠上下文補拍子。",
|
|
||||||
"suggestion": "改用更完整的名稱,例如 `dependencyIssueNumber` 或 `blockingIssueNumber`,讓「誰被誰阻擋」在簽名裡就清楚成形。",
|
|
||||||
"suggestedCode": "```\nfunction addIssueDependency(ctx, issueNumber, dependencyIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {\n index: dependencyIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```",
|
|
||||||
"sourceIssue": 15
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "F022",
|
"id": "F022",
|
||||||
"reviewer": "Leo",
|
"reviewer": "Leo",
|
||||||
@@ -148,34 +134,6 @@
|
|||||||
"suggestedCode": "",
|
"suggestedCode": "",
|
||||||
"sourceIssue": 18
|
"sourceIssue": 18
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "F023",
|
|
||||||
"reviewer": "Mage",
|
|
||||||
"focus": "",
|
|
||||||
"badge": "🔮",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 374,
|
|
||||||
"endLine": 387,
|
|
||||||
"problem": "建問題模式下只要有任何保留 finding 就會建立 issue,且後續一律把 PR 設為相依於該 issue;但收尾結果仍是 `severe.length === 0 ? 'success' : 'failure'`。最小情境:攻擊方只產生 1 條「建議」,`severe.length` 為 0,action commit `[success]`,但 PR 被 issue dependency 擋住無法合併。這讓「success=可通過」與「非嚴重問題也阻擋合併」兩個語義互相矛盾。",
|
|
||||||
"suggestion": "明確對齊語義:若只有嚴重問題才應阻擋合併,則只在 `severe.length > 0` 時建立 dependency;若所有保留問題都要阻擋合併,則 result/exit code 不應只看嚴重問題。",
|
|
||||||
"suggestedCode": "```\nif (severe.length > 0) {\n try {\n await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number);\n log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}。`);\n } catch (err) {\n log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`);\n }\n}\n```",
|
|
||||||
"sourceIssue": 18
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "F024",
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 179,
|
|
||||||
"endLine": 181,
|
|
||||||
"problem": "`issueBuffer` 的旋律太含糊:讀者會以為裡面放的是 issue,實際上暫存的是尚未送出的留言 body。命名沒有把資料形狀唱清楚。",
|
|
||||||
"suggestion": "改成能描述內容與用途的名稱,例如 `pendingIssueCommentBodies`,並同步調整註解與迴圈變數。",
|
|
||||||
"suggestedCode": "```\nconst pendingIssueCommentBodies = [];\nlet issue = null;\n```",
|
|
||||||
"sourceIssue": 18
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "F025",
|
"id": "F025",
|
||||||
"reviewer": "Assassin",
|
"reviewer": "Assassin",
|
||||||
@@ -204,34 +162,6 @@
|
|||||||
"suggestedCode": "",
|
"suggestedCode": "",
|
||||||
"sourceIssue": 19
|
"sourceIssue": 19
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "F027",
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 181,
|
|
||||||
"endLine": 185,
|
|
||||||
"problem": "`issue` 這個變數名太素,像樂譜上只寫「音符」卻不說是哪一聲部。此處承載的是建問題模式建立出的追蹤 issue,後面還會與 PR issue 編號、Gitea issue API 參數交錯出現,名稱過泛會讓閱讀節奏變濁。",
|
|
||||||
"suggestion": "改成能表明角色的名稱,例如 `trackingIssue`。對應的 `ensureIssueCreated` 也可改為 `ensureTrackingIssueCreated`,讓閉包狀態與用途一眼對上。",
|
|
||||||
"suggestedCode": "```\nconst issueBuffer = [];\nlet trackingIssue = null;\n\nconst postComment = async (body) => {\n if (ctx.createIssue) {\n if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n issueBuffer.push(body);\n return null;\n }\n const created = await gitea.createIssueComment(ctx, body);\n currentRunCommentIds.add(created.id);\n return created;\n};\n```",
|
|
||||||
"sourceIssue": 19
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "F029",
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/lib/templates.js",
|
|
||||||
"startLine": 388,
|
|
||||||
"endLine": 405,
|
|
||||||
"problem": "`issueLinkComment` 產生的是 PR 上唯一的建問題模式回貼留言,但名稱少了 PR 的聲部;同檔已有 `issueBody`、`issueFindingComment`,乍看會以為這也是 issue 內留言模板,命名層次不夠分明。",
|
|
||||||
"suggestion": "改名為 `prIssueLinkComment` 或 `trackingIssueLinkComment`,讓模板的投遞位置與用途直接寫在名稱裡,避免與 issue 內文、issue finding 留言混成一團。",
|
|
||||||
"suggestedCode": "```\nfunction prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {\n return `${MARK}\n## 🔍 AI Code Review|已建立追蹤問題\n\n本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`;\n}\n```",
|
|
||||||
"sourceIssue": 19
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "F031",
|
"id": "F031",
|
||||||
"reviewer": "Leo",
|
"reviewer": "Leo",
|
||||||
@@ -246,34 +176,6 @@
|
|||||||
"suggestedCode": "",
|
"suggestedCode": "",
|
||||||
"sourceIssue": 21
|
"sourceIssue": 21
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "F032",
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 183,
|
|
||||||
"endLine": 224,
|
|
||||||
"problem": "`issue` 與 `issueBuffer` 的命名過於泛泛;在 Gitea 裡 PR 也是 issue,追蹤問題也是 issue,單靠 `issue` 這個名字無法唱出它究竟是哪一個聲部。",
|
|
||||||
"suggestion": "建議改成更具語義的名稱,例如 `trackingIssue`、`trackingIssueCommentBuffer`,讓讀者不用回讀 create-issue 模式的整段脈絡。",
|
|
||||||
"suggestedCode": "```\nlet trackingIssue = null;\nconst trackingIssueCommentBuffer = [];\n```",
|
|
||||||
"sourceIssue": 21
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "F033",
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/lib/gitea.js",
|
|
||||||
"startLine": 190,
|
|
||||||
"endLine": 194,
|
|
||||||
"problem": "`addIssueDependency(ctx, issueNumber, dependency)` 的兩個參數名稱太相似,且 `dependency` 少了 issue 語義。這支 API 的方向性本來就容易讀錯,命名再模糊就像兩個音符共用同一個名字。",
|
|
||||||
"suggestion": "建議把參數改成能表達方向的名稱,例如 `blockedIssueNumber` 與 `blockingIssueNumber`,呼叫端也會更清楚是誰被誰擋住。",
|
|
||||||
"suggestedCode": "```\nfunction addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {\n index: blockingIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```",
|
|
||||||
"sourceIssue": 21
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "F040",
|
"id": "F040",
|
||||||
"reviewer": "Assassin",
|
"reviewer": "Assassin",
|
||||||
|
|||||||
@@ -7,141 +7,7 @@
|
|||||||
"version": "codex-cli 0.144.6",
|
"version": "codex-cli 0.144.6",
|
||||||
"model": "gpt-5.5"
|
"model": "gpt-5.5"
|
||||||
},
|
},
|
||||||
"findings": [
|
"findings": [],
|
||||||
{
|
|
||||||
"reviewer": "Mage",
|
|
||||||
"focus": "logic",
|
|
||||||
"badge": "🔮",
|
|
||||||
"severity": "嚴重",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 149,
|
|
||||||
"endLine": 153,
|
|
||||||
"problem": "這個變更把「本輪審查」改成即使有嚴重問題也回傳 0,並假設後續由 `[ai-review-bot][failure]` 結果 commit 觸發下一輪 CI 再失敗。最小重現:workflow 仍依 action.yml 常見用法傳入自動 token(或任一不會觸發 synchronize CI 的 token)→ AI 找到 1 條嚴重問題 → 本輪成功建立留言與 push 結果 commit,但該 push 不觸發下一輪 → 沒有任何檢查讀到 `[failure]` commit,PR 最終呈現通過。這是未驗證的外部時序假設;嚴重 finding 會被靜默放行。",
|
|
||||||
"suggestion": "不要讓失敗判定完全依賴下一輪 CI。若 `severe.length > 0`,本輪在完成留言與結果落地後仍應回傳 1;或至少提供明確 input 控制 direct-fail,並在無法驗證 token 會觸發 CI 時預設直接 fail。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F009",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": false,
|
|
||||||
"reason": "保留。此條指控嚴重 finding 需仰賴下一輪 CI 才失敗,且自動 token 可能不觸發 CI,與既有 pushToken 測試缺口或 PAT 風險 finding 不屬同一處同一失效模式;未能排除。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Mage",
|
|
||||||
"focus": "logic",
|
|
||||||
"badge": "🔮",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/lib/review.js",
|
|
||||||
"startLine": 707,
|
|
||||||
"endLine": 718,
|
|
||||||
"problem": "`postOthersToIssue` 以並行方式送出多則 issue 留言時,issue 上的實際留言順序取決於 API 回應與資料庫寫入完成順序,不保證等於已排序的 findings 順序。最小重現:兩條建議分別位於 `a.js:10` 與 `a.js:20`,第二個 API 較快完成,issue 會先出現第 20 行問題,再出現第 10 行問題;使用者逐檔逐行處理時順序會錯亂。",
|
|
||||||
"suggestion": "若 issue 留言順序是介面契約,逐則 `await gitea.createCommentOnIssue(...)` 發送;若要保留並行,需在每則留言標題加入穩定序號,例如 `2/5`,讓非同步完成不破壞閱讀順序。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F011",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": false,
|
|
||||||
"reason": "保留。歷史 findings 未涵蓋 postOthersToIssue 並行送出導致 issue 留言順序不穩定;目前也無排除事項可套用,故保留。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "style",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "action.yml",
|
|
||||||
"startLine": 22,
|
|
||||||
"endLine": 24,
|
|
||||||
"problem": "manifest 的註解忽然奏起一整段實作細節:PAT、CI 觸發、主程式步驟 1 全擠在 input 說明旁,和同檔其他「介面用途」型註解的節奏不一致。讀者只是想知道 token 該填什麼,卻被迫聽完流程旁白。",
|
|
||||||
"suggestion": "把 action.yml 留給介面契約;細節移到 README 或主流程文件。此處可濃縮成「建議使用可觸發 CI 的 PAT」即可。",
|
|
||||||
"suggestedCode": "# 建議傳入能觸發 CI 的 PAT;自動 token 推送結果 commit 時可能不會再觸發 workflow。",
|
|
||||||
"id": "F002",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": false,
|
|
||||||
"reason": "保留。已知排除事項與歷史 findings 主要涵蓋 push-token 說明重複或 PAT 安全風險;本條指向 action.yml 中 token input 註解混入主流程步驟細節,並非同一處同一問題,未能排除。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "style",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "readme.md",
|
|
||||||
"startLine": 40,
|
|
||||||
"endLine": 47,
|
|
||||||
"problem": "mermaid 圖的節點 ID 旋律走岔了:畫面標示是 3→4→5→...→8→2→9,但節點名稱卻用 S3、S4、S2 來回跳。即使流程語意想表達「步驟 2 延後」,節點 ID 與視覺順序不一致,會讓維護者在對照圖與文字時多繞一圈。",
|
|
||||||
"suggestion": "讓節點 ID 維持閱讀順序,將真正的流程步驟放在節點文字裡。例如用 N2、N3 或 A、B 這類中性 ID,避免 S2 看起來像應該排在 S1 後面。",
|
|
||||||
"suggestedCode": "S1 -->|未命中| N3[3 偵測 AI 工具並留言]\n N3 --> N4[4 讀 .reviewignore 整理 diff 並留言]\n N4 --> N5[5 攻擊方登場留言]\n N5 --> N6[6 攻擊方 sub agent 並行找問題]\n N6 --> N7[7 防守方登場留言]\n N7 --> N8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json]\n N8 --> N2[2 延後將舊留言標記解決(成功產生結果後才執行)]\n N2 --> S9[9 嚴重問題逐條掛行留言]",
|
|
||||||
"id": "F003",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": false,
|
|
||||||
"reason": "保留。歷史 findings 雖有流程步驟硬編碼與文件同步成本問題,但本條聚焦 mermaid 節點 ID 與視覺流程順序不一致,屬不同可讀性指控,未命中既有排除。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "style",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 122,
|
|
||||||
"endLine": 139,
|
|
||||||
"problem": "`main()` 的 JSDoc 從函式說明變成流程章回。步驟、例外模式、issue 建立時機、相依關係、commit 規則全塞在同一段,和程式下方已存在的分段註解重複,讀起來像同一旋律被兩把琴同時彈奏。",
|
|
||||||
"suggestion": "JSDoc 保留函式職責、回傳值與關鍵模式差異即可;完整 10 步驟流程交給 README 或下方區塊註解。這會讓 `main()` 開頭更快進入正題。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F004",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": false,
|
|
||||||
"reason": "保留。歷史 findings 有步驟編號散落與 main() 內閉包 JSDoc 過重等問題,但本條指向 main() 函式 JSDoc 與下方流程註解重複,範圍與主張不完全相同,證據不足以排除。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "style",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/lib/diagnostics.js",
|
|
||||||
"startLine": 45,
|
|
||||||
"endLine": 47,
|
|
||||||
"problem": "`res` 這個參數名太短促,和檔內其他 `text`、`parts`、`stderr`、`stdout` 這些直白命名相比顯得含糊。診斷工具本該讓人少猜一點,這裡卻讓讀者先猜它是哪一種 result。",
|
|
||||||
"suggestion": "改用 `result` 或 `agentResult`,讓函式簽名本身就說清楚資料來源。",
|
|
||||||
"suggestedCode": "function agentFailureDetail(agentResult) {\n const parts = [];\n const err = agentResult && agentResult.error;",
|
|
||||||
"id": "F005",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": false,
|
|
||||||
"reason": "保留。未見已知排除事項或歷史 finding 涵蓋 diagnostics.js 中 res 參數命名過短的問題;屬新的命名可讀性指控。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "style",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "test/gitea.test.js",
|
|
||||||
"startLine": 8,
|
|
||||||
"endLine": 8,
|
|
||||||
"problem": "`fn` 是一個太倉促的縮寫,放在測試輔助函式裡尤其刺耳;同一行已有 `handler` 這種完整命名,`fn` 顯得像漏拍的音符。",
|
|
||||||
"suggestion": "改成 `callback` 或 `run`,讓呼叫意圖更清楚,也和此專案偏完整語意的命名風格一致。",
|
|
||||||
"suggestedCode": "function withFetchStub(handler, callback) {",
|
|
||||||
"id": "F006",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": false,
|
|
||||||
"reason": "保留。未見已知排除事項或歷史 finding 涵蓋 test/gitea.test.js 中 fn 測試輔助參數命名問題;屬新的命名可讀性指控。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"excluded": [
|
"excluded": [
|
||||||
{
|
{
|
||||||
"reviewer": "Assassin",
|
"reviewer": "Assassin",
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
{
|
||||||
|
"generatedAt": "2026/07/21 14:42:39",
|
||||||
|
"commitSha": "d424447d1502f49538fcb3093d23b5ac7bfa16c5",
|
||||||
|
"prNumber": 6,
|
||||||
|
"tool": {
|
||||||
|
"name": "codex",
|
||||||
|
"version": "codex-cli 0.144.6",
|
||||||
|
"model": "gpt-5.5"
|
||||||
|
},
|
||||||
|
"findings": [
|
||||||
|
{
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"focus": "style",
|
||||||
|
"badge": "🎼",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 139,
|
||||||
|
"endLine": 160,
|
||||||
|
"problem": "`runFetch`、`tryMergeBase`、`firstError`、`strategies` 夾在 `resolveMergeBase` 主旋律中,使這個函式同時負責驗證、fetch 策略編排、診斷文字組裝與錯誤包裝。即使邏輯可行,閱讀節奏已偏密,維護者很難一眼分辨「主要流程」與「補救策略」。",
|
||||||
|
"suggestion": "建議將 fetch 策略與診斷收集抽成小函式,例如 `fetchAndRecord`、`resolveMergeBaseWithStrategies`,讓 `resolveMergeBase` 保留高階流程:驗證 baseRef → fetch base → 嘗試 merge-base → 補抓歷史。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F003",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": false,
|
||||||
|
"reason": "保留。歷史 findings 主要涵蓋 resolveMergeBase 缺測試與診斷不足,本條指向函式內策略編排、診斷組裝與錯誤包裝混雜的可維護性問題,未被既有排除事項完整涵蓋。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"excluded": [
|
||||||
|
{
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"focus": "style",
|
||||||
|
"badge": "🎼",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 179,
|
||||||
|
"endLine": 212,
|
||||||
|
"problem": "`queueOrPostComment` 與 `createIssueAndFlushBufferedComments` 這兩個閉包名稱像兩段不同旋律:一個強調「排隊或發布」,另一個卻把「建立 issue、flush 暫存留言、設定狀態」全塞進名稱與實作。讀者要來回追 `trackingIssue`、`pendingIssueCommentBodies`,節奏偏長且語意負擔重。",
|
||||||
|
"suggestion": "建議統一命名語彙,例如把「暫存/發布」集中成 `postReviewComment`、`flushPendingIssueComments`,讓函式名稱只描述一件事;建立 issue 與 flush 留言也可拆成兩段,讀起來會更像樂句而不是長句。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F002",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 finding 已指出 main() 內新增閉包與 issue/comment 路由細節讓主流程閱讀負擔加重;本條只是改以目前函式名稱描述同一維護性問題。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"focus": "maintainability",
|
||||||
|
"badge": "🧰",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 165,
|
||||||
|
"endLine": 213,
|
||||||
|
"problem": "`main()` 這次把「留言目的地切換、issue 建立、暫存留言 flush、標籤挑選前置狀態」都塞進閉包與可變狀態(`pendingIssueCommentBodies`、`trackingIssue`、`currentRunCommentIds`)。半年後要改建問題模式時,維護者得同時追蹤主流程步驟、閉包副作用與留言落點,任何新增留言點都可能忘記處理 queue/flush 或 PR 排除清單,長期會讓流程編排變得很脆弱。",
|
||||||
|
"suggestion": "把留言目的地抽成明確的小型物件或模組,例如 `ReviewCommentSink` / `IssueCommentSink`,由它負責 `post()`、`flush()`、`currentRunCommentIds`。`main()` 只保留流程順序,不直接管理留言暫存與 issue 狀態。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F006",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(命中已知排除事項且重複)。已知與歷史 findings 已涵蓋 main() 內留言目的地、issue 狀態、緩衝佇列與可變閉包耦合,及應抽出發布器/sink 的方向。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"focus": "maintainability",
|
||||||
|
"badge": "🧰",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 302,
|
||||||
|
"endLine": 353,
|
||||||
|
"problem": "建問題模式的收束邏輯把「挑標籤、建立 issue、發 finding、回貼 PR、設定 issue dependency、決定是否阻擋合併」全部展開在 `main()`。這段和前面的 `createIssueAndFlushBufferedComments` 共同組成一條隱性的 issue workflow,但邊界沒有被封裝;未來要新增 issue 模板、改排序、改阻擋條件或重試策略時,會在主流程裡到處補條件,維護成本會快速上升。",
|
||||||
|
"suggestion": "把建問題模式整理成單一高階函式,例如 `review.publishIssueModeResults({ ctx, gitea, tool, model, cwd, kept, severe, others, bufferedComments })`,主流程只接收 `trackingIssue` / `filesToCommit` 等結果。這樣 PR 模式與 issue 模式的責任邊界會清楚很多。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F007",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。此條與 F006 及歷史 findings 同樣指向建問題模式在 main() 中承擔 issue workflow、發布分流與狀態管理,屬同一維護性問題的延伸描述。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"focus": "maintainability",
|
||||||
|
"badge": "🧰",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "readme.md",
|
||||||
|
"startLine": 56,
|
||||||
|
"endLine": 132,
|
||||||
|
"problem": "README 內大量維護到原始碼行號的連結,這次 diff 只是分支與行號位移就需要同步改一整片表格。這種文件和程式碼行號的硬耦合很容易在後續修改時過期,讀者點到錯誤位置,維護者也要花時間做機械式同步。",
|
||||||
|
"suggestion": "若這份表格是 API 索引,建議改成自動產生,或至少移除 `#Lxx` 行號錨點,改連到檔案或穩定章節錨點。讓文件描述 API,而不是跟著每次程式碼行號漂移。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F008",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已多次指出 README 功能表硬編分支與行號連結,導致文件與原始碼高度耦合且容易失準。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Mage",
|
||||||
|
"focus": "logic",
|
||||||
|
"badge": "🔮",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 251,
|
||||||
|
"endLine": 253,
|
||||||
|
"problem": "無可審查變更的分支在一般模式下會先把舊留言標為過時,之後才保存 findings 與 commit/push 結果。最小重現:`files.length === 0`、`nothingToReviewComment` 成功、`resolveOldComments` 成功,但接著 `saveFindings` 因檔案系統錯誤或 `commitFindings` 因 push 失敗拋例外。此時舊審查結果已被標過時,但沒有成功落地本回合的 success 結果 commit,狀態會停在半更新。",
|
||||||
|
"suggestion": "把 `review.resolveOldComments` 延後到 `saveFindings` 與必要的 `commitFindings` 成功之後;或至少在空變更路徑也遵守同一個交易順序:先產生並提交本回合結果,再清理舊留言。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F010",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(命中已知排除事項)。維護者已裁示舊留言應刻意在本回合結果發布前標為過時;本條要求延後清理,落在同一已排除範圍。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 160,
|
||||||
|
"endLine": 353,
|
||||||
|
"problem": "建問題模式的主流程被大幅改寫,但目前測試只驗證了少數 helper,沒有驗證「留言先暫存、確定有 kept finding 才建 issue、無保留問題時靜默通過、嚴重問題才加 issue dependency、最後回貼 PR 連結」這些新增流程。這些行為一旦順序或條件寫錯,測試不會擋下來。",
|
||||||
|
"suggestion": "請補 `main()` 層級的流程測試,stub `agents`、`review`、`gitea`、`gitrepo`,至少覆蓋:`createIssue=true && kept=[]` 不建立 issue/不貼 PR 留言;`kept` 只有警告/建議時建立 issue、flush 暫存留言、回貼 PR 連結但不呼叫 `addIssueDependency`;`kept` 含嚴重時呼叫 `addIssueDependency`,且 dependency 失敗時流程仍完成並 commit failure findings。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F011",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式 main() 層級流程缺測試,包括暫存留言、有 finding 才建 issue、無 finding 靜默、PR 回貼、嚴重與非嚴重分流及 dependency 失敗降級。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 307,
|
||||||
|
"endLine": 361,
|
||||||
|
"problem": "`pushWithCredential` 新增了關鍵認證行為,但沒有測試驗證它真的用 `GIT_CONFIG_*` 清掉 checkout 的 extraheader、再注入 PAT Authorization,也沒有測試遠端 URL 不符與 push 失敗時不洩漏 URL/token 的失敗路徑。這段是結果 commit 能否觸發下一輪 CI 的核心,沒有測試等於這個新契約還沒通過試煉。",
|
||||||
|
"suggestion": "請補推送行為測試。可用可注入的 exec wrapper 或子程序測試方式驗證:`git push` argv 不含 token、env 包含兩筆同 scope extraheader、`GIT_CONFIG_VALUE_0` 為空值、`GIT_CONFIG_VALUE_1` 為 Basic header;再補 `remote.origin !== server.origin` 與 exec 失敗時錯誤訊息不含 token/remoteUrl 的案例。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F012",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已記錄 pushWithCredential/pushToken 認證推送路徑缺少測試,包含 token 不進 argv、認證遮蔽、推送目標與失敗訊息不洩漏敏感資訊。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 130,
|
||||||
|
"endLine": 192,
|
||||||
|
"problem": "`resolveMergeBase` 新增多段 fetch fallback:先抓 base、再 deepen base、deepen PR HEAD、最後 shallow repo 才 unshallow,但現有測試只驗證不安全 baseRef 會被拒絕,沒有驗證淺層歷史補抓順序、每次 fetch 後會重試 merge-base、成功後會短路,也沒有覆蓋所有策略失敗時的診斷錯誤。",
|
||||||
|
"suggestion": "請補針對 `resolveMergeBase` 的失敗與邊界測試。建議把 git 執行函式抽成可替換依賴,或用臨時 git repo 模擬 shallow 狀態,斷言:首次 merge-base 成功不走 fallback;deepen base 成功後立即回傳;deepen PR HEAD 使用目前 HEAD SHA 而不是遠端 `HEAD`;非 shallow repo 不呼叫 `--unshallow`;全部失敗時錯誤包含策略診斷。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F013",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、成功短路、降級順序與最終失敗診斷缺測試提出相同問題。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/lib/diagnostics.js",
|
||||||
|
"startLine": 18,
|
||||||
|
"endLine": 69,
|
||||||
|
"problem": "`redactSecrets` 與 `agentFailureDetail` 是新加入的安全診斷防線,但測試只覆蓋 Authorization/token 的基本遮罩。控制字元單行化、URL 內嵌帳密、長 token/hex、輸入與輸出長度截斷、以及沒有 error code 時的 fallback 訊息都還沒被驗證。",
|
||||||
|
"suggestion": "請補診斷邊界測試,至少包含:含換行與 tab 的輸出不會產生多行 log;`https://user:pass@host` 會被遮罩;40 字元以上 token 會被遮罩;debug 模式下 stderr/stdout 只輸出到限制長度;沒有 `error.code`、`signal`、`stderr/stdout` 時仍回傳安全的 fallback 訊息。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F014",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已涵蓋 redactSecrets/agentFailureDetail 對控制字元、URL 帳密、token 格式、截斷與 fallback 診斷等邊界缺少測試。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
{
|
||||||
|
"generatedAt": "2026/07/21 16:00:33",
|
||||||
|
"commitSha": "216bc39255ec176a0f66f2b257beea5929ef4eb8",
|
||||||
|
"prNumber": 6,
|
||||||
|
"tool": {
|
||||||
|
"name": "codex",
|
||||||
|
"version": "codex-cli 0.144.6",
|
||||||
|
"model": "gpt-5.5"
|
||||||
|
},
|
||||||
|
"findings": [],
|
||||||
|
"excluded": [
|
||||||
|
{
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"focus": "style",
|
||||||
|
"badge": "🎼",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 166,
|
||||||
|
"endLine": 223,
|
||||||
|
"problem": "`main()` 這段新添的建問題模式狀態、暫存佇列、三個閉包輔助函式,全塞在主流程開頭,讓主旋律還沒開始就先進入一大段插曲。`issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies` 彼此共享可變狀態,讀者必須在腦中追蹤閉包副作用,主流程的步驟節奏因此變得沉重。",
|
||||||
|
"suggestion": "把留言去向抽成小型 helper(例如 `createCommentSink`),讓 `main()` 只看見 `commentSink.post()`、`commentSink.flushToIssue()`、`commentSink.fallbackToPr()` 這類語意清楚的介面。主流程保留編排,狀態管理移到專責函式,樂句會乾淨許多。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F002",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。此條指涉 main() 內 issue 模式狀態、暫存佇列與閉包職責混雜,已由既有排除事項與歷史 findings 多次涵蓋。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"focus": "style",
|
||||||
|
"badge": "🎼",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 230,
|
||||||
|
"endLine": 239,
|
||||||
|
"problem": "註解標成「步驟 2:延後執行」,但實際位置夾在快速檢查與步驟 3 之前,後面又在步驟 8 後再次出現「步驟 2(延後執行)」。同一個步驟號在不同位置反覆變奏,雖然註解有解釋,閱讀節拍仍容易打結。",
|
||||||
|
"suggestion": "將流程步驟編號與執行順序拆開命名,例如稱為「舊留言清理(延後)」或「收斂前清理舊留言」,避免用 `步驟 2` 這種線性編號描述一個實際延後執行的階段。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F003",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已指出流程步驟編號散落且實際順序變成 1、3~8、2、9~10,與本條「步驟 2 延後但仍用線性編號」相同。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"focus": "maintainability",
|
||||||
|
"badge": "🧰",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 166,
|
||||||
|
"endLine": 235,
|
||||||
|
"problem": "`main()` 現在同時負責流程編排、PR 留言、issue 模式暫存、issue 建立、fallback 狀態切換與留言 flush。這段靠 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 多個閉包變數互相配合,半年後要改「留言要發去哪裡」或「建 issue 失敗怎麼降級」時,很容易漏掉某個狀態轉換,尤其後面收尾 commit、resolve 舊留言、嚴重/其他問題發布都還會讀這些狀態。",
|
||||||
|
"suggestion": "把發布目的地抽成一個小型 publisher 模組或類別,讓 `main()` 只呼叫 `publisher.comment()`、`publisher.ensureIssue()`、`publisher.fallbackToPr()`、`publisher.resolveOldComments()` 這類語意方法。狀態留在 publisher 內部,並補單元測試覆蓋「一般模式、issue 模式有 finding、issue 模式無 finding、建立 issue 失敗降級」四條路徑。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F006",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(命中已知排除事項且重複)。main() 同時承擔留言路由、issue 狀態、fallback 與 flush 的發布狀態管理,已由既有排除事項與歷史 findings 涵蓋。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"focus": "maintainability",
|
||||||
|
"badge": "🧰",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 222,
|
||||||
|
"endLine": 230,
|
||||||
|
"problem": "註解與 log 名稱把「標記舊留言過時」稱為「步驟 2」,但實際執行點被延後到流程後段。這種非時間順序的步驟編號會讓維護者追 log 或對照 README 流程圖時產生認知落差:看到「步驟 2」不再代表第二個發生的動作,而是某個被延後的歷史步驟。",
|
||||||
|
"suggestion": "改用穩定的階段名稱取代數字,例如 `清理舊留言`、`工具偵測`、`diff 摘要`,或重新編號成實際執行順序。若一定要保留舊編號,建議只放在 README 遷移說明,不要散落在程式碼註解與 log label 中。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F007",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。與 F003 及歷史 findings 指涉同一個延後執行的「步驟 2」仍以線性步驟編號呈現所造成的認知落差。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"focus": "maintainability",
|
||||||
|
"badge": "🧰",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "readme.md",
|
||||||
|
"startLine": 76,
|
||||||
|
"endLine": 131,
|
||||||
|
"problem": "README 內大量手動維護到 `src/branch/develop/...#Lxx` 的精確行號連結。這次變更已經因程式碼位移而更新一整片連結,未來每次插入函式或註解都會造成文件 churn;更糟的是漏改時文件會指到錯誤行,讀者以為文件可信,實際上卻被帶到過期位置。",
|
||||||
|
"suggestion": "避免在手寫 README 維護精確行號。可改連到檔案或章節錨點,或把 API 列表改由文件產生器輸出。若需要固定定位,優先使用語意穩定的錨點或函式章節,而不是會隨程式碼行數漂移的 `#Lxx`。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F008",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。README 大量手動維護 develop 分支與 #Lxx 行號連結,歷史 findings 已多次記錄相同維護成本與連結漂移風險。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Mage",
|
||||||
|
"focus": "logic",
|
||||||
|
"badge": "🔮",
|
||||||
|
"severity": "嚴重",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 117,
|
||||||
|
"endLine": 120,
|
||||||
|
"problem": "當 PR 審查出嚴重問題,但結果 commit/push 失敗時,`commitFindings` 只回傳 `false`。新版主流程又改成「本輪審查不因嚴重問題直接 exit 1,而是依賴下一輪讀到 `[failure]` commit 才失敗」。最小情境:`severe.length > 0`、token 權限不足或 push 競態導致 `commitAndPushFindings` 丟錯;此時沒有 `[failure]` commit、也不會有下一輪快速回報,但本輪仍可能以 0 結束,PR 檢查會在存在嚴重問題時通過。",
|
||||||
|
"suggestion": "嚴重問題存在時,若 failure 結果 commit 沒有成功產生,必須讓本輪直接回傳 1。可保留「成功 push failure commit 時本輪回傳 0、下一輪失敗」的設計,但 push 失敗不可靜默通過。",
|
||||||
|
"suggestedCode": "const result = severe.length === 0 ? 'success' : 'failure';\nconst committed = commitFindings({ cwd, ctx, files: filesToCommit, result });\nif (result === 'failure' && !committed) {\n log('收尾', 'ERR', '有嚴重問題但無法推送 failure 結果 commit,本輪直接失敗。');\n return 1;\n}\nreturn 0;",
|
||||||
|
"id": "F010",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。此條與 F001 指涉同一風險:嚴重 finding 存在時 failure 結果 commit/push 失敗,可能使本輪檢查仍以成功結束。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 163,
|
||||||
|
"endLine": 437,
|
||||||
|
"problem": "建問題模式被大幅改寫:情境留言先暫存、只有 kept finding 才建 issue、建立失敗會 fallback 回 PR、嚴重/非嚴重 finding 會改發到 issue、最後還要回貼追蹤 issue 連結與設定相依關係。但目前新增測試只覆蓋了少數 helper,沒有驗證 `main()` 在這些分支下的實際副作用。這些行為沒經過試煉,等於還不知道留言會不會發錯地方、fallback 後會不會漏清舊留言、或無 finding 時是否真的靜默通過。",
|
||||||
|
"suggestion": "請補 `src/index.js` 的流程層級測試,mock `gitea`、`review`、`agents`、`gitrepo` 等依賴,至少覆蓋:\n\n| 情境 | 應驗證 |\n| --- | --- |\n| `createIssue=true` 且 `kept.length===0` | 不建 issue、不發 PR 留言、暫存情境留言不外洩 |\n| `createIssue=true` 且有 finding | 先建 issue,再依序 flush 工具/diff/角色留言,finding 發到 issue,PR 只留下追蹤 issue 連結 |\n| 建 issue 或寫入 issue 失敗 | fallback 後情境留言改發 PR,後續使用一般模式留言與舊留言清理 |\n| 嚴重 finding | 結果 commit 標記為 failure,但本輪 `main()` 回傳 0,下一輪才由 bot commit 回報 1 |",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F011",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、只在有 finding 時建 issue、fallback、嚴重/非嚴重分流、PR 回貼與靜默通過等核心分支缺少流程測試。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 298,
|
||||||
|
"endLine": 355,
|
||||||
|
"problem": "`commitAndPushFindings` 的推送行為改成一律走 `pushWithCredential`,並宣稱 token 不進 argv、會清掉 checkout 的 extraheader、只接受同一 Gitea origin 的 `.git` URL。這是安全與 CI 觸發都很關鍵的失敗路徑,但目前 `test/gitrepo.test.js` 只測了分支名稱檢查,沒有驗證推送時的 argv/env,也沒有驗證遠端 URL 不符時會停止。這段若改壞,測試不會提醒我們 token 可能出現在命令列或推送根本沒用 PAT 身分。",
|
||||||
|
"suggestion": "請補針對 `commitAndPushFindings`/`pushWithCredential` 的測試。建議用可替換的 `execFileSync` seam 或測試專用注入方式,斷言:\n\n| 情境 | 斷言方向 |\n| --- | --- |\n| 正常推送 | `git push` argv 只含不帶帳密的 `remoteUrl` 與 refspec |\n| 正常推送 | env 含 `GIT_CONFIG_COUNT=2`,先清空 `http.<origin>/.extraheader`,再注入 `Authorization: Basic ...` |\n| `remoteUrl` origin 不符或非 `.git` | 拋出固定錯誤且不執行 push |\n| push 失敗 | 對外錯誤訊息不含 token、remote URL 或 argv |",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F012",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已記錄 commitAndPushFindings/pushWithCredential 的認證推送路徑、argv/env、遠端目標檢查與失敗訊息遮蔽缺少測試。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 130,
|
||||||
|
"endLine": 188,
|
||||||
|
"problem": "`resolveMergeBase` 新增了多階段 fetch 補歷史流程:先 fetch base、首次 merge-base、再 deepen base、deepen PR HEAD、必要時 unshallow,且每個策略成功後要立即重試並短路返回。現有測試只驗證不安全 `baseRef` 會在 fetch 前被拒絕,沒有測到淺層 checkout、首次失敗後補抓成功、策略失敗繼續下一個、全部失敗時診斷訊息等邊界。這正是容易 off-by-one 或順序錯的流程,現在還沒有測試保護。",
|
||||||
|
"suggestion": "請補資料驅動的 `resolveMergeBase` 測試,mock git 執行結果來驗證:首次 merge-base 成功時不跑 deepen;deepen base 後成功會立刻回傳;deepen PR HEAD 使用目前 `HEAD` SHA 而不是遠端符號 `HEAD`;非 shallow repo 不呼叫 `--unshallow`;全部策略失敗時錯誤包含各策略診斷且保留 cause。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F013",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。resolveMergeBase 多階段 fetch、淺層與非淺層路徑、策略停止條件、全部失敗診斷與 cause 缺少測試,已由歷史 findings 涵蓋。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
{
|
||||||
|
"generatedAt": "2026/07/21 16:30:12",
|
||||||
|
"commitSha": "43ad2b56d27429aa3c812522dc1d4fedce4a9b76",
|
||||||
|
"prNumber": 6,
|
||||||
|
"tool": {
|
||||||
|
"name": "codex",
|
||||||
|
"version": "codex-cli 0.144.6",
|
||||||
|
"model": "gpt-5.5"
|
||||||
|
},
|
||||||
|
"findings": [
|
||||||
|
{
|
||||||
|
"reviewer": "Mage",
|
||||||
|
"focus": "logic",
|
||||||
|
"badge": "🔮",
|
||||||
|
"severity": "嚴重",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 134,
|
||||||
|
"endLine": 139,
|
||||||
|
"problem": "這裡把「本輪審查」改成即使找到嚴重 finding 也回傳 0,並依賴後續 `[ai-review-bot][failure]` 結果 commit 觸發下一輪才讓檢查失敗。最小重現情境:PR 內有 1 條嚴重問題,但 `commitFindings()` 因分支保護、token 無 push 權限、遠端競態或 PAT 無法觸發 CI 而回傳 false;本輪仍成功結束,且沒有下一輪 failure commit 可被步驟 1 讀到,嚴重問題就被靜默放行。未驗證「結果 commit 一定成功且一定觸發下一輪」這個假設時,它就是流程成敗判定的單點失效。",
|
||||||
|
"suggestion": "嚴重 finding 已確認後,若 failure 結果 commit/push 沒有成功產生,就應在本輪直接回傳 1;只有在確認 failure commit 已成功推送時,才可把失敗狀態交給下一輪快速回報。也就是:`severe.length > 0 && !resultCommitPushed` 必須阻擋。",
|
||||||
|
"suggestedCode": "const result = severe.length === 0 ? 'success' : 'failure';\nlet pushed = false;\nif (filesToCommit.length > 0) {\n pushed = commitFindings({ cwd, ctx, files: filesToCommit, result });\n}\n\nif (severe.length > 0 && !pushed) {\n log('收尾', 'ERR', '已有嚴重問題,但 failure 結果 commit 未成功產生;本輪直接以失敗收場。');\n return 1;\n}\n\nreturn 0;",
|
||||||
|
"id": "F009",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": false,
|
||||||
|
"reason": "保留。此條指控嚴重 finding 依賴結果 commit 觸發下一輪才失敗,且 commit/push 失敗時本輪可能靜默通過;既有排除事項未涵蓋此成敗判定風險。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Mage",
|
||||||
|
"focus": "logic",
|
||||||
|
"badge": "🔮",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 201,
|
||||||
|
"endLine": 209,
|
||||||
|
"problem": "`createIssueAndFlushBufferedComments()` 先建立 `trackingIssue`,再逐則寫入暫存留言;但只要其中一則留言失敗,外層 catch 會呼叫 `fallbackToPrComments()`,把整批 `pendingIssueCommentBodies` 全部改貼回 PR。最小重現情境:追蹤 issue 建立成功,第一則工具留言也成功寫入 issue,第二則 diff 留言 API 回 500;流程降級後 PR 會收到全部暫存留言,而已建立的 issue 仍殘留第一則留言、沒有後續 finding、也可能沒有 PR 連結或相依關係。這會留下部分成功、部分降級的不一致狀態。",
|
||||||
|
"suggestion": "flush 時應在每則留言成功後立刻從 pending 佇列移除,並明確處理「issue 已建立但 flush 失敗」的狀態:要嘛繼續沿用已建立 issue 並讓後續失敗冒泡,要嘛在降級前補一則 PR 診斷/連結並避免重貼已成功寫入 issue 的留言。",
|
||||||
|
"suggestedCode": "while (pendingIssueCommentBodies.length > 0) {\n const body = pendingIssueCommentBodies[0];\n await gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n pendingIssueCommentBodies.shift();\n}",
|
||||||
|
"id": "F010",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": false,
|
||||||
|
"reason": "保留。既有排除事項涵蓋 issue 建立後留言失敗導致重跑重複 issue,但本條另指出 flush 部分成功後 fallback 會把整批 pending 留言改貼 PR,造成 issue 與 PR 狀態不一致;不是單純重複。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"focus": "style",
|
||||||
|
"badge": "🎼",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "action.yml",
|
||||||
|
"startLine": 3,
|
||||||
|
"endLine": 3,
|
||||||
|
"problem": "手動維護的「更新時間」已與本次檔案標示的最後更新時間不同步;樂譜開頭的拍號一錯,讀者後面每段註解都會多一分懷疑。同樣的不協調也出現在 `readme.md` 與 `src/index.js` 的 banner/文件時間。",
|
||||||
|
"suggestion": "移除這類容易走調的手動時間戳,或改由發布流程自動產生。若一定要保留,請讓所有檔案的時間標示與本次變更一致。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F001",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": false,
|
||||||
|
"reason": "保留。已知排除事項與歷史 findings 未涵蓋手動更新時間不同步;目前也無足夠原始碼證據可判定為誤報。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"focus": "style",
|
||||||
|
"badge": "🎼",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 199,
|
||||||
|
"endLine": 199,
|
||||||
|
"problem": "`createIssueAndFlushBufferedComments` 這個名稱把「建立 issue」與「flush 暫存留言」兩個實作細節硬串在一起,像一句過長的歌詞;呼叫點讀起來偏機械,沒有清楚表達業務意圖。",
|
||||||
|
"suggestion": "改成較語意化的命名,例如 `openTrackingIssue` 或 `createTrackingIssueWithContext`,讓讀者先理解目的,再從函式內容看見 flush 的細節。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F004",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": false,
|
||||||
|
"reason": "保留。此條針對 createIssueAndFlushBufferedComments 的命名語意與可讀性,未明確命中既有排除事項;與先前 ensureIssueCreated 暗示冪等的問題不完全相同。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/lib/gitref.js",
|
||||||
|
"startLine": 12,
|
||||||
|
"endLine": 24,
|
||||||
|
"problem": "assertSafeBranchRef 目前只測了一個正常分支與一個 ../../ 路徑穿越案例;但這個函式承擔 refspec 安全邊界,新增的空值、前後斜線、反斜線、以及 git check-ref-format 拒絕的格式都沒有案例。邊界沒被驗證時,未來調整條件很容易放過不合法 ref。",
|
||||||
|
"suggestion": "補上表格測試,至少涵蓋空字串、純空白、/feature、feature/、feature\\\\x、feature..x、feature.lock、feature@{x},並斷言錯誤訊息能區分「不安全」與「不合法 git 分支名稱」。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F014",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": false,
|
||||||
|
"reason": "保留。此條針對 assertSafeBranchRef 的 refspec 安全邊界測試不足,未見已知排除事項或歷史 findings 明確涵蓋;證據不足以排除。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"excluded": [
|
||||||
|
{
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"focus": "style",
|
||||||
|
"badge": "🎼",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "readme.md",
|
||||||
|
"startLine": 48,
|
||||||
|
"endLine": 48,
|
||||||
|
"problem": "Mermaid 節點代號從 `N8` 跳回 `N2`,雖然流程語意是「步驟 2 延後」,但原始碼讀起來像旋律突然倒帶;維護者在對照圖與步驟時容易被代號順序絆住。",
|
||||||
|
"suggestion": "節點 id 建議改用語意名稱,而把顯示文字保留步驟編號。例如 `ResolveOldComments[2 延後將舊留言標記解決...]`,讓圖的原始碼與顯示內容各自清楚。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F002",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已指出流程步驟編號與實際順序、README 流程圖及文件對照會產生認知落差;本條 Mermaid 節點代號問題屬同一類指控。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"focus": "style",
|
||||||
|
"badge": "🎼",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 167,
|
||||||
|
"endLine": 235,
|
||||||
|
"problem": "`main()` 內新增了建 issue 模式狀態、暫存佇列與三個帶長篇 JSDoc 的閉包,讓主流程的旋律被大量伴奏蓋過。這段同時管理 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies` 與 PR 留言 id,可讀性負擔明顯升高。",
|
||||||
|
"suggestion": "把留言路由與建 issue 暫存邏輯抽成小型 helper(例如 `createCommentRouter`),`main()` 只保留流程編排;JSDoc 也移到 helper 旁,讓主流程步驟重新一眼可讀。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F003",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。已知排除事項與歷史 findings 已涵蓋 main() 內留言路由、issue 狀態、暫存佇列與發布職責耦合造成可讀性與維護負擔。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Bard",
|
||||||
|
"focus": "style",
|
||||||
|
"badge": "🎼",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 121,
|
||||||
|
"endLine": 128,
|
||||||
|
"problem": "`strategies` 以陣列位置承載語意,`[label, ...args]` 雖簡短,卻讓每個元素的第二欄之後全靠讀者猜是 git argv;這裡的可讀性像沒有小節線的樂譜。",
|
||||||
|
"suggestion": "改成物件陣列,明確標出 `label` 與 `args`,可降低後續新增 fetch 策略時的閱讀成本。",
|
||||||
|
"suggestedCode": "const strategies = [\n { label: 'deepen base', args: ['fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`] },\n { label: 'deepen PR HEAD', args: ['fetch', '--no-tags', '--deepen=1000', 'origin', headSha] },\n];",
|
||||||
|
"id": "F005",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 finding 已針對 resolveMergeBase 內 strategies 與補救策略編排使閱讀節奏偏密提出問題;本條 tuple 可讀性屬同一段策略結構的細項。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"focus": "maintainability",
|
||||||
|
"badge": "🧰",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 163,
|
||||||
|
"endLine": 231,
|
||||||
|
"problem": "`main()` 這次把一般模式/建問題模式/降級模式的留言去向都塞進閉包狀態:`issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 彼此耦合。半年後要新增一種落地方式或調整「何時清舊留言」時,維護者必須同時追多個可變狀態與後續多個 `if (issueModeActive)` 分支,很容易漏掉 flush、清空、登錄留言 id 或 fallback 後的收尾行為。",
|
||||||
|
"suggestion": "把留言目的地抽成小型物件,讓主流程只呼叫 `commentSink.postContext()`、`commentSink.finalizeAfterFindings()`、`commentSink.postFinding()` 這類語意方法。一般 PR、issue buffering、fallback 的狀態轉換集中在同一個模組或工廠函式內,`main()` 保持審查流程編排,不承擔留言狀態機。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F006",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。與 F003 及既有排除事項相同,均指向 main() 以可變閉包狀態承擔留言目的地、issue 生命週期與 fallback 狀態機。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"focus": "maintainability",
|
||||||
|
"badge": "🧰",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 232,
|
||||||
|
"endLine": 241,
|
||||||
|
"problem": "流程註解把「將舊留言標記為解決」稱為「步驟 2(延後執行)」,但實際執行點在審查結果產生後、接近原本步驟 9 前。這種「編號是 2、時間點是 8 後」的設計已經擴散到多個檔案的 JSDoc 與 log 描述,未來查 CI log 或維護流程圖時會產生認知落差:看到 `步驟2` 不代表它真的發生在第二步。",
|
||||||
|
"suggestion": "不要用固定序號承載流程語意。建議改成穩定階段名稱,例如 `清理舊留言`、`工具偵測`、`diff整理`,或重新編號為實際執行順序並在文件說明「清理舊留言延後到結果產生後」。這樣 log、JSDoc、README 流程圖才不會互相背離。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F007",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已明確記錄步驟編號散落於 log、JSDoc、README 且實際順序變成 1、3 到 8、2、9 到 10 的維護風險。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Leo",
|
||||||
|
"focus": "maintainability",
|
||||||
|
"badge": "🧰",
|
||||||
|
"severity": "建議",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 103,
|
||||||
|
"endLine": 160,
|
||||||
|
"problem": "`resolveMergeBase()` 內同時負責驗證 base ref、fetch base、重試 merge-base、補抓淺層歷史、累積診斷與組錯誤訊息。這些都和「取得 merge-base」有關,但現在全部攤在同一個函式裡,策略陣列、診斷格式與 git 操作細節混在一起;未來要新增 fetch 策略或改診斷文字時,容易不小心改壞主流程判斷。",
|
||||||
|
"suggestion": "把補抓策略與重試邏輯拆成私有 helper,例如 `fetchBaseRef()`、`mergeBaseOrNull()`、`shallowFetchStrategies()`。主函式保留高階流程:驗證 ref → 更新 base → 嘗試 merge-base → 逐一套用策略 → 丟出診斷錯誤。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F008",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 finding 已指出 resolveMergeBase 同時負責驗證、fetch 策略編排、診斷與錯誤包裝,難以分辨主流程與補救策略。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/index.js",
|
||||||
|
"startLine": 164,
|
||||||
|
"endLine": 232,
|
||||||
|
"problem": "建問題模式新增了「先暫存情境留言、確定有保留 finding 才建 issue、建 issue 失敗時降級回 PR 留言」這一整段流程,但目前測試只覆蓋了部分 helper,沒有驗證主流程在 create-issue=true 下的分流行為。這代表像「無保留問題時不應建 issue/不應留言」、「建立 issue 失敗時應把 pending 留言補回 PR」、「trackingIssue 建立後後續留言應改送 issue」這些行為都還沒通過試煉。",
|
||||||
|
"suggestion": "建議把 main 流程拆出可注入 gitea/review/agents/gitrepo 的 orchestrator,或至少匯出可測的流程函式,補上 create-issue=true 的案例:1. kept=[] 時不呼叫 createIssue/createIssueComment;2. kept>0 時先 flush pending issue comments 再發 finding;3. createIssue 拋錯時 fallbackToPrComments 會發回 PR 並記錄 currentRunCommentIds;4. issueModeActive 降級後會走一般 PR 留言與 resolveOldComments。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F011",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式核心流程、暫存留言、issue 建立條件、發布分流、外部 API 狀態切換與降級路徑缺少測試。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 104,
|
||||||
|
"endLine": 150,
|
||||||
|
"problem": "resolveMergeBase 新增了多階段 fetch/deepen/unshallow fallback 與診斷錯誤,但測試只驗證不安全 baseRef 會被拒絕,沒有驗證任何成功或失敗 fallback 路徑。這段是 diff 基準的核心邏輯,若 shallow checkout 下 deepen PR HEAD 用錯 ref、策略順序跑錯,或全部失敗時診斷不正確,現有測試都抓不到。",
|
||||||
|
"suggestion": "建議用假的 git 執行層或臨時 git repo 補測:首次 merge-base 成功時不跑後續策略;首次失敗但 deepen base 後成功;deepen base 失敗會繼續 deepen PR HEAD;shallow repo 才嘗試 --unshallow;全部失敗時錯誤訊息包含各策略診斷且保留 cause。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F012",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、策略停止條件、降級與最終診斷缺少測試提出相同問題。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reviewer": "Maya",
|
||||||
|
"focus": "testing",
|
||||||
|
"badge": "🧪",
|
||||||
|
"severity": "警告",
|
||||||
|
"file": "src/lib/gitrepo.js",
|
||||||
|
"startLine": 272,
|
||||||
|
"endLine": 322,
|
||||||
|
"problem": "commitAndPushFindings 改成一律透過 pushWithCredential 用 PAT extraheader 推送,且新增 headRef 安全檢查與固定錯誤遮蔽;但目前 gitrepo 測試沒有覆蓋 commitAndPushFindings/pushWithCredential 的推送行為。這讓「token 不進 argv」、「清掉 checkout 既有 extraheader」、「遠端 URL 不符時拒絕」、「push 失敗不洩漏 URL/token」這些失敗與安全邊界都沒有被驗證。",
|
||||||
|
"suggestion": "建議以 stub child_process.execFileSync 或可注入 git runner 的方式補測:commit 後呼叫 git push 的 argv 不含 token;env 內含 GIT_CONFIG_COUNT 與 Authorization extraheader;headRef 不安全時在 git add/commit/push 前拋錯;remote origin 不符與 push 失敗時錯誤訊息不包含 token、repo URL 或 Basic header。",
|
||||||
|
"suggestedCode": "",
|
||||||
|
"id": "F013",
|
||||||
|
"verdicts": {
|
||||||
|
"Paladin": {
|
||||||
|
"exclude": true,
|
||||||
|
"reason": "可排除(重複)。歷史 findings 已涵蓋 commitAndPushFindings 與 pushWithCredential 的推送策略、認證遮蔽、失敗路徑與 token 不外洩等測試缺口。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+3
-5
@@ -16,13 +16,11 @@ 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:用於對 PR/issue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。
|
# Gitea API token:用於對 PR/issue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。
|
||||||
token:
|
token:
|
||||||
# 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。
|
# 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。
|
||||||
# 建議傳入「能觸發 CI 的 PAT」:以自動 token(gitea.token / GITHUB_TOKEN)推送的結果 commit 不會
|
# 建議傳入能觸發 CI 的 PAT;自動 token 推送結果 commit 時可能不會再觸發 workflow。
|
||||||
# 再觸發 CI,導致新 head 缺檢查而卡合併;改用 PAT 推送會讓 PR 的 synchronize 事件再觸發 CI,
|
description: 'Gitea API token(PR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)'
|
||||||
# 由主程式步驟 1 快速回報([success]/[failure])廉價地把結果蓋到新 head。
|
|
||||||
description: 'Gitea API token(PR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)'
|
|
||||||
# 必填:缺少 token 無法呼叫 Gitea API,action 無法運作。
|
# 必填:缺少 token 無法呼叫 Gitea API,action 無法運作。
|
||||||
required: true
|
required: true
|
||||||
# 指定 AI 工具使用的模型名稱。
|
# 指定 AI 工具使用的模型名稱。
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ 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 -->|未命中| N3[3 偵測 AI 工具並留言]
|
||||||
S3 --> S4[4 讀 .reviewignore 整理 diff 並留言]
|
N3 --> N4[4 讀 .reviewignore 整理 diff 並留言]
|
||||||
S4 --> S5[5 攻擊方登場留言]
|
N4 --> N5[5 攻擊方登場留言]
|
||||||
S5 --> S6[6 攻擊方 sub agent 並行找問題]
|
N5 --> N6[6 攻擊方 sub agent 並行找問題]
|
||||||
S6 --> S7[7 防守方登場留言]
|
N6 --> N7[7 防守方登場留言]
|
||||||
S7 --> S8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json]
|
N7 --> N8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json]
|
||||||
S8 --> S2[2 延後將舊留言標記解決(成功產生結果後才執行)]
|
N8 --> N2[2 延後將舊留言標記解決(成功產生結果後才執行)]
|
||||||
S2 --> S9[9 嚴重問題逐條掛行留言]
|
N2 --> N9[9 嚴重問題逐條掛行留言]
|
||||||
S9 --> S10[10 警告+建議彙整表格留言]
|
N9 --> N10[10 警告+建議彙整表格留言]
|
||||||
S10 --> E1[收尾 commit/push + exit code]
|
N10 --> E1[收尾 commit/push + exit code]
|
||||||
```
|
```
|
||||||
|
|
||||||
## 專案列表
|
## 專案列表
|
||||||
|
|||||||
+71
-61
@@ -72,7 +72,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) {
|
|||||||
* 供下一回合 `main()` 步驟 1 比對辨識、直接回報結果而不重複審查。
|
* 供下一回合 `main()` 步驟 1 比對辨識、直接回報結果而不重複審查。
|
||||||
* 依 `commitAndPushFindings` 的回傳值記錄不同日誌:true=已 commit/push;
|
* 依 `commitAndPushFindings` 的回傳值記錄不同日誌:true=已 commit/push;
|
||||||
* false=檔案無實際變更(空 commit 防護),記「略過 commit/push」。
|
* false=檔案無實際變更(空 commit 防護),記「略過 commit/push」。
|
||||||
* commit/push 失敗(例如與開發者新 commit 競態)時僅記 WRN log,不拋出例外、不改變審查結果。
|
* commit/push 失敗(例如與開發者新 commit 競態)時僅記 WRN log,不拋出例外;呼叫端可依回傳值決定是否阻擋。
|
||||||
*
|
*
|
||||||
* @param {Object} params - 解構參數。
|
* @param {Object} params - 解構參數。
|
||||||
* @param {string} params.cwd - repo 根目錄(workspace)絕對路徑,git 操作在此目錄執行。
|
* @param {string} params.cwd - repo 根目錄(workspace)絕對路徑,git 操作在此目錄執行。
|
||||||
@@ -84,7 +84,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) {
|
|||||||
* @param {string} params.ctx.repository - `owner/repo` 形式的 repo 名稱。
|
* @param {string} params.ctx.repository - `owner/repo` 形式的 repo 名稱。
|
||||||
* @param {string[]} params.files - 要 commit 的檔案 repo 相對路徑陣列(如 findings 檔、`.gitea/ai-review/exclusions.json`);全數無變更時只記 INF 略過。
|
* @param {string[]} params.files - 要 commit 的檔案 repo 相對路徑陣列(如 findings 檔、`.gitea/ai-review/exclusions.json`);全數無變更時只記 INF 略過。
|
||||||
* @param {'success'|'failure'} params.result - 本回合審查結果:success=無嚴重問題、failure=有嚴重問題;會拼進 commit 訊息尾端。
|
* @param {'success'|'failure'} params.result - 本回合審查結果:success=無嚴重問題、failure=有嚴重問題;會拼進 commit 訊息尾端。
|
||||||
* @returns {void} 無回傳值;成敗僅反映在 log 上。
|
* @returns {boolean} true=已 commit/push;false=無變更或 commit/push 失敗。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、
|
* 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、
|
||||||
* 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json;
|
* 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json;
|
||||||
@@ -107,41 +107,24 @@ function commitFindings({ cwd, ctx, files, result }) {
|
|||||||
});
|
});
|
||||||
if (committed) {
|
if (committed) {
|
||||||
log('收尾', 'INF', `審查結果檔已 commit 並 push 回 ${ctx.headRef}(結果:${result})。`);
|
log('收尾', 'INF', `審查結果檔已 commit 並 push 回 ${ctx.headRef}(結果:${result})。`);
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
log('收尾', 'INF', '審查結果檔無實際變更,略過 commit/push。');
|
log('收尾', 'INF', '審查結果檔無實際變更,略過 commit/push。');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// push 失敗(例如與開發者新 commit 競態)時只記錄,不改變審查結果。
|
// push 失敗(例如與開發者新 commit 競態)時只記錄,交由呼叫端依嚴重度決定是否阻擋。
|
||||||
log('收尾', 'WRN', `commit/push 審查結果檔失敗:${err.message}。`);
|
log('收尾', 'WRN', `commit/push 審查結果檔失敗:${err.message}。`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI code review 主流程:依固定 10 步驟執行多角色審查,回傳 process exit code。
|
* AI code review 主流程:編排多角色審查、發布審查結果,並回傳 process exit code。
|
||||||
*
|
*
|
||||||
* 流程概要(步驟 2~10 描述一般模式;建問題模式差異見末段):
|
* 一般模式會把審查情境、嚴重問題與警告/建議發布到 PR,並在成功產生本回合結果後才把舊留言標為過時。
|
||||||
* 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審;
|
* 建問題模式會把審查情境與每條 finding 發到追蹤 issue;沒有保留 finding 時不建立 issue、PR 也不留言。
|
||||||
* 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步)——
|
* 嚴重 finding 會寫入 failure 結果 commit,警告與建議只建立追蹤資訊,不直接阻擋合併。
|
||||||
* 此步延後到「本回合審查已成功產生結果、即將發布問題留言前」才執行,避免工具偵測/diff/
|
|
||||||
* 攻防裁決任一失敗時舊結果先被清掉卻沒有新結果(一般模式);
|
|
||||||
* 3. 偵測 AI 工具(antigravity/codex/claude)並留言;
|
|
||||||
* 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings,
|
|
||||||
* 一般模式 commit success、建問題模式略過 commit,回傳 0);
|
|
||||||
* 5–6. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題;
|
|
||||||
* 7–8. 防守方登場留言、裁決誤報後排序並保存 findings JSON,
|
|
||||||
* 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json;
|
|
||||||
* 9. 嚴重問題逐條掛在程式碼行上留言;
|
|
||||||
* 10. 警告+建議彙整為單一表格留言;
|
|
||||||
* 建問題模式(input: create-issue):不執行步驟 2、不觸碰 PR 既有留言;步驟 3~10 的所有留言
|
|
||||||
* 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後先挑好標籤、連同標籤一次建立 issue
|
|
||||||
* 並寫入暫存留言,嚴重問題與警告+建議再逐條發到該 issue,讓每條問題都能被個別回覆);
|
|
||||||
* 無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過);
|
|
||||||
* 收束時在 PR 回貼 issue 連結形成雙向關聯,
|
|
||||||
* 並「僅在有嚴重問題時」讓 PR 相依於該 issue(addIssueDependency,issue 關閉前 PR 無法合併;
|
|
||||||
* 需 repo 啟用問題相依功能)——僅有警告/建議時 issue 仍建立供追蹤,但不阻擋合併;
|
|
||||||
* 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、
|
|
||||||
* 建問題模式只 commit exclusions.json、無檔案可 commit 時略過;
|
|
||||||
* commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。
|
|
||||||
*
|
*
|
||||||
* @returns {Promise<number>} process exit code:本輪「審查」一律回傳 0(不因嚴重問題直接讓檢查失敗——
|
* @returns {Promise<number>} process exit code:本輪「審查」一律回傳 0(不因嚴重問題直接讓檢查失敗——
|
||||||
* 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報);
|
* 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報);
|
||||||
@@ -182,14 +165,15 @@ async function main() {
|
|||||||
|
|
||||||
// 本回合(一般模式)發出的 PR 留言 id:resolveOldComments 標註過時時要跳過這些。
|
// 本回合(一般模式)發出的 PR 留言 id:resolveOldComments 標註過時時要跳過這些。
|
||||||
const currentRunCommentIds = new Set();
|
const currentRunCommentIds = new Set();
|
||||||
// 建問題模式:issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色)
|
// 建問題模式:追蹤 issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色)
|
||||||
// 先暫存於 issueBuffer,建立 issue 後一次寫入。
|
// 先暫存於 pendingIssueCommentBodies,建立 issue 後一次寫入。
|
||||||
const issueBuffer = [];
|
const pendingIssueCommentBodies = [];
|
||||||
let issue = null;
|
let issueModeActive = ctx.createIssue;
|
||||||
|
let trackingIssue = null;
|
||||||
/**
|
/**
|
||||||
* 發布一則審查留言。依模式決定去向:
|
* 發布一則審查留言。依模式決定去向:
|
||||||
* - 一般模式:發到 PR,並記錄留言 id 供 `resolveOldComments` 排除。
|
* - 一般模式:發到 PR,並記錄留言 id 供 `resolveOldComments` 排除。
|
||||||
* - 建問題模式:issue 已建立時發到 issue;尚未建立時先暫存到 `issueBuffer`。
|
* - 建問題模式:追蹤 issue 已建立時發到 issue;尚未建立時先暫存到 `pendingIssueCommentBodies`。
|
||||||
*
|
*
|
||||||
* @param {string} body 要發布的 Markdown 留言內容。
|
* @param {string} body 要發布的 Markdown 留言內容。
|
||||||
* @returns {Promise<Object|null>} 一般模式、或建問題模式且 issue 已建立時回傳 Gitea 留言物件;
|
* @returns {Promise<Object|null>} 一般模式、或建問題模式且 issue 已建立時回傳 Gitea 留言物件;
|
||||||
@@ -199,9 +183,9 @@ async function main() {
|
|||||||
* 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。
|
* 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。
|
||||||
*/
|
*/
|
||||||
const queueOrPostComment = async (body) => {
|
const queueOrPostComment = async (body) => {
|
||||||
if (ctx.createIssue) {
|
if (issueModeActive) {
|
||||||
if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body);
|
if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);
|
||||||
issueBuffer.push(body);
|
pendingIssueCommentBodies.push(body);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const created = await gitea.createIssueComment(ctx, body);
|
const created = await gitea.createIssueComment(ctx, body);
|
||||||
@@ -210,24 +194,39 @@ async function main() {
|
|||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
|
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
|
||||||
* 並把 `issueBuffer` 內暫存的情境留言依流程順序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
|
* 並把 `pendingIssueCommentBodies` 內暫存的情境留言依流程順序寫入 issue;
|
||||||
|
* 設定閉包變數 `trackingIssue` 供後續留言直接發到 issue。
|
||||||
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
|
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
|
||||||
*
|
*
|
||||||
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
|
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
|
||||||
* 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。
|
* 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。
|
||||||
* @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。
|
* @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `trackingIssue` 與 issue 留言。
|
||||||
*/
|
*/
|
||||||
const createIssueAndFlushBufferedComments = async (labelIds = []) => {
|
const createIssueAndFlushBufferedComments = async (labelIds = []) => {
|
||||||
issue = await gitea.createIssue(ctx, {
|
trackingIssue = await gitea.createIssue(ctx, {
|
||||||
title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`,
|
title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`,
|
||||||
body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
|
body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
|
||||||
labels: labelIds,
|
labels: labelIds,
|
||||||
});
|
});
|
||||||
log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`);
|
log('建問題', 'INF', `已建立追蹤 issue #${trackingIssue.number},寫入 ${pendingIssueCommentBodies.length} 則情境留言。`);
|
||||||
for (const body of issueBuffer) {
|
for (const body of pendingIssueCommentBodies) {
|
||||||
await gitea.createCommentOnIssue(ctx, issue.number, body);
|
await gitea.createCommentOnIssue(ctx, trackingIssue.number, body);
|
||||||
}
|
}
|
||||||
issueBuffer.length = 0;
|
pendingIssueCommentBodies.length = 0;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 建問題模式降級:追蹤 issue 無法建立或寫入時,改把已暫存的情境留言發回 PR,後續沿用一般模式。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>} 無回傳值;會關閉建問題模式並把 PR 留言 id 登錄到 `currentRunCommentIds`。
|
||||||
|
*/
|
||||||
|
const fallbackToPrComments = async () => {
|
||||||
|
issueModeActive = false;
|
||||||
|
trackingIssue = null;
|
||||||
|
for (const body of pendingIssueCommentBodies) {
|
||||||
|
const created = await gitea.createIssueComment(ctx, body);
|
||||||
|
currentRunCommentIds.add(created.id);
|
||||||
|
}
|
||||||
|
pendingIssueCommentBodies.length = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 步驟 2:延後執行 ───────────────────────────────────────────────────
|
// ── 步驟 2:延後執行 ───────────────────────────────────────────────────
|
||||||
@@ -235,7 +234,7 @@ async function main() {
|
|||||||
// 舊結果會先被清掉卻沒有新結果。故延後到「本回合審查已成功產生結果、發布問題留言前」
|
// 舊結果會先被清掉卻沒有新結果。故延後到「本回合審查已成功產生結果、發布問題留言前」
|
||||||
// 才呼叫 review.resolveOldComments(見下方步驟 4 空變更路徑與步驟 9 前);
|
// 才呼叫 review.resolveOldComments(見下方步驟 4 空變更路徑與步驟 9 前);
|
||||||
// 屆時本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時。
|
// 屆時本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時。
|
||||||
// 建問題模式全程不觸碰 PR 既有留言(審查內容改發到 issue)。
|
// 建問題模式不清理 PR 既有審查內容,只在收束時標記舊追蹤 issue 連結。
|
||||||
|
|
||||||
// ── 步驟 3:偵測 AI agent 工具並留言 ──────────────────────────────────
|
// ── 步驟 3:偵測 AI agent 工具並留言 ──────────────────────────────────
|
||||||
const tool = agents.detectTool();
|
const tool = agents.detectTool();
|
||||||
@@ -267,7 +266,7 @@ async function main() {
|
|||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
// 沒有可審查的變更:保存空 findings、以 success 收場。
|
// 沒有可審查的變更:保存空 findings、以 success 收場。
|
||||||
// 一般模式在 PR 留言告知;建問題模式靜默通過(不建 issue、PR 也不留言,暫存的情境留言捨棄)。
|
// 一般模式在 PR 留言告知;建問題模式靜默通過(不建 issue、PR 也不留言,暫存的情境留言捨棄)。
|
||||||
if (ctx.createIssue) {
|
if (issueModeActive) {
|
||||||
log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。');
|
log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。');
|
||||||
} else {
|
} else {
|
||||||
await queueOrPostComment(templates.nothingToReviewComment(ignoredCount));
|
await queueOrPostComment(templates.nothingToReviewComment(ignoredCount));
|
||||||
@@ -275,7 +274,7 @@ async function main() {
|
|||||||
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
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 (issueModeActive) {
|
||||||
// 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。
|
// 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。
|
||||||
log('收尾', 'INF', '建問題模式且無可審查變更,略過 commit/push。');
|
log('收尾', 'INF', '建問題模式且無可審查變更,略過 commit/push。');
|
||||||
} else {
|
} else {
|
||||||
@@ -318,7 +317,7 @@ async function main() {
|
|||||||
|
|
||||||
// ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入;
|
// ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入;
|
||||||
// 無保留問題則不建 issue、PR 也完全不留言(靜默通過,暫存的情境留言捨棄)。 ──────
|
// 無保留問題則不建 issue、PR 也完全不留言(靜默通過,暫存的情境留言捨棄)。 ──────
|
||||||
if (ctx.createIssue) {
|
if (issueModeActive) {
|
||||||
if (kept.length > 0) {
|
if (kept.length > 0) {
|
||||||
// 先依保留問題挑好標籤,於建立 issue 時一次帶入(省去「先建空標籤 issue 再補掛」的多餘 API 往返);
|
// 先依保留問題挑好標籤,於建立 issue 時一次帶入(省去「先建空標籤 issue 再補掛」的多餘 API 往返);
|
||||||
// 標籤挑選失敗一律降級為不掛標籤,不阻斷建 issue 流程。
|
// 標籤挑選失敗一律降級為不掛標籤,不阻斷建 issue 流程。
|
||||||
@@ -337,7 +336,12 @@ async function main() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
|
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
await createIssueAndFlushBufferedComments(labelIds);
|
await createIssueAndFlushBufferedComments(labelIds);
|
||||||
|
} catch (err) {
|
||||||
|
log('建問題', 'WRN', `建立或寫入追蹤 issue 失敗(${err.message}),改用 PR 留言與 findings 檔流程。`);
|
||||||
|
await fallbackToPrComments();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。
|
// 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。
|
||||||
log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。');
|
log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。');
|
||||||
@@ -348,14 +352,14 @@ async function main() {
|
|||||||
// 延後到此可避免工具偵測/diff/攻防裁決任一失敗時舊結果先被清掉卻無新結果;
|
// 延後到此可避免工具偵測/diff/攻防裁決任一失敗時舊結果先被清掉卻無新結果;
|
||||||
// 本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時;
|
// 本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時;
|
||||||
// 嚴重/其他問題留言於本步驟之後才發布,同樣不受影響。
|
// 嚴重/其他問題留言於本步驟之後才發布,同樣不受影響。
|
||||||
if (!ctx.createIssue) {
|
if (!issueModeActive) {
|
||||||
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue)─
|
// ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue)─
|
||||||
if (severe.length > 0) {
|
if (severe.length > 0) {
|
||||||
if (ctx.createIssue) {
|
if (issueModeActive && trackingIssue) {
|
||||||
await review.postSevereToIssue({ ctx, gitea, issueNumber: issue.number, severe });
|
await review.postSevereToIssue({ ctx, gitea, issueNumber: trackingIssue.number, severe });
|
||||||
} else {
|
} else {
|
||||||
await review.postSevereComments({ ctx, gitea, severe, cwd });
|
await review.postSevereComments({ ctx, gitea, severe, cwd });
|
||||||
}
|
}
|
||||||
@@ -364,8 +368,8 @@ async function main() {
|
|||||||
// ── 步驟 10:警告+建議——一般模式彙整為單一表格留言到 PR;
|
// ── 步驟 10:警告+建議——一般模式彙整為單一表格留言到 PR;
|
||||||
// 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ──
|
// 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ──
|
||||||
if (others.length > 0) {
|
if (others.length > 0) {
|
||||||
if (ctx.createIssue) {
|
if (issueModeActive && trackingIssue) {
|
||||||
await review.postOthersToIssue({ ctx, gitea, issueNumber: issue.number, others });
|
await review.postOthersToIssue({ ctx, gitea, issueNumber: trackingIssue.number, others });
|
||||||
} else {
|
} else {
|
||||||
await queueOrPostComment(templates.othersComment(others));
|
await queueOrPostComment(templates.othersComment(others));
|
||||||
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
|
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
|
||||||
@@ -374,12 +378,13 @@ async function main() {
|
|||||||
|
|
||||||
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─
|
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─
|
||||||
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。
|
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。
|
||||||
if (ctx.createIssue && issue) {
|
if (issueModeActive && trackingIssue) {
|
||||||
|
await review.resolveOldIssueLinkComments({ ctx, gitea });
|
||||||
await gitea.createIssueComment(
|
await gitea.createIssueComment(
|
||||||
ctx,
|
ctx,
|
||||||
templates.issueLinkComment({
|
templates.prIssueLinkComment({
|
||||||
issueNumber: issue.number,
|
issueNumber: trackingIssue.number,
|
||||||
issueUrl: issue.html_url,
|
issueUrl: trackingIssue.html_url,
|
||||||
severeCount: severe.length,
|
severeCount: severe.length,
|
||||||
otherCount: others.length,
|
otherCount: others.length,
|
||||||
}),
|
}),
|
||||||
@@ -388,15 +393,15 @@ async function main() {
|
|||||||
// 僅有警告/建議時,issue 仍建立供追蹤,但不掛相依、不阻擋 PR 合併。
|
// 僅有警告/建議時,issue 仍建立供追蹤,但不掛相依、不阻擋 PR 合併。
|
||||||
if (severe.length > 0) {
|
if (severe.length > 0) {
|
||||||
try {
|
try {
|
||||||
await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number);
|
await gitea.addIssueDependency(ctx, ctx.prNumber, trackingIssue.number);
|
||||||
log('建問題', 'INF', `有嚴重問題:已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number},issue 關閉前無法合併。`);
|
log('建問題', 'INF', `有嚴重問題:已將 PR #${ctx.prNumber} 設為相依於 issue #${trackingIssue.number},issue 關閉前無法合併。`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`);
|
log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log('建問題', 'INF', `無嚴重問題(僅警告/建議):issue #${issue.number} 僅供追蹤,不阻擋 PR 合併。`);
|
log('建問題', 'INF', `無嚴重問題(僅警告/建議):issue #${trackingIssue.number} 僅供追蹤,不阻擋 PR 合併。`);
|
||||||
}
|
}
|
||||||
log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`);
|
log('建問題', 'INF', `issue #${trackingIssue.number} 已寫入審查內容,並在 PR 回貼連結。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)───────
|
// ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)───────
|
||||||
@@ -404,13 +409,14 @@ async function main() {
|
|||||||
// 若有嚴重問題,仍 commit findings 檔產生 [failure] 結果 commit,避免相依 API 不支援時 fail-open。
|
// 若有嚴重問題,仍 commit findings 檔產生 [failure] 結果 commit,避免相依 API 不支援時 fail-open。
|
||||||
const result = severe.length === 0 ? 'success' : 'failure';
|
const result = severe.length === 0 ? 'success' : 'failure';
|
||||||
const filesToCommit = review.resultFilesToCommit({
|
const filesToCommit = review.resultFilesToCommit({
|
||||||
createIssue: ctx.createIssue,
|
createIssue: issueModeActive,
|
||||||
severeCount: severe.length,
|
severeCount: severe.length,
|
||||||
relativePath,
|
relativePath,
|
||||||
exclusionsChanged,
|
exclusionsChanged,
|
||||||
});
|
});
|
||||||
|
let resultCommitted = false;
|
||||||
if (filesToCommit.length > 0) {
|
if (filesToCommit.length > 0) {
|
||||||
commitFindings({ cwd, ctx, files: filesToCommit, result });
|
resultCommitted = commitFindings({ cwd, ctx, files: filesToCommit, result });
|
||||||
} else {
|
} else {
|
||||||
log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。');
|
log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。');
|
||||||
}
|
}
|
||||||
@@ -418,6 +424,10 @@ async function main() {
|
|||||||
// 由它再觸發的下一輪在步驟 1 讀 commit 訊息時才回報失敗(exit 1)。如此失敗檢查落在帶有結果
|
// 由它再觸發的下一輪在步驟 1 讀 commit 訊息時才回報失敗(exit 1)。如此失敗檢查落在帶有結果
|
||||||
// 標記的最新 head 上,與合併判定一致。(result 僅用於上方 commit 訊息的結果標記。)
|
// 標記的最新 head 上,與合併判定一致。(result 僅用於上方 commit 訊息的結果標記。)
|
||||||
if (result === 'failure') {
|
if (result === 'failure') {
|
||||||
|
if (!resultCommitted) {
|
||||||
|
log('收尾', 'ERR', '本輪有嚴重問題,但未成功產生 [failure] 結果 commit;直接回報失敗避免 fail-open。');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。');
|
log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。');
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
+18
-7
@@ -29,6 +29,19 @@ function redactSecrets(text) {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 將 AI CLI 失敗輸出整理成單行、遮罩且限長的診斷片段。
|
||||||
|
*
|
||||||
|
* @param {string[]} parts - 要附加診斷片段的陣列。
|
||||||
|
* @param {string} label - 診斷欄位名稱(如 stderr/stdout)。
|
||||||
|
* @param {*} value - 原始診斷輸出。
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function appendRedactedOutput(parts, label, value) {
|
||||||
|
const redacted = redactSecrets(String(value || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
|
||||||
|
if (redacted) parts.push(`${label}:${redacted.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
|
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
|
||||||
*
|
*
|
||||||
@@ -37,13 +50,13 @@ function redactSecrets(text) {
|
|||||||
* 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets}
|
* 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets}
|
||||||
* 遮罩且去除控制字元的 stderr/stdout 片段。
|
* 遮罩且去除控制字元的 stderr/stdout 片段。
|
||||||
*
|
*
|
||||||
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
|
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} agentResult
|
||||||
* `runAgent` 的回傳物件。
|
* `runAgent` 的回傳物件。
|
||||||
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
|
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
|
||||||
*/
|
*/
|
||||||
function agentFailureDetail(res) {
|
function agentFailureDetail(agentResult) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
const err = res && res.error;
|
const err = agentResult && agentResult.error;
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.killed) parts.push('已逾時終止');
|
if (err.killed) parts.push('已逾時終止');
|
||||||
if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
|
if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
|
||||||
@@ -52,10 +65,8 @@ function agentFailureDetail(res) {
|
|||||||
}
|
}
|
||||||
// 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。
|
// 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。
|
||||||
if (process.env.ACTIONS_STEP_DEBUG === 'true') {
|
if (process.env.ACTIONS_STEP_DEBUG === 'true') {
|
||||||
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
|
appendRedactedOutput(parts, 'stderr', agentResult && agentResult.stderr);
|
||||||
if (stderr) parts.push(`stderr:${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
|
appendRedactedOutput(parts, 'stdout', agentResult && agentResult.output);
|
||||||
const stdout = redactSecrets(String((res && res.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
|
|
||||||
if (stdout) parts.push(`stdout:${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
|
|
||||||
}
|
}
|
||||||
if (parts.length === 0) {
|
if (parts.length === 0) {
|
||||||
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
|
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
|
||||||
|
|||||||
+7
-7
@@ -173,23 +173,23 @@ function createIssue(ctx, { title, body, labels }) {
|
|||||||
* 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/dependencies`
|
* 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/dependencies`
|
||||||
* (body 為 IssueMeta:`{index, owner, repo}`)。
|
* (body 為 IssueMeta:`{index, owner, repo}`)。
|
||||||
*
|
*
|
||||||
* 語義:URL 的 issue(`issueNumber`)相依於 body 的 issue(`dependency`)——
|
* 語義:URL 的 issue(`blockedIssueNumber`)相依於 body 的 issue(`blockingIssueNumber`)——
|
||||||
* 在 `dependency` 關閉前,`issueNumber` 無法合併/關閉。本 endpoint 需 repo 啟用
|
* 在 `blockingIssueNumber` 關閉前,`blockedIssueNumber` 無法合併/關閉。本 endpoint 需 repo 啟用
|
||||||
* 「問題相依(issue dependencies)」功能,屬版本/設定相依;未啟用或不支援時 API 會回非 2xx。
|
* 「問題相依(issue dependencies)」功能,屬版本/設定相依;未啟用或不支援時 API 會回非 2xx。
|
||||||
*
|
*
|
||||||
* @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、
|
* @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、
|
||||||
* `owner`(repo 擁有者)、`repo`(repo 名稱)。
|
* `owner`(repo 擁有者)、`repo`(repo 名稱)。
|
||||||
* @param {number|string} issueNumber - 要被阻擋的 issue/PR 編號(相依方)。
|
* @param {number|string} blockedIssueNumber - 要被阻擋的 issue/PR 編號(相依方)。
|
||||||
* @param {number} dependency - 作為阻擋來源的 issue 編號(同一 repo)。
|
* @param {number} blockingIssueNumber - 作為阻擋來源的 issue 編號(同一 repo)。
|
||||||
* @returns {Promise<object>} 建立成功的相依關係物件(依 Gitea API 回應而定)。
|
* @returns {Promise<object>} 建立成功的相依關係物件(依 Gitea API 回應而定)。
|
||||||
* @throws {Error} 請求失敗(非 2xx,例如未啟用問題相依功能)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
* @throws {Error} 請求失敗(非 2xx,例如未啟用問題相依功能)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
||||||
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立追蹤 issue 後,
|
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立追蹤 issue 後,
|
||||||
* 以本函式把「PR(`ctx.prNumber`)相依於追蹤 issue」,讓 issue 完成/關閉前 PR 無法合併;
|
* 以本函式把「PR(`ctx.prNumber`)相依於追蹤 issue」,讓 issue 完成/關閉前 PR 無法合併;
|
||||||
* 呼叫端以 try/catch 降級(功能未啟用時記 WRN、不阻斷流程)。
|
* 呼叫端以 try/catch 降級(功能未啟用時記 WRN、不阻斷流程)。
|
||||||
*/
|
*/
|
||||||
function addIssueDependency(ctx, issueNumber, dependency) {
|
function addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {
|
||||||
return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {
|
return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {
|
||||||
index: dependency,
|
index: blockingIssueNumber,
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.repo,
|
repo: ctx.repo,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。
|
||||||
|
*
|
||||||
|
* @param {string} refName - 使用者或事件 payload 提供的分支名稱。
|
||||||
|
* @param {string} fieldName - 錯誤訊息中的欄位名稱。
|
||||||
|
* @returns {string} 原樣回傳通過驗證的分支名稱。
|
||||||
|
* @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。
|
||||||
|
*/
|
||||||
|
function assertSafeBranchRef(refName, fieldName) {
|
||||||
|
const value = String(refName || '').trim();
|
||||||
|
if (!value) throw new Error(`${fieldName} 不可為空。`);
|
||||||
|
if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) {
|
||||||
|
throw new Error(`${fieldName} 不是安全的分支名稱:${value}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
execFileSync('git', ['check-ref-format', '--branch', value], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
assertSafeBranchRef,
|
||||||
|
};
|
||||||
+6
-46
@@ -1,37 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { execFileSync } = require('child_process');
|
const { execFileSync } = require('child_process');
|
||||||
|
const { assertSafeBranchRef } = require('./gitref');
|
||||||
|
|
||||||
// git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。
|
// git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。
|
||||||
|
|
||||||
/**
|
|
||||||
* 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。
|
|
||||||
*
|
|
||||||
* @param {string} refName - 使用者或事件 payload 提供的分支名稱。
|
|
||||||
* @param {string} fieldName - 錯誤訊息中的欄位名稱。
|
|
||||||
* @returns {string} 原樣回傳通過驗證的分支名稱。
|
|
||||||
* @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。
|
|
||||||
* @remarks
|
|
||||||
* 使用情境:`resolveMergeBase` 的 `baseRef` 與 `commitAndPushFindings` 的
|
|
||||||
* `headRef` 會被組進 refspec;先驗證可避免惡意 payload 影響本地 refs 路徑。
|
|
||||||
*/
|
|
||||||
function assertSafeBranchRef(refName, fieldName) {
|
|
||||||
const value = String(refName || '').trim();
|
|
||||||
if (!value) throw new Error(`${fieldName} 不可為空。`);
|
|
||||||
if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) {
|
|
||||||
throw new Error(`${fieldName} 不是安全的分支名稱:${value}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
execFileSync('git', ['check-ref-format', '--branch', value], {
|
|
||||||
encoding: 'utf8',
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步執行 git 指令並回傳原始 stdout 輸出。
|
* 同步執行 git 指令並回傳原始 stdout 輸出。
|
||||||
*
|
*
|
||||||
@@ -300,8 +273,7 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
|
|||||||
);
|
);
|
||||||
const refspec = `HEAD:refs/heads/${headRef}`;
|
const refspec = `HEAD:refs/heads/${headRef}`;
|
||||||
const remoteUrl = `${serverUrl}/${repository}.git`;
|
const remoteUrl = `${serverUrl}/${repository}.git`;
|
||||||
// 一律以 token 的身分明確認證推送(不走 origin 的自動 token)——只要 token 是能觸發 CI 的 PAT,
|
// 一律以 token 的身分明確認證推送,不沿用 origin 的自動 token。
|
||||||
// 結果 commit 就會讓 PR 的 synchronize 事件再觸發 CI,由步驟 1 快速回報把結果蓋到新 head。
|
|
||||||
pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl);
|
pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -309,18 +281,9 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
|
|||||||
/**
|
/**
|
||||||
* 以帶認證的方式推送到指定遠端,認證資訊只經環境變數傳入、不進命令列 argv。
|
* 以帶認證的方式推送到指定遠端,認證資訊只經環境變數傳入、不進命令列 argv。
|
||||||
*
|
*
|
||||||
* 認證方式:等同 `https://ai-review-bot:<secret>@host/...` 的 HTTP Basic(git 會把
|
* 以 `GIT_CONFIG_*` 注入本次 HTTP Basic extraheader,避免憑證出現在 argv;
|
||||||
* URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的
|
* 同時先清空 checkout 持久化的自動 token extraheader,確保本次 push 使用呼叫端 token。
|
||||||
* `GIT_CONFIG_*` 環境變數注入 `http.<serverUrl>/.extraheader`,使 base64 憑證**不出現在 argv**
|
* 推送失敗時改拋固定訊息,避免原始例外帶出遠端 URL 或認證資訊。
|
||||||
* (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。
|
|
||||||
*
|
|
||||||
* 觸發 CI 關鍵:`actions/checkout` 會把「自動 Actions token」持久化在同一個
|
|
||||||
* `http.<serverUrl>/.extraheader` scope;若沿用它推送,Gitea 會視為「自動 token 觸發」而
|
|
||||||
* **不再觸發 workflow**(防遞迴)。故本函式對這次 push 於該 scope**先以空值重置**(清掉自動
|
|
||||||
* token——git 對 extraHeader 給空值即清空既有清單),**再注入 PAT 的 Authorization**,讓推送以
|
|
||||||
* PAT 身分進行、觸發 PR 的 synchronize;作用範圍僅限本次 push 的環境變數,不影響 action 其他
|
|
||||||
* 仰賴 checkout 持久化憑證的 fetch(如 {@link resolveMergeBase})。
|
|
||||||
* 推送失敗時**不重拋原始例外**(其 message 會含命令列與遠端 URL),改拋固定訊息。
|
|
||||||
*
|
*
|
||||||
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
|
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
|
||||||
* @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。
|
* @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。
|
||||||
@@ -348,7 +311,7 @@ function pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) {
|
|||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
GIT_TERMINAL_PROMPT: '0',
|
GIT_TERMINAL_PROMPT: '0',
|
||||||
// 兩筆同 scope 設定:先空值清掉 checkout 的自動 token,再注入 PAT 的 Authorization。
|
// 先清空 checkout extraheader,再注入本次 PAT header。
|
||||||
GIT_CONFIG_COUNT: '2',
|
GIT_CONFIG_COUNT: '2',
|
||||||
GIT_CONFIG_KEY_0: headerScope,
|
GIT_CONFIG_KEY_0: headerScope,
|
||||||
GIT_CONFIG_VALUE_0: '',
|
GIT_CONFIG_VALUE_0: '',
|
||||||
@@ -368,7 +331,4 @@ module.exports = {
|
|||||||
fileDiff,
|
fileDiff,
|
||||||
fileLastUpdatedIso,
|
fileLastUpdatedIso,
|
||||||
commitAndPushFindings,
|
commitAndPushFindings,
|
||||||
__test: {
|
|
||||||
assertSafeBranchRef,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
+41
-6
@@ -690,9 +690,9 @@ ${JSON.stringify(brief)}
|
|||||||
* 不使用 {@link templates.othersComment} 的單一表格——表格僅用於一般模式(PR)。
|
* 不使用 {@link templates.othersComment} 的單一表格——表格僅用於一般模式(PR)。
|
||||||
*/
|
*/
|
||||||
async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
|
async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
|
||||||
await Promise.all(
|
for (const finding of severe) {
|
||||||
severe.map((finding) => gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding))),
|
await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding));
|
||||||
);
|
}
|
||||||
log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}。`);
|
log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,9 +716,9 @@ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
|
|||||||
* 以本函式把警告+建議逐條留言到追蹤 issue,確保 issue 上每條問題都是可個別回覆的留言。
|
* 以本函式把警告+建議逐條留言到追蹤 issue,確保 issue 上每條問題都是可個別回覆的留言。
|
||||||
*/
|
*/
|
||||||
async function postOthersToIssue({ ctx, gitea, issueNumber, others }) {
|
async function postOthersToIssue({ ctx, gitea, issueNumber, others }) {
|
||||||
await Promise.all(
|
for (const finding of others) {
|
||||||
others.map((finding) => gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding))),
|
await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding));
|
||||||
);
|
}
|
||||||
log('步驟10', 'INF', `已將 ${others.length} 條警告+建議逐條留言到 issue #${issueNumber}。`);
|
log('步驟10', 'INF', `已將 ${others.length} 條警告+建議逐條留言到 issue #${issueNumber}。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,6 +832,40 @@ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建問題模式:只將 PR 上舊的「追蹤問題連結」留言標註為過時,不觸碰 issue 內審查內容。
|
||||||
|
*
|
||||||
|
* @param {Object} params - 解構參數。
|
||||||
|
* @param {Object} params.ctx - 執行環境 context(`loadContext()` 產出)。
|
||||||
|
* @param {Object} params.gitea - Gitea API 模組,需提供 `whoAmI`、`listIssueComments`、`editIssueComment`。
|
||||||
|
* @returns {Promise<void>} 無回傳值;失敗時只記 WRN,不阻斷主流程。
|
||||||
|
*/
|
||||||
|
async function resolveOldIssueLinkComments({ ctx, gitea }) {
|
||||||
|
let botLogin = '';
|
||||||
|
try {
|
||||||
|
botLogin = (await gitea.whoAmI(ctx)).login || '';
|
||||||
|
} catch (err) {
|
||||||
|
log('建問題', 'WRN', `無法取得 bot 身分(${err.message}),略過舊追蹤連結標註。`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const comments = await gitea.listIssueComments(ctx);
|
||||||
|
let outdatedCount = 0;
|
||||||
|
for (const comment of comments) {
|
||||||
|
const isBot = comment.user && comment.user.login === botLogin;
|
||||||
|
const body = typeof comment.body === 'string' ? comment.body : '';
|
||||||
|
const isIssueLink = body.includes(templates.MARK) && body.includes('## 🔍 AI Code Review|已建立追蹤問題');
|
||||||
|
if (!isBot || !isIssueLink || body.startsWith(templates.OUTDATED_PREFIX)) continue;
|
||||||
|
await gitea.editIssueComment(ctx, comment.id, `${templates.OUTDATED_PREFIX}${body}`);
|
||||||
|
outdatedCount += 1;
|
||||||
|
}
|
||||||
|
log('建問題', 'INF', `舊追蹤 issue 連結已標註〔已過時〕:${outdatedCount} 則。`);
|
||||||
|
} catch (err) {
|
||||||
|
log('建問題', 'WRN', `標註舊追蹤 issue 連結失敗:${err.message}。`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 步驟 9:嚴重問題逐條掛在 PR 程式碼行上留言(建立 code review);
|
* 步驟 9:嚴重問題逐條掛在 PR 程式碼行上留言(建立 code review);
|
||||||
* 建立 review 失敗時降級為一般留言逐條發布(留言內補上檔案與行號位置)。
|
* 建立 review 失敗時降級為一般留言逐條發布(留言內補上檔案與行號位置)。
|
||||||
@@ -884,5 +918,6 @@ module.exports = {
|
|||||||
postSevereToIssue,
|
postSevereToIssue,
|
||||||
postOthersToIssue,
|
postOthersToIssue,
|
||||||
resolveOldComments,
|
resolveOldComments,
|
||||||
|
resolveOldIssueLinkComments,
|
||||||
postSevereComments,
|
postSevereComments,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ function nothingToReviewComment(ignoredCount) {
|
|||||||
* 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以
|
* 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以
|
||||||
* {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。
|
* {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。
|
||||||
*/
|
*/
|
||||||
function issueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {
|
function prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {
|
||||||
return `${MARK}
|
return `${MARK}
|
||||||
## 🔍 AI Code Review|已建立追蹤問題
|
## 🔍 AI Code Review|已建立追蹤問題
|
||||||
|
|
||||||
@@ -421,5 +421,5 @@ module.exports = {
|
|||||||
issueBody,
|
issueBody,
|
||||||
issueFindingComment,
|
issueFindingComment,
|
||||||
nothingToReviewComment,
|
nothingToReviewComment,
|
||||||
issueLinkComment,
|
prIssueLinkComment,
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ const test = require('node:test');
|
|||||||
|
|
||||||
const gitea = require('../src/lib/gitea');
|
const gitea = require('../src/lib/gitea');
|
||||||
|
|
||||||
function withFetchStub(handler, fn) {
|
function withFetchStub(handler, callback) {
|
||||||
const originalFetch = global.fetch;
|
const originalFetch = global.fetch;
|
||||||
const calls = [];
|
const calls = [];
|
||||||
global.fetch = async (url, options = {}) => {
|
global.fetch = async (url, options = {}) => {
|
||||||
@@ -13,7 +13,7 @@ function withFetchStub(handler, fn) {
|
|||||||
return handler(url, options);
|
return handler(url, options);
|
||||||
};
|
};
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => fn(calls))
|
.then(() => callback(calls))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
global.fetch = originalFetch;
|
global.fetch = originalFetch;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ const assert = require('node:assert/strict');
|
|||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
|
|
||||||
const gitrepo = require('../src/lib/gitrepo');
|
const gitrepo = require('../src/lib/gitrepo');
|
||||||
|
const gitref = require('../src/lib/gitref');
|
||||||
|
|
||||||
test('assertSafeBranchRef 接受一般分支名稱', () => {
|
test('assertSafeBranchRef 接受一般分支名稱', () => {
|
||||||
assert.equal(gitrepo.__test.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123');
|
assert.equal(gitref.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => {
|
test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => gitrepo.__test.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'),
|
() => gitref.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'),
|
||||||
/不是安全的分支名稱/,
|
/不是安全的分支名稱/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
+39
-2
@@ -44,7 +44,7 @@ test('agentFailureDetail 在 debug 模式輸出遮罩後片段', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('postOthersToIssue 批次送出 issue 留言', async () => {
|
test('postOthersToIssue 依序送出 issue 留言以維持排序', async () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
let active = 0;
|
let active = 0;
|
||||||
let maxActive = 0;
|
let maxActive = 0;
|
||||||
@@ -71,7 +71,7 @@ test('postOthersToIssue 批次送出 issue 留言', async () => {
|
|||||||
|
|
||||||
assert.equal(calls.length, 2);
|
assert.equal(calls.length, 2);
|
||||||
assert.equal(calls[0].issueNumber, 7);
|
assert.equal(calls[0].issueNumber, 7);
|
||||||
assert.ok(maxActive > 1);
|
assert.equal(maxActive, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resultFilesToCommit 在建問題模式有嚴重問題時仍提交 findings', () => {
|
test('resultFilesToCommit 在建問題模式有嚴重問題時仍提交 findings', () => {
|
||||||
@@ -97,3 +97,40 @@ test('resultFilesToCommit 在建問題模式無嚴重問題時只提交 exclusio
|
|||||||
['.gitea/ai-review/exclusions.json'],
|
['.gitea/ai-review/exclusions.json'],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolveOldIssueLinkComments 只標註舊追蹤 issue 連結', async () => {
|
||||||
|
const edited = [];
|
||||||
|
const fakeGitea = {
|
||||||
|
async whoAmI() {
|
||||||
|
return { login: 'bot' };
|
||||||
|
},
|
||||||
|
async listIssueComments() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
user: { login: 'bot' },
|
||||||
|
body: '<!-- ai-code-review -->\n## 🔍 AI Code Review|已建立追蹤問題\nold',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
user: { login: 'bot' },
|
||||||
|
body: '<!-- ai-code-review -->\n## 📋 變更摘要(送審 git diff)\nkeep',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
user: { login: 'someone' },
|
||||||
|
body: '<!-- ai-code-review -->\n## 🔍 AI Code Review|已建立追蹤問題\nkeep',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
async editIssueComment(ctx, commentId, body) {
|
||||||
|
edited.push({ ctx, commentId, body });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await review.resolveOldIssueLinkComments({ ctx: { token: 'hidden' }, gitea: fakeGitea });
|
||||||
|
|
||||||
|
assert.equal(edited.length, 1);
|
||||||
|
assert.equal(edited[0].commentId, 1);
|
||||||
|
assert.match(edited[0].body, /^> 〔已過時〕/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user