Author SHA1 Message Date
ai-review-bot 76617dd9dc chore: update ai-review findings [ai-review-bot][failure]
node-actions/template: CI / BUILD (pull_request) Successful in 5s
CI / TEST (Codex) (pull_request) Failing after 25s
CI / TEST (Claude) (pull_request) Failing after 27s
CI / TEST (Antigravity) (pull_request) Failing after 35s
2026-07-21 08:00:35 +00:00
Jeffery 216bc39255 chore(ai-review 狀態): 回寫本輪已處理 findings
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Successful in 26s
CI / TEST (Antigravity) (pull_request) Successful in 49s
CI / TEST (Codex) (pull_request) Successful in 2m34s
2026-07-21 15:57:53 +08:00
Jeffery 75ca8cdb83 refactor(ai-review 診斷): 收斂失敗輸出處理與推送註解 2026-07-21 15:57:53 +08:00
Jeffery aaf2e484d5 fix(ai-review): 避免建問題模式阻擋失效與連結累積 2026-07-21 15:57:53 +08:00
8 changed files with 493 additions and 98 deletions
+99
View File
@@ -1890,5 +1890,104 @@
"endLine": 69,
"problem": "`redactSecrets` 與 `agentFailureDetail` 是新加入的安全診斷防線,但測試只覆蓋 Authorization/token 的基本遮罩。控制字元單行化、URL 內嵌帳密、長 token/hex、輸入與輸出長度截斷、以及沒有 error code 時的 fallback 訊息都還沒被驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 redactSecretsagentFailureDetail 對控制字元、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 已記錄 commitAndPushFindingspushWithCredential 的認證推送路徑、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 涵蓋。"
}
]
@@ -8,63 +8,6 @@
"model": "gpt-5.5"
},
"findings": [
{
"reviewer": "Assassin",
"focus": "security",
"badge": "🗡️",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 344,
"endLine": 348,
"problem": "建問題模式在 `addIssueDependency` 失敗時只記錄警告,接著本輪仍會回傳 0;若 `commitFindings` 後續因沒有實際 diff 可提交而沒有產生 `[failure]` 結果 commit,攻擊者只要讓嚴重 finding 被搬到追蹤 issue,且目標 Gitea 未啟用 issue dependencies 或 token 權限不足,就會 fail-open:PR 既沒有相依阻擋,也沒有失敗檢查阻擋合併。",
"suggestion": "有嚴重問題時,相依關係設定失敗應視為阻擋條件:要嘛直接回傳 1,要嘛確認 failure 結果 commit 已成功產生後才允許本輪回傳 0。不要把阻擋機制失效降級成純警告。",
"suggestedCode": "",
"id": "F001",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。此條不是單純重複既有「相依 API 失敗降級缺測試」,而是指控嚴重 finding 搬到 issue 後,dependency 失敗可能使阻擋機制失效;已知排除事項未涵蓋此安全語義。"
}
}
},
{
"reviewer": "Mage",
"focus": "logic",
"badge": "🔮",
"severity": "警告",
"file": "src/index.js",
"startLine": 330,
"endLine": 338,
"problem": "在建問題模式下,這段新增的 PR 回貼 issue 連結會在每次審查有保留問題時都新增一則 PR 留言,但同一流程前面明確跳過 `resolveOldComments`(建問題模式不清理 PR 舊留言)。最小重現:PR 第一次審查建立 issue #10 並在 PR 留連結;後續推新 commit 再跑一次,建立 issue #11 並再留一則連結。PR 上會同時存在 #10 與 #11,舊 issue 可能已過時,讀者無法判斷哪個才是目前審查結果。",
"suggestion": "建問題模式也應對本 action 先前的 PR 連結留言做過時標記,或在新增連結前查找並更新既有連結留言。若要避免碰觸 issue 內的審查內容,清理範圍可限制在 PR 上含 `MARK` 且標題為「已建立追蹤問題」的留言。",
"suggestedCode": "",
"id": "F009",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。已知排除事項只裁示舊審查留言在本輪結果前標過時的時機;本條指控建問題模式跳過 PR 舊連結留言清理,導致多個追蹤 issue 連結並存,未被既有排除涵蓋。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/diagnostics.js",
"startLine": 55,
"endLine": 63,
"problem": "`agentFailureDetail` 裡的 `stderr` 與 `stdout` 區塊幾乎同譜重奏:取值、slice、redact、判斷、push 只差欄位名。這種重複雖小,卻讓後續若要調整遮罩或長度時容易改一半走調。",
"suggestion": "建議抽出小 helper,例如 `appendRedactedOutput(parts, label, value)`,讓 stderr/stdout 共用同一段處理節奏。",
"suggestedCode": "",
"id": "F005",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。此條針對 diagnostics.js 中 stderr/stdout 處理重複的維護性問題,歷史 findings 主要是安全診斷缺測試與外洩風險,並非同一指控。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
@@ -83,25 +26,6 @@
"reason": "保留。歷史 findings 主要涵蓋 resolveMergeBase 缺測試與診斷不足,本條指向函式內策略編排、診斷組裝與錯誤包裝混雜的可維護性問題,未被既有排除事項完整涵蓋。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/gitrepo.js",
"startLine": 317,
"endLine": 346,
"problem": "`pushWithCredential` 的 JSDoc 已經很完整,但正文註解再次長篇解釋 checkout token、PAT、extraheader 清空等細節;文件與程式內註解重複奏同一段旋律,反而稀釋真正需要看的程式碼。",
"suggestion": "保留 JSDoc 的背景說明,函式內註解縮成操作提示即可,例如只說明「先清空 checkout extraheader,再注入本次 PAT header」。",
"suggestedCode": "",
"id": "F004",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。既有排除事項雖有 push-token manifest 說明重複,但未涵蓋 pushWithCredential 函式內 JSDoc 與正文註解重複;證據不足以判定為重複或誤報。"
}
}
}
],
"excluded": [
@@ -0,0 +1,261 @@
{
"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": [
{
"reviewer": "Assassin",
"focus": "security",
"badge": "🗡️",
"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、也沒有下一輪失敗檢查,等同把必要檢查繞過。",
"suggestion": "嚴重問題存在時,結果 commit/push 失敗必須直接讓本輪 workflow 失敗;只有 success 結果或無變更時才可降級不阻擋。呼叫端應檢查 `commitFindings` 回傳值,或讓 `commitFindings` 在 `result === 'failure'` 時重拋錯誤。",
"suggestedCode": "const committed = commitFindings({ cwd, ctx, files: filesToCommit, result });\nif (result === 'failure' && !committed) {\n log('收尾', 'ERR', '存在嚴重問題,但無法推送 failure 結果 commit;本輪直接失敗以避免繞過檢查。');\n return 1;\n}\nreturn 0;",
"id": "F001",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留(成立)。已知排除事項與歷史 findings 未涵蓋「嚴重 finding 依賴 failure commit,但 failure commit 推送失敗時本輪仍可能通過」這個繞過風險;證據不足以排除。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "action.yml",
"startLine": 18,
"endLine": 19,
"problem": "中文敘述裡混入 `PR/issue`、`findings/exclusions` 這種半形斜線寫法,但同一份變更其他地方大量使用 `PR/issue`、`警告+建議` 這類全形符號。標點像節拍器,這裡忽然換拍,讓 manifest 的文字風格不夠一致。",
"suggestion": "統一中文文件與註解中的分隔符號,建議在中文語境使用全形斜線:`PR/issue`、`findingsexclusions`;若是程式路徑或指令片段才保留半形 `/`。",
"suggestedCode": "",
"id": "F004",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留(成立)。未命中已知排除事項,也未見歷史 finding 涵蓋 action.yml 中文標點半形/全形風格不一致;依現有資料不能判為重複或誤報。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/gitrepo.js",
"startLine": 299,
"endLine": 344,
"problem": "`pushWithCredential` 的 JSDoc 幾乎把安全設計、CI 觸發語意、checkout extraheader 行為全部寫成一篇短文。資訊本身有價值,但集中在函式註解裡會壓過函式簽名,讀者想找參數與責任邊界時,得先穿過一大段敘事。",
"suggestion": "保留函式層級的摘要與關鍵安全不變式,其餘背景可移到較短的段落或專門文件。JSDoc 建議聚焦在「做什麼、為何不能改、參數怎麼用」,避免把完整決策紀錄塞進 API 註解。",
"suggestedCode": "",
"id": "F005",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留(成立)。歷史 finding 雖有 resolveMergeBase 閱讀密度問題,但未涵蓋 pushWithCredential JSDoc 過長且混入決策紀錄;目前不能排除。"
}
}
},
{
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/gitrepo.js",
"startLine": 367,
"endLine": 369,
"problem": "為了測試把內部函式掛在 `module.exports.__test`,會讓 production module 的公開形狀混入測試專用 API。未來其他程式碼可能誤用 `__test.assertSafeBranchRef`,而維護者也得在重構時顧慮這個非正式出口,模組邊界會慢慢變模糊。",
"suggestion": "把分支名稱驗證抽到獨立小模組並正常匯出,例如 `src/lib/gitref.js`,讓 production code 與測試都依賴同一個正式 API;或若它只屬於 gitrepo 內部,就改由測試 `resolveMergeBase``commitAndPushFindings` 的外部行為覆蓋,不暴露 `__test`。",
"suggestedCode": "",
"id": "F009",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留(成立)。目前提供的排除事項與歷史 findings 未涵蓋 module.exports.__test 暴露測試專用 API 的模組邊界問題;證據不足以排除。"
}
}
}
],
"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 發到 issuePR 只留下追蹤 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 已記錄 commitAndPushFindingspushWithCredential 的認證推送路徑、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 成功時不跑 deependeepen base 後成功會立刻回傳;deepen PR HEAD 使用目前 `HEAD` SHA 而不是遠端符號 `HEAD`;非 shallow repo 不呼叫 `--unshallow`;全部策略失敗時錯誤包含各策略診斷且保留 cause。",
"suggestedCode": "",
"id": "F013",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。resolveMergeBase 多階段 fetch、淺層與非淺層路徑、策略停止條件、全部失敗診斷與 cause 缺少測試,已由歷史 findings 涵蓋。"
}
}
}
]
}
+43 -14
View File
@@ -72,7 +72,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) {
* 供下一回合 `main()` 步驟 1 比對辨識、直接回報結果而不重複審查。
* 依 `commitAndPushFindings` 的回傳值記錄不同日誌:true=已 commit/push
* false=檔案無實際變更(空 commit 防護),記「略過 commit/push」。
* commit/push 失敗(例如與開發者新 commit 競態)時僅記 WRN log,不拋出例外、不改變審查結果
* commit/push 失敗(例如與開發者新 commit 競態)時僅記 WRN log,不拋出例外;呼叫端可依回傳值決定是否阻擋
*
* @param {Object} params - 解構參數。
* @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.files - 要 commit 的檔案 repo 相對路徑陣列(如 findings 檔、`.gitea/ai-review/exclusions.json`);全數無變更時只記 INF 略過。
* @param {'success'|'failure'} params.result - 本回合審查結果:success=無嚴重問題、failure=有嚴重問題;會拼進 commit 訊息尾端。
* @returns {void} 無回傳值;成敗僅反映在 log 上
* @returns {boolean} true=已 commit/pushfalse=無變更或 commit/push 失敗
* @remarks
* 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、
* 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json
@@ -107,12 +107,15 @@ function commitFindings({ cwd, ctx, files, result }) {
});
if (committed) {
log('收尾', 'INF', `審查結果檔已 commit 並 push 回 ${ctx.headRef}(結果:${result})。`);
return true;
} else {
log('收尾', 'INF', '審查結果檔無實際變更,略過 commit/push。');
return false;
}
} catch (err) {
// push 失敗(例如與開發者新 commit 競態)時只記錄,不改變審查結果
// push 失敗(例如與開發者新 commit 競態)時只記錄,交由呼叫端依嚴重度決定是否阻擋
log('收尾', 'WRN', `commit/push 審查結果檔失敗:${err.message}`);
return false;
}
}
@@ -165,6 +168,7 @@ async function main() {
// 建問題模式:追蹤 issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色)
// 先暫存於 pendingIssueCommentBodies,建立 issue 後一次寫入。
const pendingIssueCommentBodies = [];
let issueModeActive = ctx.createIssue;
let trackingIssue = null;
/**
* 發布一則審查留言。依模式決定去向:
@@ -179,7 +183,7 @@ async function main() {
* 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。
*/
const queueOrPostComment = async (body) => {
if (ctx.createIssue) {
if (issueModeActive) {
if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);
pendingIssueCommentBodies.push(body);
return null;
@@ -210,13 +214,27 @@ async function main() {
}
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:延後執行 ───────────────────────────────────────────────────
// 「將 PR 既有留言標記為解決」原本在此執行,但若工具偵測/diff/攻防裁決任一失敗,
// 舊結果會先被清掉卻沒有新結果。故延後到「本回合審查已成功產生結果、發布問題留言前」
// 才呼叫 review.resolveOldComments(見下方步驟 4 空變更路徑與步驟 9 前);
// 屆時本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時。
// 建問題模式全程不觸碰 PR 既有留言(審查內容改發到 issue
// 建問題模式不清理 PR 既有審查內容,只在收束時標記舊追蹤 issue 連結
// ── 步驟 3:偵測 AI agent 工具並留言 ──────────────────────────────────
const tool = agents.detectTool();
@@ -248,7 +266,7 @@ async function main() {
if (files.length === 0) {
// 沒有可審查的變更:保存空 findings、以 success 收場。
// 一般模式在 PR 留言告知;建問題模式靜默通過(不建 issue、PR 也不留言,暫存的情境留言捨棄)。
if (ctx.createIssue) {
if (issueModeActive) {
log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。');
} else {
await queueOrPostComment(templates.nothingToReviewComment(ignoredCount));
@@ -256,7 +274,7 @@ async function main() {
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
}
const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] });
if (ctx.createIssue) {
if (issueModeActive) {
// 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。
log('收尾', 'INF', '建問題模式且無可審查變更,略過 commit/push。');
} else {
@@ -299,7 +317,7 @@ async function main() {
// ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入;
// 無保留問題則不建 issue、PR 也完全不留言(靜默通過,暫存的情境留言捨棄)。 ──────
if (ctx.createIssue) {
if (issueModeActive) {
if (kept.length > 0) {
// 先依保留問題挑好標籤,於建立 issue 時一次帶入(省去「先建空標籤 issue 再補掛」的多餘 API 往返);
// 標籤挑選失敗一律降級為不掛標籤,不阻斷建 issue 流程。
@@ -318,7 +336,12 @@ async function main() {
} catch (err) {
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
}
try {
await createIssueAndFlushBufferedComments(labelIds);
} catch (err) {
log('建問題', 'WRN', `建立或寫入追蹤 issue 失敗(${err.message}),改用 PR 留言與 findings 檔流程。`);
await fallbackToPrComments();
}
} else {
// 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。
log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。');
@@ -329,13 +352,13 @@ async function main() {
// 延後到此可避免工具偵測/diff/攻防裁決任一失敗時舊結果先被清掉卻無新結果;
// 本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時;
// 嚴重/其他問題留言於本步驟之後才發布,同樣不受影響。
if (!ctx.createIssue) {
if (!issueModeActive) {
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
}
// ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue)─
if (severe.length > 0) {
if (ctx.createIssue) {
if (issueModeActive && trackingIssue) {
await review.postSevereToIssue({ ctx, gitea, issueNumber: trackingIssue.number, severe });
} else {
await review.postSevereComments({ ctx, gitea, severe, cwd });
@@ -345,7 +368,7 @@ async function main() {
// ── 步驟 10:警告+建議——一般模式彙整為單一表格留言到 PR;
// 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ──
if (others.length > 0) {
if (ctx.createIssue) {
if (issueModeActive && trackingIssue) {
await review.postOthersToIssue({ ctx, gitea, issueNumber: trackingIssue.number, others });
} else {
await queueOrPostComment(templates.othersComment(others));
@@ -355,7 +378,8 @@ async function main() {
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。
if (ctx.createIssue && trackingIssue) {
if (issueModeActive && trackingIssue) {
await review.resolveOldIssueLinkComments({ ctx, gitea });
await gitea.createIssueComment(
ctx,
templates.prIssueLinkComment({
@@ -385,13 +409,14 @@ async function main() {
// 若有嚴重問題,仍 commit findings 檔產生 [failure] 結果 commit,避免相依 API 不支援時 fail-open。
const result = severe.length === 0 ? 'success' : 'failure';
const filesToCommit = review.resultFilesToCommit({
createIssue: ctx.createIssue,
createIssue: issueModeActive,
severeCount: severe.length,
relativePath,
exclusionsChanged,
});
let resultCommitted = false;
if (filesToCommit.length > 0) {
commitFindings({ cwd, ctx, files: filesToCommit, result });
resultCommitted = commitFindings({ cwd, ctx, files: filesToCommit, result });
} else {
log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。');
}
@@ -399,6 +424,10 @@ async function main() {
// 由它再觸發的下一輪在步驟 1 讀 commit 訊息時才回報失敗(exit 1)。如此失敗檢查落在帶有結果
// 標記的最新 head 上,與合併判定一致。(result 僅用於上方 commit 訊息的結果標記。)
if (result === 'failure') {
if (!resultCommitted) {
log('收尾', 'ERR', '本輪有嚴重問題,但未成功產生 [failure] 結果 commit;直接回報失敗避免 fail-open。');
return 1;
}
log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。');
}
return 0;
+15 -4
View File
@@ -29,6 +29,19 @@ function redactSecrets(text) {
.trim();
}
/**
* 將 AI CLI 失敗輸出整理成單行、遮罩且限長的診斷片段。
*
* @param {string[]} parts - 要附加診斷片段的陣列。
* @param {string} label - 診斷欄位名稱(如 stderrstdout)。
* @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` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
*
@@ -52,10 +65,8 @@ function agentFailureDetail(agentResult) {
}
// 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。
if (process.env.ACTIONS_STEP_DEBUG === 'true') {
const stderr = redactSecrets(String((agentResult && agentResult.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
if (stderr) parts.push(`stderr${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
const stdout = redactSecrets(String((agentResult && agentResult.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
if (stdout) parts.push(`stdout${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
appendRedactedOutput(parts, 'stderr', agentResult && agentResult.stderr);
appendRedactedOutput(parts, 'stdout', agentResult && agentResult.output);
}
if (parts.length === 0) {
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
+2 -3
View File
@@ -300,8 +300,7 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
);
const refspec = `HEAD:refs/heads/${headRef}`;
const remoteUrl = `${serverUrl}/${repository}.git`;
// 一律以 token 的身分明確認證推送(不走 origin 的自動 token)——只要 token 是能觸發 CI 的 PAT
// 結果 commit 就會讓 PR 的 synchronize 事件再觸發 CI,由步驟 1 快速回報把結果蓋到新 head。
// 一律以 token 的身分明確認證推送,不沿用 origin 的自動 token
pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl);
return true;
}
@@ -348,7 +347,7 @@ function pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) {
env: {
...process.env,
GIT_TERMINAL_PROMPT: '0',
// 兩筆同 scope 設定:先空值清掉 checkout 的自動 token,再注入 PAT 的 Authorization
// 先清空 checkout extraheader,再注入本次 PAT header
GIT_CONFIG_COUNT: '2',
GIT_CONFIG_KEY_0: headerScope,
GIT_CONFIG_VALUE_0: '',
+35
View File
@@ -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);
* 建立 review 失敗時降級為一般留言逐條發布(留言內補上檔案與行號位置)。
@@ -884,5 +918,6 @@ module.exports = {
postSevereToIssue,
postOthersToIssue,
resolveOldComments,
resolveOldIssueLinkComments,
postSevereComments,
};
+37
View File
@@ -97,3 +97,40 @@ test('resultFilesToCommit 在建問題模式無嚴重問題時只提交 exclusio
['.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, /^> 〔已過時〕/);
});