40 Commits
Author SHA1 Message Date
Jeffery 34aecf6e43 chore(ai-review): 清空已處理 findings 並更新 exclusions
node-actions/template: CI / BUILD (pull_request) Successful in 6s
2026-07-28 14:02:24 +08:00
Jeffery 2d3128eb45 docs(token): 收斂 token 權限建議並移除 README 行號連結 2026-07-28 14:02:18 +08:00
Jeffery 885b79f8b3 test(diagnostics): 補上診斷限長與 PII 遮罩測試 2026-07-28 14:02:11 +08:00
Jeffery db83b54c2d perf(gitrepo): 改用 shallow 檔案判斷避免額外 git 子行程 2026-07-28 14:02:06 +08:00
Jeffery 3c1cd55444 fix(diagnostics): 限縮 debug 診斷輸出並擴充遮罩 2026-07-28 14:02:01 +08:00
admin cce35b3bef Merge pull request 'feat(ai-code-review): 建問題模式導向 issue,並整併流程調整、文件與 CI' (#6) from ai-review-resolve/develop-20260717-185330 into develop
Reviewed-on: #6
2026-07-21 09:39:48 +00:00
ai-review-bot 25f76e6e6e chore: update ai-review findings [ai-review-bot][success]
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Successful in 24s
CI / TEST (Codex) (pull_request) Successful in 27s
CI / TEST (Antigravity) (pull_request) Successful in 33s
2026-07-21 09:25:39 +00:00
Jeffery 44c33b6c1e chore(ai-review 狀態): 回寫本輪已處理 findings
node-actions/template: CI / BUILD (pull_request) Successful in 5s
CI / TEST (Antigravity) (pull_request) Successful in 1m1s
CI / TEST (Codex) (pull_request) Successful in 3m30s
CI / TEST (Claude) (pull_request) Successful in 24s
2026-07-21 17:21:57 +08:00
Jeffery 449b17480b fix(ai-review): 結果推送失敗時回報工作流失敗 2026-07-21 17:21:57 +08:00
ai-review-bot b60f50f2b4 chore: update ai-review findings [ai-review-bot][failure]
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Codex) (pull_request) Failing after 25s
CI / TEST (Claude) (pull_request) Failing after 26s
CI / TEST (Antigravity) (pull_request) Failing after 32s
2026-07-21 08:30:13 +00:00
Jeffery 43ad2b56d2 chore(ai-review 狀態): 回寫本輪 findings 與排除事項
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Successful in 33s
CI / TEST (Antigravity) (pull_request) Successful in 49s
CI / TEST (Codex) (pull_request) Successful in 3m38s
2026-07-21 16:26:30 +08:00
Jeffery 8e18bbacc2 style(action manifest): 統一中文斜線標點 2026-07-21 16:26:30 +08:00
Jeffery 1f012c6cfb refactor(gitref): 將分支名稱驗證改為正式模組 2026-07-21 16:26:30 +08:00
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
ai-review-bot 55da29a86f chore: update ai-review findings [ai-review-bot][failure]
CI / TEST (Claude) (pull_request) Failing after 22s
CI / TEST (Codex) (pull_request) Failing after 26s
CI / TEST (Antigravity) (pull_request) Failing after 33s
node-actions/template: CI / BUILD (pull_request) Successful in 4s
2026-07-21 06:42:40 +00:00
Jeffery d424447d15 chore(ai-review 狀態): 回寫本輪已處理 findings
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Antigravity) (pull_request) Successful in 52s
CI / TEST (Codex) (pull_request) Successful in 2m47s
CI / TEST (Claude) (pull_request) Successful in 26s
2026-07-21 14:39:42 +08:00
Jeffery 9d05af647b fix(ai-review): 對齊建問題模式留言順序與命名 2026-07-21 14:39:42 +08:00
ai-review-bot 941d763129 chore: update ai-review findings [ai-review-bot][failure]
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Codex) (pull_request) Failing after 24s
CI / TEST (Claude) (pull_request) Failing after 28s
CI / TEST (Antigravity) (pull_request) Failing after 33s
2026-07-21 06:27:43 +00:00
Jeffery 9e7f8a2a0a chore(ai-review 狀態): 回寫本輪已處理 findings
node-actions/template: CI / BUILD (pull_request) Successful in 3s
CI / TEST (Claude) (pull_request) Successful in 27s
CI / TEST (Antigravity) (pull_request) Successful in 48s
CI / TEST (Codex) (pull_request) Successful in 3m18s
2026-07-21 14:24:14 +08:00
Jeffery 8b50a0d643 test(ai-review): 補上建問題模式結果檔測試 2026-07-21 14:24:10 +08:00
Jeffery 555611db07 fix(ai-review): 避免建問題模式嚴重結果 fail-open 2026-07-21 14:24:04 +08:00
ai-review-bot 24b248884f chore: update ai-review findings [ai-review-bot][failure]
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Failing after 26s
CI / TEST (Codex) (pull_request) Failing after 32s
CI / TEST (Antigravity) (pull_request) Failing after 46s
2026-07-21 05:50:00 +00:00
Jeffery e0ae6f886f chore(ai-review 狀態): 回寫已處理 findings
node-actions/template: CI / BUILD (pull_request) Successful in 5s
CI / TEST (Claude) (pull_request) Successful in 28s
CI / TEST (Antigravity) (pull_request) Successful in 52s
CI / TEST (Codex) (pull_request) Successful in 2m58s
2026-07-21 13:46:48 +08:00
Jeffery 55b349da07 test(ai-review): 補上安全與 Gitea 契約測試 2026-07-21 13:46:43 +08:00
Jeffery 6a26984bee fix(ai-review): 強化安全防護與建問題留言處理 2026-07-21 13:46:39 +08:00
ai-review-bot a8561b4257 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 26s
CI / TEST (Antigravity) (pull_request) Failing after 32s
2026-07-20 10:45:36 +00:00
JefferyandClaude Opus 4.8 5ac73f290e refactor(review): 診斷輸出長度常數提升為模組層級
node-actions/template: CI / BUILD (pull_request) Successful in 5s
CI / TEST (Claude) (pull_request) Successful in 32s
CI / TEST (Antigravity) (pull_request) Successful in 49s
CI / TEST (Codex) (pull_request) Successful in 2m22s
依議題 #24 Bard 建議,將 agentFailureDetail 內的 INPUT_LIMIT/
AGENT_DIAGNOSTIC_OUTPUT_LIMIT 移至模組頂層,與既有 PER_FILE_DIFF_LIMIT/
TOTAL_DIFF_LIMIT 並列集中管理;值與行為不變。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:43:06 +08:00
ai-review-bot 05153bba8e chore: update ai-review findings [ai-review-bot][failure]
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Codex) (pull_request) Failing after 25s
CI / TEST (Claude) (pull_request) Failing after 25s
CI / TEST (Antigravity) (pull_request) Failing after 32s
2026-07-20 10:36:03 +00:00
JefferyandClaude Opus 4.8 d7cecc8fa8 chore(ai-review findings): 寫回議題 #11–#23 只來自議題的待人工 findings
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Successful in 24s
CI / TEST (Antigravity) (pull_request) Successful in 58s
CI / TEST (Codex) (pull_request) Successful in 2m51s
以 --issue all 處理 13 個建問題模式追蹤議題,逐條對照目前程式碼與 exclusions.json 後:
 已解決 18 條(現行程式碼已修)、🚫 誤報 28 條(已列入 exclusions,不重複新增)、
⏭️ 待人工 44 條(設計/效能/慣例取捨,依主題去重寫回 42 條追蹤)。各議題已留言處理進度並關閉。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:33:01 +08:00
ai-review-bot 0c29daa014 chore: update ai-review findings [ai-review-bot][failure]
CI / TEST (Antigravity) (pull_request) Failing after 51s
node-actions/template: CI / BUILD (pull_request) Successful in 28s
CI / TEST (Claude) (pull_request) Failing after 31s
CI / TEST (Codex) (pull_request) Failing after 41s
2026-07-20 10:07:27 +00:00
JefferyandClaude Opus 4.8 d8dd5ce92e chore(ai-review 狀態): 記錄 issue #10 誤報排除並回寫待人工 findings
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Successful in 27s
CI / TEST (Antigravity) (pull_request) Successful in 50s
CI / TEST (Codex) (pull_request) Successful in 3m2s
- exclusions.json 新增一條誤報(gitrepo.js:276 使用者名稱固定 ai-review-bot,依管理員指示)。
- 新增 findings 檔追蹤 4 條待人工處理問題(步驟編號硬編碼、兩處缺測試、debug 輸出取捨)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:04:23 +08:00
JefferyandClaude Opus 4.8 cc9c8afed7 docs(ai-review): 同步 JSDoc 對 main() 閉包改名的引用
配合 postComment/ensureIssueCreated 改名,更新 gitea.js 與 templates.js
JSDoc 內對兩個閉包的引用名稱。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:04:23 +08:00
JefferyandClaude Opus 4.8 0c108ab7f9 refactor(ai-review): 重新命名留言閉包並抽出診斷輸出長度常數
- postComment → queueOrPostComment:涵蓋建問題模式下可能只暫存不立即發布的語義。
- ensureIssueCreated → createIssueAndFlushBufferedComments:明示建立 issue 並清空暫存留言的完整行為。
- review.js agentFailureDetail 的 500 字上限抽為具名常數 AGENT_DIAGNOSTIC_OUTPUT_LIMIT。
純內部命名與可讀性調整,無外部行為變更。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:04:23 +08:00
JefferyandClaude Opus 4.8 83dd56f37f fix(resolveMergeBase): deepen HEAD 改以目前 HEAD 的 SHA 補抓 PR head 歷史
原策略 fetch 遠端符號 HEAD,會被伺服器解析為遠端預設分支,
只加深預設分支歷史、補不到目前 checkout 的 PR head,淺層時仍可能算不出 merge-base。
改先 rev-parse 取得 HEAD 的 commit SHA,以該 SHA 加深 PR head 側歷史。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:04:23 +08:00
ai-review-bot d354f30c27 chore: update ai-review findings [ai-review-bot][failure]
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Failing after 20s
CI / TEST (Codex) (pull_request) Failing after 27s
CI / TEST (Antigravity) (pull_request) Failing after 33s
2026-07-20 09:54:00 +00:00
JefferyandClaude Opus 4.8 b5264141c4 fix(推送觸發 CI): push 前重置 checkout 的自動 token extraheader,改以 PAT 身分推送
node-actions/template: CI / BUILD (pull_request) Successful in 3s
CI / TEST (Claude) (pull_request) Successful in 32s
CI / TEST (Antigravity) (pull_request) Successful in 52s
CI / TEST (Codex) (pull_request) Successful in 2m38s
checkout 於 http.<serverUrl>/.extraheader 持久化自動 Actions token;沿用它推送會被
Gitea 視為自動 token 觸發而不再觸發 workflow。改為推送前於同 scope 先空值重置、再注入
PAT 的 Authorization,使 findings 結果 commit 以 PAT 身分推送、觸發 PR synchronize。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 17:51:21 +08:00
ai-review-bot 4b779ee00b chore: update ai-review findings [ai-review-bot][failure] 2026-07-20 09:43:57 +00:00
24 changed files with 2885 additions and 558 deletions
File diff suppressed because it is too large Load Diff
@@ -7,72 +7,6 @@
"version": "0.0.8", "version": "0.0.8",
"model": "(工具預設)" "model": "(工具預設)"
}, },
"findings": [ "findings": [],
{
"id": "F001",
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 210,
"endLine": 224,
"problem": "`ensureIssueCreated` 同時建立 issue、修改外層 `issue` 狀態、逐筆清空 `issueBuffer`,但整段流程沒有可重入或冪等機制。若 issue 建立成功後,寫入其中一則暫存留言時失敗,主流程會中止;重跑後又會建立另一個 issue,留下內容不完整的孤兒 issue。這種依賴閉包可變狀態的半完成狀態,半年後要加入重試、續傳或測試失敗情境都會很痛苦。",
"suggestion": "把「建立追蹤 issue 並沖刷留言」抽成獨立、可注入 Gitea client 的服務函式,明確回傳 issue 與已寫入進度;建立前以 PR 編號或隱藏識別標記查找既有追蹤 issue,讓重跑能接續而非重複建立。至少也應保留已建立的 issue 編號並在錯誤訊息中回報,避免留下無法追蹤的半成品。",
"suggestedCode": ""
},
{
"id": "F002",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 306,
"endLine": 359,
"problem": "建問題模式的核心流程已大幅改變,但 diff 中沒有對應測試驗證各分支:有保留問題時才建立 issue、暫存留言依序送出、無問題時靜默通過、嚴重與非嚴重問題送往正確位置,以及標籤失敗後仍須回貼 PR 連結。這些分支牽涉多次外部 API 呼叫與狀態切換,未測試時很容易出現漏留言、留言送錯 PR/issue,或在 `issue` 尚未建立時解參考的回歸。",
"suggestion": "新增主流程測試並 mock Gitea、agent 與 git 操作,至少涵蓋:`kept=[]` 時不建立 issue 且不留言;僅嚴重問題;僅警告/建議;混合問題;建立 issue 或寫入暫存留言失敗;標籤查詢、AI 選標籤及補掛標籤失敗時仍回貼 issue 連結。除了呼叫次數,也應斷言 API 呼叫順序、目標 issue 編號及留言內容。",
"suggestedCode": ""
},
{
"id": "F003",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitea.js",
"startLine": 188,
"endLine": 191,
"problem": "新增的 `addLabelsToIssue` 沒有對應測試,尚未驗證空值捷徑與實際 API 請求格式。這個函式位於新建問題流程的收尾路徑;若 endpoint、HTTP method 或 `{ labels }` payload 不符預期,追蹤 issue 將無法取得標籤,而空陣列是否真的不發出請求也未被保護。",
"suggestion": "新增單元測試,分別傳入 `undefined`、`null`、空陣列及多個 label id;斷言前三者回傳 `null` 且完全不呼叫 API,多個 id 時以 POST 呼叫正確的 ownerrepoissue endpoint 並傳送 `{ labels: [...] }`,另驗證 API 拋錯會原樣往上傳遞。",
"suggestedCode": ""
},
{
"id": "F004",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 100,
"endLine": 122,
"problem": "`resolveMergeBase` 新增多階段 fetch 與淺層 checkout 修復邏輯,但沒有看到測試驗證成功、降級與最終失敗路徑。尤其初次 merge-base 失敗後,淺層與非淺層 repository 會走不同路徑,且多個 `tryGit` 失敗會被刻意吞掉;若參數、refspec 或重試順序有誤,只會在實際 CI checkout 深度不足時才暴露。",
"suggestion": "以 stub 的 git 執行器或暫存 repository 補齊案例:首次 merge-base 成功;淺層 repository 經 `--unshallow` 後成功;`--unshallow` 失敗但 `--deepen=1000` 後成功;非淺層首次失敗後重試成功;所有策略失敗時拋出含 `baseRef` 且保留原始 `cause` 的錯誤。並斷言 base/head fetch 的 refspec 與執行順序。",
"suggestedCode": ""
},
{
"id": "F005",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 17,
"endLine": 45,
"problem": "新增的 agent 失敗診斷涵蓋逾時、數字或字串 exit code、signal、空輸出及 500 字截斷等多個邊界,但沒有測試鎖定輸出。這些資訊只在失敗路徑出現,正常審查不會自然覆蓋;日後修改時可能悄悄遺失真正的 CLI 錯誤內容,或破壞單行與截斷約束。",
"suggestion": "將摘要邏輯匯出供測試,或透過失敗的 `runAttackers``runDefenders``fillPurposes` 測試間接斷言 log。至少覆蓋 killed、數字 exit code、字串 code、signal、stderr 與 stdout 同時存在、超過 500 字、完全無資訊,以及 `res` 為 nullundefined 的案例。",
"suggestedCode": ""
}
],
"excluded": [] "excluded": []
} }
@@ -7,137 +7,6 @@
"version": "0.0.8", "version": "0.0.8",
"model": "(工具預設)" "model": "(工具預設)"
}, },
"findings": [ "findings": [],
{
"id": "F001",
"reviewer": "Assassin",
"focus": "security",
"badge": "🗡️",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 72,
"endLine": 81,
"problem": "開啟 ACTIONS_STEP_DEBUG=true 後,會把攻擊者可間接操控的 AI CLI stderrstdout(經黑名單式 redactSecrets 遮罩)寫入長期保存的 CI log。短 token、JWT、含標點或空白的密碼、非典型金鑰及 PII 仍可能繞過遮罩。建議即使除錯模式也只記錄退出碼/signal/逾時/診斷 ID,若需原文則寫入受限、短期保存、需授權取得的安全 artifact。",
"suggestion": "CI log 不輸出 AI CLI 原文,即使 debug 模式;需要原文時改寫入存取受限的安全 artifact,並套用允許清單式結構化診斷與 PII/機密掃描。此為安全性與可除錯性的政策取捨:現行已刻意保留 debug-gated 遮罩輸出,是否完全移除需維護者裁示。",
"suggestedCode": "function agentFailureDetail(res) {\n const err = res && res.error;\n if (err && err.killed) return '已逾時終止';\n if (err && typeof err.code === 'number') return `exit ${err.code}`;\n if (err && err.signal) return `signal ${err.signal}`;\n return 'AI CLI 執行失敗(原始輸出已隱藏)';\n}"
},
{
"id": "F002",
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 176,
"endLine": 218,
"problem": "main() 內新增 issueBuffer、可變 issue、postComment 與 ensureIssueCreated 閉包,後續多處依 ctx.createIssue 分支。留言路由、issue 生命週期、標籤、相依關係與審查編排共享同一批可變狀態;未來增加發布目的地或重試策略時須同步理解並修改整個超長主流程,測試也只能透過 main() 間接覆蓋。",
"suggestion": "抽出具明確介面的發布器(如 PrReviewPublisher 與 IssueReviewPublisher),封裝留言暫存、issue 建立、沖刷、問題明細與收束關聯;main() 只呼叫一致的 publishContextpublishFindingsfinalize,便於分別注入假的 Gitea client 測試兩種模式並移除散落的模式判斷。屬大範圍重構+設計取捨,需維護者確認方向。",
"suggestedCode": ""
},
{
"id": "F003",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 184,
"endLine": 245,
"problem": "建問題模式新增「issue 建立前暫存留言、建立後依序沖刷」的狀態流程,但沒有對應測試。尚未驗證 createIssue 或沖刷途中拋錯、空 buffer、重複呼叫、以及一般/建問題模式留言目的地是否正確;失敗路徑可能造成 issue 已建立但內容不完整或留言誤發到 PR。",
"suggestion": "補主流程整合測試並 mock Gitea API,驗證:一般模式直接發 PR 且記錄留言 id;issue 未建立時只暫存;建立後依序沖刷並清空 buffer;createIssue 與第 N 則沖刷留言失敗時以失敗結束且不再發布後續內容。屬測試架構決策(專案目前無測試框架)。",
"suggestedCode": ""
},
{
"id": "F004",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 309,
"endLine": 395,
"problem": "建問題模式核心分支被大幅改寫,但沒有測試驗證各種 findings 組合與 API 失敗行為。kept.length===0、只有嚴重、只有警告/建議、兩者皆有,以及標籤挑選/issue 建立/PR 回貼連結/相依 API 失敗等路徑尚未試煉,無法確認「無問題完全靜默」「有問題才建 issue」及相依失敗僅降級等契約成立。",
"suggestion": "以參數化測試覆蓋 findings 四種組合,斷言 API 呼叫順序/次數/issue number/統計;並分別讓 listLabelsselectLabelscreateIssue/問題留言/PR 連結留言/addIssueDependency 拋錯,驗證哪些中止、哪些僅記警告續行;零 findings 時斷言所有寫入 API 皆不呼叫。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F005",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitea.js",
"startLine": 171,
"endLine": 215,
"problem": "新增的問題相依 API 包裝沒有測試驗證 endpoint、HTTP method 與 payload。特別是 addIssueDependency 容易顛倒的「PR 相依於 issue」方向未被斷言;若 index 或 body 欄位放反,合併阻擋語意會相反。(原併列的 addLabelsToIssue 已於本次移除。)",
"suggestion": "mock 底層 API,對相依關係使用不同的 PR/issue 編號,精確斷言 URL 指向 PR、body.index 指向阻擋來源 issue,並覆蓋 API 非 2xx 時錯誤原樣往上拋出的案例。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F006",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 103,
"endLine": 157,
"problem": "resolveMergeBase 新增多階段 fetch/重試策略,卻沒有測試鎖定淺層與失敗路徑:首次成功提早返回、非 shallow 不 unshallow、某次 fetch 失敗後仍嘗試下一策略、補抓成功立即停止、全部失敗時診斷與 cause 是否完整;易因呼叫順序或 off-by-one 在 runner 上才暴露。",
"suggestion": "將 git 執行器注入或 stub,建立表格化案例覆蓋首次成功/unshallow 後成功/deepen base 後成功/deepen HEAD 後成功/各 fetch 個別失敗/全部失敗;逐案斷言 git 參數與順序、成功後不再額外 fetch,並檢查最終錯誤含各策略成敗摘要且保留首次錯誤為 cause。屬可測性重構+測試架構決策。",
"suggestedCode": ""
},
{
"id": "F007",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 249,
"endLine": 289,
"problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)",
"suggestion": "mock git 執行器與 URLenv 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F008",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 16,
"endLine": 80,
"problem": "redactSecretsagentFailureDetail 直接決定 CI 日誌是否洩漏內容及失敗診斷是否可用,但沒有測試覆蓋。空值、控制字元、Authorization/Bearer、URL 帳密、各種 token 樣式、截斷,以及 ACTIONS_STEP_DEBUG 大小寫與未啟用時隱藏 stdout/stderr 等邊界尚未驗證。",
"suggestion": "為這兩個純函式補單元測試(必要時受控匯出或抽獨立模組),以假憑證逐一測試遮罩規則與換行注入並斷言輸出不含原始秘密;保存還原 ACTIONS_STEP_DEBUG 驗證預設/true/混合大小寫/逾時/exit codesignal/無 error/超長輸出。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F009",
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "action.yml",
"startLine": 47,
"endLine": 61,
"problem": "push-token 的用途與退回行為在區塊註解、欄位描述、required 與 default 註解中反覆說明,且單行 description 過長,資訊雖完整但重複,日後修改語意易只改到一處。",
"suggestion": "保留一段「為何需要 PAT」的必要背景,其餘讓欄位名稱、required、default 自行表意,將 description 收斂成呼叫端真正需要知道的契約。註:本專案採 doc-funcs 高密度註解慣例,是否精簡屬慣例取捨,需維護者確認。",
"suggestedCode": " # 專用推送 PAT;以 PAT 推送可重新觸發 CI。留空時沿用 token。\n push-token:\n description: '推送審查結果 commit 的 PAT(留空時沿用 token'\n required: false\n default: ''"
},
{
"id": "F010",
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 203,
"endLine": 226,
"problem": "ensureIssueCreated 之名帶有「已存在便沿用」的冪等語意,實際卻無條件建立新 issue 並悄悄改寫外層 issue;名稱、行為與副作用不一致,閱讀呼叫處易形成錯誤預期。",
"suggestion": "若此函式只允許呼叫一次,改用直接表達「建立並沖刷暫存留言」的名稱並回傳建立結果,由呼叫端明確指派 issue,讓資料流一眼可見。註:本項與 F002(抽出 publisher 大重構)指向同一段核心流程、維護者正審視中,宜與該重構一併處理,避免重複改動。",
"suggestedCode": "const createIssueAndFlushBuffer = async (labelIds = []) => {\n const createdIssue = await gitea.createIssue(ctx, {\n title: ctx.prTitle || `AI Code ReviewPR #${ctx.prNumber}`,\n body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),\n labels: labelIds,\n });\n for (const body of issueBuffer) {\n await gitea.createCommentOnIssue(ctx, createdIssue.number, body);\n }\n issueBuffer.length = 0;\n return createdIssue;\n};\nissue = await createIssueAndFlushBuffer(labelIds);"
}
],
"excluded": [] "excluded": []
} }
@@ -7,111 +7,6 @@
"version": "0.0.8", "version": "0.0.8",
"model": "(工具預設)" "model": "(工具預設)"
}, },
"findings": [ "findings": [],
{
"id": "F001",
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 121,
"endLine": 145,
"problem": "流程步驟編號硬編碼在主流程註解、日誌字串、README 與多個函式 JSDoc 中;插入一個步驟就要同步修改大量檔案,文件與實作高耦合,日後調整流程易漏改而互相矛盾。",
"suggestion": "共用函式 JSDoc 改以語意階段名稱描述、不引用易變動的數字;日誌集中定義階段名稱或由單一流程描述產生編號;README 流程圖也以語意名稱為主。屬跨檔重構+設計取捨。",
"suggestedCode": "const STAGE = Object.freeze({\n TOOL_DETECTION: '偵測工具',\n DIFF_COLLECTION: '整理差異',\n ATTACK_REVIEW: '攻擊方審查',\n DEFENSE_REVIEW: '防守方裁決',\n});\nlog(STAGE.DIFF_COLLECTION, 'INF', message);"
},
{
"id": "F002",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitea.js",
"startLine": 172,
"endLine": 215,
"problem": "建立 issue 相依關係的 API 封裝(addIssueDependency)沒有對應測試:尚未驗證 URL 的 issue 編號與 {index, owner, repo} payload 正確、以及非 2xx 錯誤是否原樣往上傳遞。(原併提的 addLabelsToIssue 已於先前 commit 移除。)",
"suggestion": "mock 底層 API,驗證 addIssueDependency 的 URL issue 編號與 payload,加入 4xx/5xx 拋錯案例,並搭配主流程測試確認相依失敗會被降級而不阻斷審查。屬測試架構決策(專案無測試框架)。",
"suggestedCode": ""
},
{
"id": "F003",
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 47,
"endLine": 65,
"problem": "tryGit 將所有 git 失敗壓成布林值,resolveMergeBase 的診斷只知策略成敗、無法區分認證/refspec/網路/版本問題;CI 出錯時維護者只能重跑或自行重現,診斷成本高。",
"suggestion": "讓嘗試結果保留結構化且已清理的錯誤分類(exit code、git 子命令、安全化後短訊息),仍避免記錄遠端 URL 或憑證;最終錯誤彙整足以行動的原因,並可對各失敗類型做單元測試。屬診斷重構+需測試。",
"suggestedCode": "function tryGit(cwd, ...args) {\n try {\n git(cwd, ...args);\n return { ok: true };\n } catch (error) {\n return { ok: false, code: error.status ?? error.code ?? null, reason: sanitizeGitError(error) };\n }\n}"
},
{
"id": "F004",
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 270,
"endLine": 291,
"problem": "findings 推送的認證路徑沒有測試。註:原「PAT 直接推送 vs origin 失敗重試」雙路徑已於先前 commit 合併為「一律以 token 明確認證推送」,測試仍待補:空 token 邊界、推送目標正確、錯誤與輸出不含 token 原文。",
"suggestion": "補單元測試攔截 git 參數/env:斷言以 token 認證推送、推送目標 refspec 正確、呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。",
"suggestedCode": ""
},
{
"id": "F005",
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "action.yml",
"startLine": 3,
"endLine": 3,
"problem": "檔頭「更新時間」為手動維護的固定字串,與實際檔案更新時間不一致;散落各檔的手動時間戳容易走調,讀者無法判斷可信度。",
"suggestion": "屬 jsc spec-time-log 慣例(各檔頭「更新時間」由 doc-funcs 流程統一產生/同步)。是否移除改用版控紀錄、或如何統一更新,宜由 doc-funcs 流程處理,不在 resolve 逐條硬改。",
"suggestedCode": ""
},
{
"id": "F006",
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "readme.md",
"startLine": 3,
"endLine": 3,
"problem": "README 檔頭手動「更新時間」與實際更新時間不符,並與 action.yml、src/index.js 重複保存同類易過期資訊,形成多個不一致的真相來源。",
"suggestion": "同 F005:屬 jsc spec-time-log 慣例,交 doc-funcs 流程統一維護(移除或自動注入)。",
"suggestedCode": ""
},
{
"id": "F007",
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 7,
"endLine": 7,
"problem": "啟動橫幅硬編碼的「更新時間」與程式實際更新時間不一致,每次改程式都要人工校準,製造噪音並讓執行日誌呈現失真版本資訊。",
"suggestion": "同 F005:屬 jsc spec-time-log 慣例,交 doc-funcs 流程統一維護;若日誌需辨識版本,可改顯示 CI 注入的 commit SHA/版本號(屬慣例調整)。",
"suggestedCode": ""
},
{
"id": "F008",
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 179,
"endLine": 231,
"problem": "main() 內新增兩個帶完整 JSDoc 的閉包函式與一大段「步驟 2:延後執行」說明,使主流程在進入步驟 3 前被近六十行細節打斷,留言路由、issue 建立與流程說明混在同一層,閱讀節奏沉重。",
"suggestion": "將留言路由與 issue 建立封裝成具語義名稱的輔助物件/模組(例如 createCommentPublisher),讓 main() 只保留流程級呼叫;延後清理理由縮成貼近呼叫點的簡短註解。與 F001(語意階段名稱)同屬主流程重構,宜一併處理。",
"suggestedCode": "const comments = createCommentPublisher({ ctx, gitea });\nawait comments.post(templates.toolComment({ /* ... */ }));"
}
],
"excluded": [] "excluded": []
} }
@@ -0,0 +1,12 @@
{
"generatedAt": "2026/07/20 18:00:02",
"commitSha": "d354f30c270e99baa9dcfc6aa76510710c0193ca",
"prNumber": null,
"tool": {
"name": "code-review-resolve",
"version": "0.0.8",
"model": "(工具預設)"
},
"findings": [],
"excluded": []
}
@@ -0,0 +1,12 @@
{
"generatedAt": "2026/07/20 18:31:07",
"commitSha": "0c29daa01434d6b6fa1b20ce81c513c75b1b1c6a",
"prNumber": null,
"tool": {
"name": "code-review-resolve",
"version": "0.0.9",
"model": "(工具預設)"
},
"findings": [],
"excluded": []
}
@@ -0,0 +1,184 @@
{
"generatedAt": "2026/07/21 14:27:42",
"commitSha": "9e7f8a2a0a807c26e12e9bf0381e7decfb9e5568",
"prNumber": 6,
"tool": {
"name": "codex",
"version": "codex-cli 0.144.6",
"model": "gpt-5.5"
},
"findings": [],
"excluded": [
{
"reviewer": "Assassin",
"focus": "security",
"badge": "🗡️",
"severity": "嚴重",
"file": "src/lib/diagnostics.js",
"startLine": 23,
"endLine": 23,
"problem": "攻擊者只要讓 AI CLI 在 debug 模式下把 `Authorization: token <secret>` 或 `Authorization: Basic <base64>` 印到 stderr/stdout,這條遮罩規則只會遮掉 `token``Basic` 這個 scheme,後面的真正憑證仍會進 CI log。這正好打穿本檔宣稱的「安全診斷」防線,PAT 或 API token 會被長期保存並暴露給可讀 log 的人。",
"suggestion": "把整個 Authorization header value 視為機密,不要只特判 Bearer。應遮掉 scheme 與 credential 全段;若要保留 scheme,也只能保留固定白名單字樣,不能留下後面的 token。",
"suggestedCode": ".replace(/(authorization\\s*[:=]\\s*)(?:\\S+\\s+)?\\S+/gi, '$1***')",
"id": "F001",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 finding 已指出除錯診斷輸出中 Authorization 遮罩規則可能只遮到 scheme、留下後方憑證,並造成 CI log 洩漏,與本條同一問題。"
}
}
},
{
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 181,
"endLine": 239,
"problem": "`main()` 這次把「PR 留言」、「issue 暫存」、「issue 建立後 flush」三種狀態直接塞進閉包變數 `currentRunCommentIds`、`issueBuffer`、`issue` 與 `queueOrPostComment()`。未來只要再新增一種落地方式或調整留言順序,維護者必須同時理解整個主流程的時序,否則很容易出現留言漏發、舊留言誤標、或 issue 尚未建立卻被使用的狀態錯誤。這段已經不是單純編排,而是隱含一個留言發布狀態機。",
"suggestion": "把留言目的地抽成小型模組,例如 `CommentSink` / `ReviewPublisher`,讓 `main()` 只負責流程編排:`publisher.postContext()`、`publisher.flushIssue()`、`publisher.resolveOldPrComments()`。建問題模式的暫存與 flush 可獨立測試,半年後改流程時不用在 `main()` 裡追閉包狀態。",
"suggestedCode": "",
"id": "F007",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列與發布狀態機耦合的同一問題。"
}
}
},
{
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "readme.md",
"startLine": 56,
"endLine": 132,
"problem": "README 這次把大量連結從 `branch/master` 改成 `branch/develop`,同時保留許多硬編碼行號。這種文件會隨分支名稱、預設分支、或任一檔案插入幾行就整批失準;未來維護者每次調整程式都得同步修幾十個 URL,否則文件看起來精準、實際上卻導到錯誤位置。",
"suggestion": "改用 repo 相對連結並盡量避免行號,例如 `src/lib/log.js` 或 `src/lib/log.js#L20` 只保留少數真正需要精準定位的 API。若這段是產生文件,請把分支名稱與連結格式集中在產生器設定,不要把同一個 branch 字串散落在 README 表格裡。",
"suggestedCode": "",
"id": "F008",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已多次指出 README 大量硬編 branch 與行號深連結,造成文件與原始碼高同步成本;本條為同一問題。"
}
}
},
{
"reviewer": "Mage",
"focus": "logic",
"badge": "🔮",
"severity": "警告",
"file": "src/index.js",
"startLine": 217,
"endLine": 225,
"problem": "建問題模式先建立 issue,再逐則 flush `issueBuffer`,但中途任一留言 API 失敗會直接拋出,沒有回滾或冪等處理。最小重現:`gitea.createIssue` 成功建立 #10,第一則 buffered comment 成功,第二則因 500/timeout 失敗 → 主流程 exit 1,PR 沒有 issue 連結、也沒有結果 commit;重跑時又建立 #11,留下半成品 issue #10。這會在暫時性 API 錯誤下造成重複追蹤 issue 與狀態不一致。",
"suggestion": "建立 issue 後立即寫入可重試識別資訊,重跑前先搜尋/復用既有本 PR 的 AI review issue;或在 flush 失敗時捕捉錯誤,將已建立 issue 補上失敗狀態與 PR 回連,再讓流程以可診斷方式收束。",
"suggestedCode": "",
"id": "F010",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(命中已知排除事項且重複)。issue 建立後 flush 暫存留言失敗、重跑又建立重複 issue,正是已知排除事項與多筆歷史 findings 已記錄的問題。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 241,
"problem": "建問題模式新增了「先暫存留言、確定有保留問題才建立 issue、再 flush 暫存留言」的流程,但目前新增測試只驗到低階 helper,沒有驗證主流程在 create-issue=true 時的留言去向與靜默通過行為。這裡若順序錯了,可能會把原本應該留在 issue 的工具/diff/角色留言發到 PR,或在無保留問題時留下不該出現的 PR 留言。",
"suggestion": "請補主流程層級測試,stub `gitea`、`review.runAttackers`、`review.runDefenders`、`agents.detectTool`,至少驗證:\n\n| 情境 | 應斷言 |\n| --- | --- |\n| `createIssue=true` 且 `kept.length > 0` | 建 issue 前的情境留言先暫存,issue 建立後依序寫入該 issue |\n| `createIssue=true` 且 `kept.length === 0` | 不建立 issue、不對 PR 留言、不呼叫 `resolveOldComments` |\n| 一般模式 | 情境留言仍發到 PR,且 id 被加入 `currentRunCommentIds` |",
"suggestedCode": "",
"id": "F012",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已涵蓋 create-issue 模式中留言先暫存、issue 建立後依序寫入、無 finding 靜默通過與一般模式留言目的地缺少主流程測試。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 354,
"endLine": 395,
"problem": "新增的建問題模式會依嚴重度分流:嚴重問題改發到 issue、PR 回貼 issue 連結,且只有嚴重問題才呼叫 `addIssueDependency`。目前測試沒有覆蓋這個決策矩陣,等於沒有驗證「警告/建議不阻擋合併」與「嚴重問題要建立相依」這兩個關鍵行為。",
"suggestion": "請補 create-issue 模式的結果分流測試,至少涵蓋:\n\n| findings | 應斷言 |\n| --- | --- |\n| 只有警告/建議 | 建 issue、逐條留言、PR 回貼連結,但不呼叫 `addIssueDependency` |\n| 有嚴重問題 | 呼叫 `postSevereToIssue`、PR 回貼連結,並呼叫 `addIssueDependency(ctx, ctx.prNumber, issue.number)` |\n| `addIssueDependency` 拋錯 | 流程降級完成,不中斷收尾 commit |",
"suggestedCode": "",
"id": "F013",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式的嚴重/非嚴重分流、PR 回貼、相依 API 降級與核心分支缺少測試;本條屬同一測試缺口。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 130,
"endLine": 188,
"problem": "`resolveMergeBase` 新增了多段 fetch 補歷史策略、每次補抓後重試 merge-base、以及淺層 repo 才執行 `--unshallow` 的行為,但測試只驗證不安全 baseRef 會被拒絕,沒有驗證這些成功/失敗路徑。這段是 diff 基準的核心,一旦 fallback 順序或條件壞掉,review 可能拿錯範圍或在 shallow checkout 直接失敗。",
"suggestion": "請把 git 執行層抽成可注入或以臨時 repo 測試,補齊:首次 merge-base 成功不跑 fallback、deepen base 後成功即停止、deepen PR HEAD 用目前 HEAD SHA、只有 shallow repo 才嘗試 `--unshallow`、全部策略失敗時錯誤訊息含診斷且保留 cause。",
"suggestedCode": "",
"id": "F014",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、shallow 與非 shallow 分支、停止條件、降級與最終失敗診斷缺少測試提出相同問題。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 331,
"endLine": 366,
"problem": "`pushWithCredential` 是這次結果 commit 能否用 PAT 觸發下一輪 CI 的關鍵,但目前沒有測試驗證它真的清掉 checkout 的自動 token、再注入 PAT Authorization,也沒有測失敗時不外洩 remote URL/token。這條失敗路徑沒被試煉過,可能讓 `[failure]` 結果 commit 無法觸發快速回報,或在錯誤訊息裡洩漏認證資訊。",
"suggestion": "請補 `commitAndPushFindings``pushWithCredential` 的測試,stub `execFileSync` 驗證:`git push` 使用不含帳密的 remote URL、`GIT_CONFIG_COUNT=2`、第 0 筆 extraheader 為空值、第 1 筆為 Basic Authorization、remote origin 不符時拒絕、push 失敗時拋出的錯誤不包含 token 或 remote URL。",
"suggestedCode": "",
"id": "F015",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已涵蓋 pushWithCredentialcommitAndPushFindings 的認證推送策略、token 注入、推送目標與失敗時不外洩 token 或 URL 的測試缺口。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "建議",
"file": "src/lib/diagnostics.js",
"startLine": 21,
"endLine": 32,
"problem": "`redactSecrets` 新增多個遮罩規則與控制字元單行化,但測試只透過 `agentFailureDetail` 間接覆蓋 Authorization 與 token 兩種格式。URL 內嵌帳密、長 token、GitHub token 樣式、控制字元注入與 null/undefined 邊界都還沒被直接驗證。",
"suggestion": "請補 `redactSecrets` 的直接單元測試,包含:`https://user:pass@host`、`ghp_...`、40 字以上 token、含換行/tab 的假日誌注入、`null``undefined`。斷言重點是輸出為單行、敏感片段被 `***` 取代,且一般錯誤文字仍保留可診斷性。",
"suggestedCode": "",
"id": "F016",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已記錄失敗診斷與 redactSecrets 對 Authorization、URL 帳密、token 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。"
}
}
}
]
}
@@ -0,0 +1,184 @@
{
"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": [],
"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 已記錄 pushWithCredentialpushToken 認證推送路徑缺少測試,包含 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 成功不走 fallbackdeepen 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 已涵蓋 redactSecretsagentFailureDetail 對控制字元、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 發到 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 涵蓋。"
}
}
}
]
}
@@ -0,0 +1,184 @@
{
"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": [],
"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/createIssueComment2. kept>0 時先 flush pending issue comments 再發 finding3. createIssue 拋錯時 fallbackToPrComments 會發回 PR 並記錄 currentRunCommentIds4. 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 HEADshallow 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 不含 tokenenv 內含 GIT_CONFIG_COUNT 與 Authorization extraheaderheadRef 不安全時在 git add/commit/push 前拋錯;remote origin 不符與 push 失敗時錯誤訊息不包含 token、repo URL 或 Basic header。",
"suggestedCode": "",
"id": "F013",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已涵蓋 commitAndPushFindings 與 pushWithCredential 的推送策略、認證遮蔽、失敗路徑與 token 不外洩等測試缺口。"
}
}
}
]
}
+4 -6
View File
@@ -1,6 +1,6 @@
# ============================================================================ # ============================================================================
# 用途:定義 AI Code Review Node action 的名稱、輸入參數與 Node.js 24 進入點,供 Gitea / GitHub workflow 以 uses 引用。 # 用途:定義 AI Code Review Node action 的名稱、輸入參數與 Node.js 24 進入點,供 Gitea / GitHub workflow 以 uses 引用。
# 更新時間:2026/07/17 18:49:58 # 更新時間:2026/07/28 13:57:24
# ============================================================================ # ============================================================================
# Gitea / GitHub node action 的 manifestaction.yml): # Gitea / GitHub node action 的 manifestaction.yml):
# 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs), # 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs),
@@ -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:用於對 PRissue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。 # Gitea API token:用於對 PRissue 留言審查結果,以及 push 審查結果檔(findingsexclusions)回 repo。
token: token:
# 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。 # 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。
# 建議傳入「能觸發 CI 的 PAT」:以自動 tokengitea.token / GITHUB_TOKEN)推送的結果 commit 不會 # 建議使用 repo 限定、最小權限且可輪替的 token;不得在不受信任 fork PR 暴露可寫入 token。
# 再觸發 CI,導致新 head 缺檢查而卡合併;改用 PAT 推送會讓 PR 的 synchronize 事件再觸發 CI description: 'Gitea API tokenPRissue 留言與 push findings 用;請以 repo 限定、最小權限且可輪替的 secrets 傳入)'
# 由主程式步驟 1 快速回報([success]/[failure])廉價地把結果蓋到新 head。
description: 'Gitea API tokenPR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)'
# 必填:缺少 token 無法呼叫 Gitea APIaction 無法運作。 # 必填:缺少 token 無法呼叫 Gitea APIaction 無法運作。
required: true required: true
# 指定 AI 工具使用的模型名稱。 # 指定 AI 工具使用的模型名稱。
+2 -1
View File
@@ -4,7 +4,8 @@
"description": "AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings", "description": "AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"build": "ncc build src/index.js -o dist" "build": "ncc build src/index.js -o dist",
"test": "node --test"
}, },
"author": "Jeffery", "author": "Jeffery",
"license": "MIT" "license": "MIT"
+61 -61
View File
@@ -1,6 +1,6 @@
# AI Code Review # AI Code Review
> 更新時間:2026/07/17 18:49:58 > 更新時間:2026/07/28 13:57:24
AI 多角色 code review 的 Gitea **node action**`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]``[failure]`)供下次觸發快速回報。 AI 多角色 code review 的 Gitea **node action**`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]``[failure]`)供下次觸發快速回報。
@@ -38,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]
``` ```
## 專案列表 ## 專案列表
@@ -76,56 +76,56 @@ flowchart TD
| 功能名稱 | 功能描述 | | 功能名稱 | 功能描述 |
| --- | --- | | --- | --- |
| [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L20) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) | | [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) |
| [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L41) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) | | [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) |
| [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) | | [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) |
| [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) | | [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js) | [以統一格式輸出一行日誌](#loglog) |
| [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) | | [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/context.js) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) |
| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L79) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | | [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) |
| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L100) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | | [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) |
| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L136) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | | [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) |
| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L157) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | | [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [取得單一檔案的 git diff 內容](#gitrepofilediff) |
| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L175) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | | [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) |
| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L213) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | | [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) |
| [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) | | [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) |
| [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) | | [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) |
| [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) | | [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [對本次 PR 新增一般留言](#giteacreateissuecomment) |
| [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L141) | [列出存取庫可用標籤](#gitealistlabels) | | [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [列出存取庫可用標籤](#gitealistlabels) |
| [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L164) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) | | [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) |
| [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L184) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) | | [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) |
| [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L202) | [編輯既有一般留言](#giteaeditissuecomment) | | [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [編輯既有一般留言](#giteaeditissuecomment) |
| [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L223) | [建立 code review 並掛行內留言](#giteacreatereview) | | [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [建立 code review 並掛行內留言](#giteacreatereview) |
| [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L243) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) | | [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) |
| [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L264) | [列出某 review 的全部行內留言](#gitealistreviewcomments) | | [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [列出某 review 的全部行內留言](#gitealistreviewcomments) |
| [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L287) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) | | [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) |
| [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L53) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) | | [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) |
| [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L98) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) | | [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) |
| [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L144) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) | | [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) |
| [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) | | [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) |
| [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) | | [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js) | [過濾出攻擊方角色](#rolesattackersof) |
| [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) | | [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js) | [過濾出防守方角色](#rolesdefendersof) |
| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L88) | [產生步驟 3 審查工具留言](#templatestoolcomment) | | [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 3 審查工具留言](#templatestoolcomment) |
| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L132) | [產生步驟 4 變更摘要留言](#templatesdiffcomment) | | [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 4 變更摘要留言](#templatesdiffcomment) |
| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L173) | [產生步驟 57 角色登場留言](#templatesrolescomment) | | [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 57 角色登場留言](#templatesrolescomment) |
| [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) | | [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) |
| [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) | | [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) |
| [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) | | [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) |
| [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L309) | [產生建問題模式的 issue 本文](#templatesissuebody) | | [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生建問題模式的 issue 本文](#templatesissuebody) |
| [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L341) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) | | [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) |
| [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L379) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) | | [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) |
| [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L29) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) | | [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) |
| [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L53) | [判斷檔案是否忽略不送審](#reviewisignored) | | [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [判斷檔案是否忽略不送審](#reviewisignored) |
| [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L77) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) | | [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) |
| [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L127) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) | | [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) |
| [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L276) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) | | [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) |
| [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L448) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) | | [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) |
| [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L572) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) | | [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) |
| [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L521) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) | | [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) |
| [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L596) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) | | [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) |
| [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L627) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) | | [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) |
| [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L694) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) | | [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) |
| [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L778) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) | | [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) |
| [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L855) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) | | [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) |
## 使用範例 ## 使用範例
+100 -86
View File
@@ -4,7 +4,7 @@
console.log('================================================'); console.log('================================================');
console.log('Action : AI Code Review'); console.log('Action : AI Code Review');
console.log('用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings'); console.log('用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings');
console.log('更新時間: 2026/07/17 18:49:58'); console.log('更新時間: 2026/07/21 17:19:15');
console.log('================================================'); console.log('================================================');
const fs = require('fs'); const fs = require('fs');
@@ -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/pushfalse=無變更或 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,51 +107,33 @@ 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 工具(antigravitycodexclaude)並留言;
* 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 相依於該 issueaddIssueDependencyissue 關閉前 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沒有嚴重問題且結果 commit 已成功推送時回傳 0;
* 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報); * 若步驟 1 偵測到 `[ai-review-bot][failure]`、前置條件不足,或需要推送結果檔卻未成功推送,
* 回傳 1 僅發生於:步驟 1 偵測到 `[ai-review-bot][failure]` 結果 commit,或前置條件不足 * 回傳 1,避免 token 權限不足或遠端競態讓 workflow 靜默通過。
* (缺 PR 編號/token、找不到 AI 工具)等無法進行審查的情況。
* @remarks * @remarks
* 使用情境:由本檔尾端的頂層呼叫端執行 —— `main().then((code) => process.exit(code))` * 使用情境:由本檔尾端的頂層呼叫端執行 —— `main().then((code) => process.exit(code))`
* 非預期例外由頂層 `catch` 記 ERR log 後以 exit code 1 收場,且刻意不 commit 結果標記, * 非預期例外由頂層 `catch` 記 ERR log 後以 exit code 1 收場,且刻意不 commit 結果標記,
* 讓下一次 workflow 觸發時重新完整審查。警告+建議等級不影響結果標記,只有「嚴重」會使結果 commit * 讓下一次 workflow 觸發時重新完整審查。警告+建議等級不影響結果標記,只有「嚴重」會使結果 commit
* 標記為 failure而「失敗檢查(exit 1)」只由步驟 1 讀到該 failure 結果 commit 時產生,審查本輪直接 exit 1。 * 標記為 failure若結果檔需要 push 卻失敗,本輪直接 exit 1,避免缺權限時無聲放行
* 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變上述結果標記判定。 * 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變上述結果標記判定。
*/ */
async function main() { async function main() {
@@ -182,14 +164,15 @@ async function main() {
// 本回合(一般模式)發出的 PR 留言 idresolveOldComments 標註過時時要跳過這些。 // 本回合(一般模式)發出的 PR 留言 idresolveOldComments 標註過時時要跳過這些。
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 留言物件;
@@ -198,10 +181,10 @@ async function main() {
* 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與 * 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與
* 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。 * 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。
*/ */
const postComment = 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);
@@ -209,25 +192,41 @@ async function main() {
return created; return created;
}; };
/** /**
* 建問題模式:建立追蹤 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 ensureIssueCreated = async (labelIds = []) => { const openTrackingIssue = async (labelIds = []) => {
issue = await gitea.createIssue(ctx, { trackingIssue = await gitea.createIssue(ctx, {
title: ctx.prTitle || `AI Code ReviewPR #${ctx.prNumber}`, title: ctx.prTitle || `AI Code ReviewPR #${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) { while (pendingIssueCommentBodies.length > 0) {
await gitea.createCommentOnIssue(ctx, issue.number, body); const body = pendingIssueCommentBodies[0];
await gitea.createCommentOnIssue(ctx, trackingIssue.number, body);
pendingIssueCommentBodies.shift();
} }
issueBuffer.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();
@@ -245,7 +244,7 @@ async function main() {
} }
log('步驟3', 'INF', `選用工具:${tool.name}${tool.version})。`); log('步驟3', 'INF', `選用工具:${tool.name}${tool.version})。`);
const runLink = `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`; const runLink = `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`;
await postComment( await queueOrPostComment(
templates.toolComment({ templates.toolComment({
toolName: tool.name, toolName: tool.name,
version: tool.version, version: tool.version,
@@ -267,15 +266,15 @@ 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 postComment(templates.nothingToReviewComment(ignoredCount)); await queueOrPostComment(templates.nothingToReviewComment(ignoredCount));
// 已成功產生本回合結果留言(無可審查變更),此時才把舊留言標為過時(本回合留言已排除)。 // 已成功產生本回合結果留言(無可審查變更),此時才把舊留言標為過時(本回合留言已排除)。
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 {
@@ -286,23 +285,25 @@ async function main() {
const diffRows = review.collectDiffRows({ cwd, files, base, gitrepo }); const diffRows = review.collectDiffRows({ cwd, files, base, gitrepo });
await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows }); await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows });
await postComment(templates.diffComment(diffRows, ignoredCount)); await queueOrPostComment(templates.diffComment(diffRows, ignoredCount));
// ── 步驟 5:攻擊方角色登場留言 ───────────────────────────────────────── // ── 步驟 5:攻擊方角色登場留言 ─────────────────────────────────────────
const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles')); const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'));
const attackers = attackersOf(roles); const attackers = attackersOf(roles);
const defenders = defendersOf(roles); const defenders = defendersOf(roles);
log('步驟5', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`); log('步驟5', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`);
await postComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers })); await queueOrPostComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers }));
// ── 步驟 6:每個攻擊方一個 sub agent 並行分析,合併問題列表 ──────────── // ── 步驟 6:每個攻擊方一個 sub agent 並行分析,合併問題列表 ────────────
const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows });
// ── 步驟 7:防守方角色登場留言 ───────────────────────────────────────── // ── 步驟 7:防守方角色登場留言 ─────────────────────────────────────────
await postComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders })); await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders }));
// ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── // ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ──────────────────
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); const { kept, excluded } = findings.length === 0
? { kept: [], excluded: [] }
: await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
review.sortFindings(kept); review.sortFindings(kept);
const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded }); const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded });
@@ -316,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 流程。
@@ -335,7 +336,12 @@ async function main() {
} catch (err) { } catch (err) {
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`); log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
} }
await ensureIssueCreated(labelIds); try {
await openTrackingIssue(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 不留言)。');
@@ -346,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 });
} }
@@ -362,22 +368,23 @@ 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 postComment(templates.othersComment(others)); await queueOrPostComment(templates.othersComment(others));
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
} }
} }
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─ // ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → ensureIssueCreated),此處不再補掛。 // 標籤已於建立 issue 時一次帶入(見上方 selectLabels → openTrackingIssue),此處不再補掛。
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,
}), }),
@@ -386,32 +393,39 @@ 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 並 pushsuccess=無嚴重問題、failure=有嚴重問題)─────── // ── 收尾:commit 並 pushsuccess=無嚴重問題、failure=有嚴重問題)───────
// 一般模式:findingsexclusions.json;建問題模式:問題明細已在 issue 留言,只 commit exclusions.json。 // 一般模式:findingsexclusions.json;建問題模式通常只 commit exclusions.json。
// 若有嚴重問題,仍 commit findings 檔產生 [failure] 結果 commit,避免相依 API 不支援時 fail-open。
const result = severe.length === 0 ? 'success' : 'failure'; const result = severe.length === 0 ? 'success' : 'failure';
const filesToCommit = ctx.createIssue ? [] : [relativePath]; const filesToCommit = review.resultFilesToCommit({
if (exclusionsChanged) { createIssue: issueModeActive,
filesToCommit.push(path.join('.gitea', 'ai-review', 'exclusions.json')); severeCount: severe.length,
} relativePath,
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。');
} }
// 本輪「審查」一律以成功收場、不直接讓檢查失敗;有嚴重問題時已推出 [failure] 結果 commit // 需要推送結果檔卻沒成功時直接失敗;這通常代表 token 權限不足、非快轉或分支保護阻擋。
// 由它再觸發的下一輪在步驟 1 讀 commit 訊息時才回報失敗(exit 1)。如此失敗檢查落在帶有結果 if (review.shouldFailMissingResultCommit({ filesToCommit, resultCommitted })) {
// 標記的最新 head 上,與合併判定一致。(result 僅用於上方 commit 訊息的結果標記。) log('收尾', 'ERR', '審查結果檔需要 push 但未成功產生結果 commit;直接回報失敗避免缺權限時靜默通過。');
return 1;
}
// 嚴重問題已推出 [failure] 結果 commit 時,由它再觸發的下一輪在步驟 1 讀 commit 訊息回報失敗。
if (result === 'failure') { if (result === 'failure') {
log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。'); log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。');
} }
+1 -1
View File
@@ -38,7 +38,7 @@ const path = require('path');
* - owner / repo repository 拆出的擁有者與專案名缺值時為空字串 * - owner / repo repository 拆出的擁有者與專案名缺值時為空字串
* - apiBaseGitea REST API 基底網址`<serverUrl>/api/v1` * - apiBaseGitea REST API 基底網址`<serverUrl>/api/v1`
* - tokenaction input `token`INPUT_TOKEN用於 Gitea API 認證以及 push findings/exclusions * - tokenaction input `token`INPUT_TOKEN用於 Gitea API 認證以及 push findings/exclusions
* commit repo建議為能觸發 CI PAT自動 token 推送不會再觸發 CI缺值時為空字串 * commit repo建議為 repo 限定最小權限且可輪替的 token缺值時為空字串
* - modelaction input `model`INPUT_MODEL trim指定 AI 模型缺值時為空字串 * - modelaction input `model`INPUT_MODEL trim指定 AI 模型缺值時為空字串
* - createIssueaction input `create-issue`INPUT_CREATE-ISSUE是否將問題建到 * - createIssueaction input `create-issue`INPUT_CREATE-ISSUE是否將問題建到
* 存取庫的問題追蹤建問題模式trim + 小寫後與字串 'true' 嚴格比對預設 false * 存取庫的問題追蹤建問題模式trim + 小寫後與字串 'true' 嚴格比對預設 false
+82
View File
@@ -0,0 +1,82 @@
'use strict';
// AI CLI 失敗診斷與機密遮罩工具:供 review 流程記錄安全、限長的一行錯誤摘要。
// 每段診斷片段(stderr/stdout)寫入日誌的字元上限。
const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500;
/**
* 遮罩診斷文字中的機密與控制字元避免寫進 CI log 時外洩
*
* 處理順序換行與控制字元一律壓成單一空白避免注入假日誌行 遮蔽
* `Authorization` 標頭`token=``token:` 型憑證URL 內嵌帳密以及常見長金鑰
* hex`ghp_` token 樣式盡力遮罩無法窮舉所有機密格式作為輸出
* CLI 診斷片段前的防線使用 {@link agentFailureDetail}
*
* @param {*} text - 待遮罩的原始文字非字串會先以 `String()` 轉型
* @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字
*/
function redactSecrets(text) {
return String(text ?? '')
.replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ')
.replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, '$1***')
.replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***')
.replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@')
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '***')
.replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, '***')
.replace(/-----BEGIN [^-]+ PRIVATE KEY-----[\s\S]*?-----END [^-]+ PRIVATE KEY-----/g, '***')
.replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***')
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***')
.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_OUTPUT_LIMIT));
if (redacted) parts.push(`${label}${redacted}`);
}
/**
* `runAgent` 的失敗結果組出可診斷的一行摘要退出碼訊號為主原始輸出預設隱藏
*
* 安全考量AI CLI 失敗時可能在 stderrstdout 回顯提示內容環境資訊tokenPII
* 原始碼祕密這些會被長期保存並供多人讀取的 CI log 收錄因此本函式預設只輸出退出碼
* 訊號與逾時狀態只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets}
* 遮罩且去除控制字元的 stderr/stdout 片段
*
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} agentResult
* `runAgent` 的回傳物件
* @returns {string} 單行診斷摘要各段以分隔無任何資訊時回傳固定字串
*/
function agentFailureDetail(agentResult) {
const parts = [];
const err = agentResult && agentResult.error;
if (err) {
if (err.killed) parts.push('已逾時終止');
if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
else if (err.code) parts.push(`code ${err.code}`);
else if (err.signal) parts.push(`signal ${err.signal}`);
}
// 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。
if (process.env.ACTIONS_STEP_DEBUG === 'true') {
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 執行失敗(無診斷輸出)');
}
return parts.join('');
}
module.exports = {
AGENT_DIAGNOSTIC_OUTPUT_LIMIT,
redactSecrets,
agentFailureDetail,
};
+9 -9
View File
@@ -119,7 +119,7 @@ function createCommentOnIssue(ctx, issueNumber, body) {
* Gitea API 回應而定 * Gitea API 回應而定
* @throws {Error} 請求失敗 2xx由底層 `api` 丟出錯誤附 `status``data` * @throws {Error} 請求失敗 2xx由底層 `api` 丟出錯誤附 `status``data`
* @remarks 使用情境AI review 各步驟把審查摘要角色登場問題彙整等內容 * @remarks 使用情境AI review 各步驟把審查摘要角色登場問題彙整等內容
* 以一般留言形式張貼到本次 PR `main()` `postComment` 閉包即以本函式實作 * 以一般留言形式張貼到本次 PR `main()` `queueOrPostComment` 閉包即以本函式實作
*/ */
function createIssueComment(ctx, body) { function createIssueComment(ctx, body) {
return createCommentOnIssue(ctx, ctx.prNumber, body); return createCommentOnIssue(ctx, ctx.prNumber, body);
@@ -156,7 +156,7 @@ function listLabels(ctx) {
* @returns {Promise<object>} 建立成功的 issue 物件 `number``title` * @returns {Promise<object>} 建立成功的 issue 物件 `number``title`
* `html_url` 等欄位 Gitea API 回應而定 * `html_url` 等欄位 Gitea API 回應而定
* @throws {Error} 請求失敗 2xx由底層 `api` 丟出錯誤附 `status``data` * @throws {Error} 請求失敗 2xx由底層 `api` 丟出錯誤附 `status``data`
* @remarks 使用情境建問題模式input: create-issue`main()` `ensureIssueCreated` * @remarks 使用情境建問題模式input: create-issue`main()` `createIssueAndFlushBufferedComments`
* PR 標題描述為 issue 標題與本文並帶入 `review.selectLabels` 事先挑好的標籤 id * PR 標題描述為 issue 標題與本文並帶入 `review.selectLabels` 事先挑好的標籤 id
* 呼叫本函式一次建立追蹤問題的 issue連同標籤之後再把審查內容逐條留言到該 issue * 呼叫本函式一次建立追蹤問題的 issue連同標籤之後再把審查內容逐條留言到該 issue
*/ */
@@ -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 - 要被阻擋的 issuePR 編號相依方 * @param {number|string} blockedIssueNumber - 要被阻擋的 issuePR 編號相依方
* @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,
}); });
+32
View File
@@ -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,
};
+41 -17
View File
@@ -1,6 +1,8 @@
'use strict'; 'use strict';
const { execFileSync } = require('child_process'); const { execFileSync } = require('child_process');
const fs = require('fs');
const { assertSafeBranchRef } = require('./gitref');
// git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。 // git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。
@@ -63,6 +65,15 @@ function tryGit(cwd, ...args) {
} }
} }
function isShallowRepository(cwd) {
try {
const shallowPath = gitTrim(cwd, 'rev-parse', '--git-path', 'shallow');
return fs.existsSync(shallowPath);
} catch {
return false;
}
}
/** /**
* 取得目前 HEAD 最新一筆 commit 的訊息標題commit message 第一行 * 取得目前 HEAD 最新一筆 commit 的訊息標題commit message 第一行
* *
@@ -102,6 +113,7 @@ function latestCommitSubject(cwd) {
* 避免把 base 分支後續演進誤算進 diff * 避免把 base 分支後續演進誤算進 diff
*/ */
function resolveMergeBase(cwd, baseRef) { function resolveMergeBase(cwd, baseRef) {
baseRef = assertSafeBranchRef(baseRef, 'baseRef');
const remoteBase = `origin/${baseRef}`; const remoteBase = `origin/${baseRef}`;
const diagnostics = []; const diagnostics = [];
// 執行一個 fetch 策略並記錄成敗(只記策略名與成敗,不含 git 原始輸出,避免洩漏遠端資訊)。 // 執行一個 fetch 策略並記錄成敗(只記策略名與成敗,不含 git 原始輸出,避免洩漏遠端資訊)。
@@ -133,11 +145,14 @@ function resolveMergeBase(cwd, baseRef) {
// 資料驅動的補抓策略:先以固定深度分批加深 base 與 HEAD(每步後重試 merge-base,成功即回傳); // 資料驅動的補抓策略:先以固定深度分批加深 base 與 HEAD(每步後重試 merge-base,成功即回傳);
// 只有仍失敗且為淺層 repo 時,才把成本最高的 --unshallow(下載完整歷史)當最後手段, // 只有仍失敗且為淺層 repo 時,才把成本最高的 --unshallow(下載完整歷史)當最後手段,
// 避免大型/長壽 repo 只為找共同祖先就無謂拉全史。 // 避免大型/長壽 repo 只為找共同祖先就無謂拉全史。
// 加深 HEAD 側須以「目前 HEAD 的 commit SHA」補抓——遠端符號 `HEAD` 由伺服器解析為
// 遠端預設分支(非目前 checkout 的 PR head),只加深它並不會補到 PR head 的歷史。
const headSha = gitTrim(cwd, 'rev-parse', 'HEAD');
const strategies = [ const strategies = [
['deepen base', 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`], ['deepen base', 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`],
['deepen HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD'], ['deepen PR HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', headSha],
]; ];
if (gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true') { if (isShallowRepository(cwd)) {
strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']); strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']);
} }
@@ -232,7 +247,7 @@ function fileLastUpdatedIso(cwd, file) {
* @param {string} [options.headSha] - PR head commit SHA有提供且與目前 HEAD 不同時會先 detach 到此 commit可省略falsy 時不 detach直接於目前 HEAD commit * @param {string} [options.headSha] - PR head commit SHA有提供且與目前 HEAD 不同時會先 detach 到此 commit可省略falsy 時不 detach直接於目前 HEAD commit
* @param {string} options.message - commit 訊息 * @param {string} options.message - commit 訊息
* @param {string[]} options.files - 要加入 commit 的檔案路徑清單相對 repo 根目錄全數無實際變更時不 commit回傳 false * @param {string[]} options.files - 要加入 commit 的檔案路徑清單相對 repo 根目錄全數無實際變更時不 commit回傳 false
* @param {string} options.token - 具該 repo push 權限的 Gitea access token建議為能觸發 CI PAT用於 findings commit 的認證推送 * @param {string} options.token - 具該 repo push 權限的 Gitea access token建議 repo 限定最小權限且可輪替用於 findings commit 的認證推送
* @param {string} options.serverUrl - Gitea 伺服器根網址例如 https://gitea.example.com),須為合法 URL。 * @param {string} options.serverUrl - Gitea 伺服器根網址例如 https://gitea.example.com),須為合法 URL。
* @param {string} options.repository - repo 完整名稱owner/repo 格式 serverUrl 組成 clone URL * @param {string} options.repository - repo 完整名稱owner/repo 格式 serverUrl 組成 clone URL
* @returns {boolean} true有變更且已 commit push 到來源分支false暫存區與 HEAD 無差異略過 commit/push * @returns {boolean} true有變更且已 commit push 到來源分支false暫存區與 HEAD 無差異略過 commit/push
@@ -248,6 +263,7 @@ function fileLastUpdatedIso(cwd, file) {
* 例外把命令列 token回顯到 CI log 或程序清單 * 例外把命令列 token回顯到 CI log 或程序清單
*/ */
function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, serverUrl, repository }) { function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, serverUrl, repository }) {
headRef = assertSafeBranchRef(headRef, 'headRef');
const current = gitTrim(cwd, 'rev-parse', 'HEAD'); const current = gitTrim(cwd, 'rev-parse', 'HEAD');
if (headSha && current !== headSha) { if (headSha && current !== headSha) {
git(cwd, 'checkout', '--detach', headSha); git(cwd, 'checkout', '--detach', headSha);
@@ -267,31 +283,36 @@ 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);
return true; return true;
} }
/** /**
* 以帶認證的方式推送到指定遠端認證資訊只經環境變數傳入不進命令列 argv * 以帶認證的方式推送到指定遠端認證資訊只經環境變數傳入不進命令列 argv
* *
* 認證方式等同 `https://ai-review-bot:<secret>@host/...` HTTP Basicgit 會把 * `GIT_CONFIG_*` 注入本次 HTTP Basic extraheader避免憑證出現在 argv
* URL 帳密轉成相同的 `Authorization: Basic` 標頭送出但改以 git * 同時先清空 checkout 持久化的自動 token extraheader確保本次 push 使用呼叫端 token
* `GIT_CONFIG_*` 環境變數注入 `http.<url>.extraheader`使 base64 憑證**不出現在 argv** * 推送失敗時改拋固定訊息避免原始例外帶出遠端 URL 或認證資訊
* 避免程序清單例外回顯洩漏推送目標 URL 亦不含帳密
* 推送失敗時**不重拋原始例外** 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`
* @param {string} secret - push 權限的 tokenPAT作為 Basic 認證的密碼 * @param {string} token - push 權限的 tokenPAT作為 Basic 認證的密碼
* @param {string} refspec - push refspec形如 `HEAD:refs/heads/<branch>` * @param {string} refspec - push refspec形如 `HEAD:refs/heads/<branch>`
* @param {string} serverUrl - Gitea 伺服器根網址用於定位 checkout 持久化 extraheader scope
* @returns {void} 成功即返回失敗拋出不含 URLargvtoken 的固定錯誤 * @returns {void} 成功即返回失敗拋出不含 URLargvtoken 的固定錯誤
* @throws {Error} 推送失敗時拋出固定訊息已隱藏遠端 URL 與認證資訊 * @throws {Error} 推送失敗時拋出固定訊息已隱藏遠端 URL 與認證資訊
* @remarks 本函式未匯出僅供 {@link commitAndPushFindings} 使用 * @remarks 本函式未匯出僅供 {@link commitAndPushFindings} 使用
*/ */
function pushWithCredential(cwd, remoteUrl, secret, refspec) { function pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) {
const basic = Buffer.from(`ai-review-bot:${secret}`).toString('base64'); const server = new URL(serverUrl);
const remote = new URL(remoteUrl);
if (remote.origin !== server.origin || !remote.pathname.endsWith('.git')) {
throw new Error('推送遠端 URL 與 Gitea 伺服器不相符,已停止推送。');
}
const basic = Buffer.from(`ai-review-bot:${token}`).toString('base64');
// checkout 持久化自動 token 的 scope 為 `http.<serverUrl>/.extraheader`(結尾帶斜線)。
const headerScope = `http.${server.origin}/.extraheader`;
try { try {
execFileSync('git', ['push', remoteUrl, refspec], { execFileSync('git', ['push', remoteUrl, refspec], {
cwd, cwd,
@@ -300,9 +321,12 @@ function pushWithCredential(cwd, remoteUrl, secret, refspec) {
env: { env: {
...process.env, ...process.env,
GIT_TERMINAL_PROMPT: '0', GIT_TERMINAL_PROMPT: '0',
GIT_CONFIG_COUNT: '1', // 先清空 checkout extraheader,再注入本次 PAT header。
GIT_CONFIG_KEY_0: `http.${remoteUrl}.extraheader`, GIT_CONFIG_COUNT: '2',
GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`, GIT_CONFIG_KEY_0: headerScope,
GIT_CONFIG_VALUE_0: '',
GIT_CONFIG_KEY_1: headerScope,
GIT_CONFIG_VALUE_1: `Authorization: Basic ${basic}`,
}, },
}); });
} catch { } catch {
+72 -66
View File
@@ -5,6 +5,7 @@ const path = require('path');
const { log, taipeiFromIso, taipeiNow } = require('./log'); const { log, taipeiFromIso, taipeiNow } = require('./log');
const { runAgent, extractJson } = require('./agents'); const { runAgent, extractJson } = require('./agents');
const { agentFailureDetail } = require('./diagnostics');
const templates = require('./templates'); const templates = require('./templates');
// 審查流程核心:.reviewignore 過濾、diff 整理、攻擊方找問題、防守方裁決、排序分組與舊留言處理。 // 審查流程核心:.reviewignore 過濾、diff 整理、攻擊方找問題、防守方裁決、排序分組與舊留言處理。
@@ -13,72 +14,6 @@ const templates = require('./templates');
const PER_FILE_DIFF_LIMIT = 16_000; const PER_FILE_DIFF_LIMIT = 16_000;
const TOTAL_DIFF_LIMIT = 160_000; const TOTAL_DIFF_LIMIT = 160_000;
/**
* 遮罩單行診斷文字中的機密與控制字元避免寫進 CI log 時外洩
*
* 處理順序換行與控制字元一律壓成單一空白避免注入假日誌行 遮蔽
* `Authorization` 標頭`token=``token:` 型憑證URL 內嵌帳密以及常見長金鑰
* hex`ghp_` token 樣式盡力遮罩無法窮舉所有機密格式作為輸出
* CLI 診斷片段前的防線使用 {@link agentFailureDetail}
*
* @param {*} text - 待遮罩的原始文字非字串會先以 `String()` 轉型
* @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字
* @remarks 本函式未匯出僅供模組內部使用
*/
function redactSecrets(text) {
return String(text ?? '')
.replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ')
.replace(/(authorization\s*[:=]\s*)\S+/gi, '$1***')
.replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***')
.replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@')
.replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***')
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***')
.trim();
}
/**
* `runAgent` 的失敗結果組出可診斷的一行摘要退出碼訊號為主原始輸出預設隱藏
*
* 安全考量AI CLI 失敗時可能在 stderrstdout 回顯提示內容環境資訊tokenPII
* 原始碼祕密這些會被長期保存並供多人讀取的 CI log 收錄權衡可除錯性本函式
* 於失敗時**預設**附上經 {@link redactSecrets} 遮罩且去除控制字元的 **stderr stdout**
* 片段各先截去過長輸入再取前 500 只印 exit code 幾乎無從判斷 CLI 為何失敗
* 且部分 CLI claude-code `-p` 模式將錯誤寫到 stdout 而非 stderr純函式不拋例外
*
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
* `runAgent` 的回傳物件
* @returns {string} 單行診斷摘要各段以分隔無任何資訊時回傳固定字串
* @remarks
* 使用情境{@link runAttackers}{@link runDefenders}{@link fillPurposes}{@link selectLabels}
* 判定 `!res.ok` 以本函式把失敗細節寫進 WRN log CI 記錄能看出 AI CLI 為何失敗
* 需要原始輸出診斷時 workflow 設定 secret `ACTIONS_STEP_DEBUG=true` 再重跑
* 本函式未匯出僅供模組內部使用
*/
function agentFailureDetail(res) {
const parts = [];
const err = res && res.error;
if (err) {
if (err.killed) parts.push('已逾時終止');
if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
else if (err.code) parts.push(`code ${err.code}`);
else if (err.signal) parts.push(`signal ${err.signal}`);
}
// 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描;
// 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。
const INPUT_LIMIT = 2_000;
// 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 與 stdout 片段——CLI 失敗時
// 只印 exit code 幾乎無從除錯(見 test-claude 秒失敗案例);且部分 CLI(如 claude-code 的
// -p 模式)會把錯誤寫到 stdout 而非 stderr,故兩者都輸出。redactSecrets 為盡力防線。
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT));
if (stderr) parts.push(`stderr${stderr.slice(0, 500)}`);
const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT));
if (stdout) parts.push(`stdout${stdout.slice(0, 500)}`);
if (parts.length === 0) {
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
}
return parts.join('');
}
/** /**
* 讀取工作目錄下的 `.reviewignore`解析為忽略路徑前綴清單 * 讀取工作目錄下的 `.reviewignore`解析為忽略路徑前綴清單
* *
@@ -622,6 +557,40 @@ function appendExclusions({ cwd, excluded, prNumber }) {
return true; return true;
} }
/**
* 依審查模式與結果決定收尾要提交的結果檔
*
* 一般模式永遠提交本回合 findings 建問題模式通常把問題明細留在 issue不提交
* findings例外是有嚴重問題時仍提交 findings `[failure]` 結果 commit 一定能產生
* 避免問題相依 API 不支援或設定失敗時 PR 缺少失敗檢查
*
* @param {Object} params - 解構參數
* @param {boolean} params.createIssue - 是否啟用建問題模式
* @param {number} params.severeCount - 嚴重 finding 數量
* @param {string} params.relativePath - 本回合保存的 findings repo 相對路徑
* @param {boolean} params.exclusionsChanged - exclusions.json 是否有實際異動
* @returns {string[]} 應交給 `commitFindings` repo 相對路徑清單
*/
function resultFilesToCommit({ createIssue, severeCount, relativePath, exclusionsChanged }) {
const files = createIssue ? (severeCount > 0 ? [relativePath] : []) : [relativePath];
if (exclusionsChanged) {
files.push(path.join('.gitea', 'ai-review', 'exclusions.json'));
}
return files;
}
/**
* 判斷本輪是否因結果 commit 未成功推送而必須失敗
*
* @param {Object} params - 解構參數
* @param {string[]} params.filesToCommit - 本輪預期寫回遠端分支的結果檔清單
* @param {boolean} params.resultCommitted - 結果檔是否已成功 commit push
* @returns {boolean} 需要寫回結果檔卻沒有成功推送時回傳 true
*/
function shouldFailMissingResultCommit({ filesToCommit, resultCommitted }) {
return filesToCommit.length > 0 && !resultCommitted;
}
/** /**
* 就地排序 findings 嚴重警告建議再依檔案路徑再依起始行遞增 * 就地排序 findings 嚴重警告建議再依檔案路徑再依起始行遞增
* *
@@ -875,6 +844,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 失敗時降級為一般留言逐條發布留言內補上檔案與行號位置
@@ -922,9 +925,12 @@ module.exports = {
runDefenders, runDefenders,
sortFindings, sortFindings,
appendExclusions, appendExclusions,
resultFilesToCommit,
shouldFailMissingResultCommit,
selectLabels, selectLabels,
postSevereToIssue, postSevereToIssue,
postOthersToIssue, postOthersToIssue,
resolveOldComments, resolveOldComments,
resolveOldIssueLinkComments,
postSevereComments, postSevereComments,
}; };
+6 -6
View File
@@ -302,7 +302,7 @@ function othersComment(findings) {
* @param {string} [params.prBody] - PR 描述原文nullish trim 後為空時輸出佔位文字 * @param {string} [params.prBody] - PR 描述原文nullish trim 後為空時輸出佔位文字
* @returns {string} 完整 issue 本文 Markdown 字串 MARK 隱藏標記 * @returns {string} 完整 issue 本文 Markdown 字串 MARK 隱藏標記
* @remarks * @remarks
* 使用情境建問題模式下 `main()`src/index.js `ensureIssueCreated` 建立 issue * 使用情境建問題模式下 `main()`src/index.js `createIssueAndFlushBufferedComments` 建立 issue
* 標題PR 標題本文本函式輸出呼叫 `gitea.createIssue` * 標題PR 標題本文本函式輸出呼叫 `gitea.createIssue`
* issue 讀者能從本文回溯到觸發審查的 PR再從下方留言逐條查看問題明細 * issue 讀者能從本文回溯到觸發審查的 PR再從下方留言逐條查看問題明細
*/ */
@@ -334,9 +334,9 @@ ${body || 'PR 無描述)'}
* @param {string} [finding.suggestedCode] - 建議寫法程式碼有值才輸出建議寫法區塊 * @param {string} [finding.suggestedCode] - 建議寫法程式碼有值才輸出建議寫法區塊
* @returns {string} 完整留言 Markdown 字串 MARK 隱藏標記 * @returns {string} 完整留言 Markdown 字串 MARK 隱藏標記
* @remarks * @remarks
* 使用情境建問題模式下 `review.postSevereToIssue`src/lib/review.js把每條嚴重 finding * 使用情境建問題模式下 `review.postSevereToIssue` `review.postOthersToIssue`
* 以本函式產生留言內容 `gitea.createCommentOnIssue` 發布到追蹤 issue * 把每條 finding 以本函式產生留言內容 `gitea.createCommentOnIssue`
* 作為問題明細的追蹤紀錄 * 發布到追蹤 issue 作為問題明細的追蹤紀錄
*/ */
function issueFindingComment(finding) { function issueFindingComment(finding) {
const emoji = SEVERITY_EMOJI[finding.severity] || '🔵'; const emoji = SEVERITY_EMOJI[finding.severity] || '🔵';
@@ -400,7 +400,7 @@ function nothingToReviewComment(ignoredCount) {
* 以本函式對 PR 留一則連結留言達成問題關聯回 PRissue 內文另以 * 以本函式對 PR 留一則連結留言達成問題關聯回 PRissue 內文另以
* {@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,
}; };
+73
View File
@@ -0,0 +1,73 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const gitea = require('../src/lib/gitea');
function withFetchStub(handler, callback) {
const originalFetch = global.fetch;
const calls = [];
global.fetch = async (url, options = {}) => {
calls.push({ url, options });
return handler(url, options);
};
return Promise.resolve()
.then(() => callback(calls))
.finally(() => {
global.fetch = originalFetch;
});
}
function jsonResponse(data, ok = true, status = 200) {
return {
ok,
status,
async text() {
return JSON.stringify(data);
},
};
}
test('addIssueDependency 使用正確 endpoint、method 與 IssueMeta body', async () => {
const ctx = {
apiBase: 'https://gitea.example.test/api/v1',
token: 'hidden',
owner: 'owner',
repo: 'repo',
};
await withFetchStub(() => jsonResponse({ ok: true }), async (calls) => {
await gitea.addIssueDependency(ctx, 12, 34);
assert.equal(calls.length, 1);
assert.equal(calls[0].url, 'https://gitea.example.test/api/v1/repos/owner/repo/issues/12/dependencies');
assert.equal(calls[0].options.method, 'POST');
assert.equal(calls[0].options.headers.Authorization, 'token hidden');
assert.deepEqual(JSON.parse(calls[0].options.body), {
index: 34,
owner: 'owner',
repo: 'repo',
});
});
});
test('createIssue 空 labels 不送出 labels 欄位', async () => {
const ctx = {
apiBase: 'https://gitea.example.test/api/v1',
token: 'hidden',
owner: 'owner',
repo: 'repo',
};
await withFetchStub(() => jsonResponse({ number: 5 }), async (calls) => {
await gitea.createIssue(ctx, { title: 'title', body: 'body', labels: [] });
assert.equal(calls[0].url, 'https://gitea.example.test/api/v1/repos/owner/repo/issues');
assert.equal(calls[0].options.method, 'POST');
assert.deepEqual(JSON.parse(calls[0].options.body), {
title: 'title',
body: 'body',
});
});
});
+52
View File
@@ -0,0 +1,52 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const gitrepo = require('../src/lib/gitrepo');
const gitref = require('../src/lib/gitref');
test('assertSafeBranchRef 接受一般分支名稱', () => {
assert.equal(gitref.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123');
});
test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => {
assert.throws(
() => gitref.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'),
/不是安全的分支名稱/,
);
});
test('assertSafeBranchRef 拒絕空白分支名稱', () => {
for (const value of ['', ' ', null, undefined]) {
assert.throws(
() => gitref.assertSafeBranchRef(value, 'baseRef'),
/baseRef 不可為空/,
);
}
});
test('assertSafeBranchRef 拒絕不安全的 refspec 分支名稱', () => {
for (const value of ['/feature', 'feature/', 'feature\\x', 'feature..x']) {
assert.throws(
() => gitref.assertSafeBranchRef(value, 'baseRef'),
/不是安全的分支名稱/,
);
}
});
test('assertSafeBranchRef 拒絕 git 不合法的分支名稱', () => {
for (const value of ['feature.lock', 'feature@{x}', 'feature~x', 'feature^x']) {
assert.throws(
() => gitref.assertSafeBranchRef(value, 'baseRef'),
/不是合法的 git 分支名稱/,
);
}
});
test('resolveMergeBase 會在 git fetch 前拒絕不安全 baseRef', () => {
assert.throws(
() => gitrepo.resolveMergeBase(process.cwd(), '../../hooks/pre-push'),
/不是安全的分支名稱/,
);
});
+179
View File
@@ -0,0 +1,179 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const review = require('../src/lib/review');
const diagnostics = require('../src/lib/diagnostics');
test('agentFailureDetail 預設不輸出 stderr/stdout 片段', () => {
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
delete process.env.ACTIONS_STEP_DEBUG;
try {
const detail = diagnostics.agentFailureDetail({
ok: false,
error: Object.assign(new Error('boom'), { code: 1 }),
stderr: 'token=super-secret-value',
output: 'stdout with password=hidden',
});
assert.match(detail, /exit 1/);
assert.doesNotMatch(detail, /super-secret-value|password|stdout|stderr/);
} finally {
if (oldDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG;
else process.env.ACTIONS_STEP_DEBUG = oldDebug;
}
});
test('agentFailureDetail 在 debug 模式輸出遮罩後片段', () => {
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
process.env.ACTIONS_STEP_DEBUG = 'true';
try {
const detail = diagnostics.agentFailureDetail({
ok: false,
error: Object.assign(new Error('boom'), { code: 2 }),
stderr: 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890',
output: 'token=abcdefghijklmnopqrstuvwxyz1234567890TOKEN',
});
assert.match(detail, /exit 2/);
assert.match(detail, /stderrAuthorization: \*\*\*/);
assert.match(detail, /stdouttoken=\*\*\*/);
assert.doesNotMatch(detail, /abcdefghijklmnopqrstuvwxyz/);
} finally {
if (oldDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG;
else process.env.ACTIONS_STEP_DEBUG = oldDebug;
}
});
test('agentFailureDetail 先限長再遮罩並處理常見 PII', () => {
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
process.env.ACTIONS_STEP_DEBUG = 'true';
try {
const detail = diagnostics.agentFailureDetail({
ok: false,
error: Object.assign(new Error('boom'), { code: 3 }),
stderr: `${'x'.repeat(520)} token=should-not-be-scanned`,
output: 'user@example.test eyJaaaaaaaaaa.bbbbbbbbbb.cccccccccc',
});
assert.match(detail, /exit 3/);
assert.doesNotMatch(detail, /should-not-be-scanned|user@example.test|eyJaaaaaaaaaa/);
assert.match(detail, /stdout\*\*\* \*\*\*/);
} finally {
if (oldDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG;
else process.env.ACTIONS_STEP_DEBUG = oldDebug;
}
});
test('postOthersToIssue 依序送出 issue 留言以維持排序', async () => {
const calls = [];
let active = 0;
let maxActive = 0;
const fakeGitea = {
async createCommentOnIssue(ctx, issueNumber, body) {
active += 1;
maxActive = Math.max(maxActive, active);
calls.push({ ctx, issueNumber, body });
await new Promise((resolve) => setTimeout(resolve, 20));
active -= 1;
return { id: calls.length };
},
};
await review.postOthersToIssue({
ctx: { token: 'hidden' },
gitea: fakeGitea,
issueNumber: 7,
others: [
{ severity: '警告', reviewer: 'Maya', file: 'a.js', startLine: 1, endLine: 1, problem: 'p1', suggestion: 's1' },
{ severity: '建議', reviewer: 'Bard', file: 'b.js', startLine: 2, endLine: 2, problem: 'p2', suggestion: 's2' },
],
});
assert.equal(calls.length, 2);
assert.equal(calls[0].issueNumber, 7);
assert.equal(maxActive, 1);
});
test('resultFilesToCommit 在建問題模式有嚴重問題時仍提交 findings', () => {
assert.deepEqual(
review.resultFilesToCommit({
createIssue: true,
severeCount: 1,
relativePath: '.gitea/ai-review/findings/run.json',
exclusionsChanged: false,
}),
['.gitea/ai-review/findings/run.json'],
);
});
test('resultFilesToCommit 在建問題模式無嚴重問題時只提交 exclusions 異動', () => {
assert.deepEqual(
review.resultFilesToCommit({
createIssue: true,
severeCount: 0,
relativePath: '.gitea/ai-review/findings/run.json',
exclusionsChanged: true,
}),
['.gitea/ai-review/exclusions.json'],
);
});
test('shouldFailMissingResultCommit 在結果檔需要 push 但未成功時回報失敗', () => {
assert.equal(
review.shouldFailMissingResultCommit({
filesToCommit: ['.gitea/ai-review/findings/run.json'],
resultCommitted: false,
}),
true,
);
assert.equal(
review.shouldFailMissingResultCommit({
filesToCommit: ['.gitea/ai-review/findings/run.json'],
resultCommitted: true,
}),
false,
);
assert.equal(
review.shouldFailMissingResultCommit({
filesToCommit: [],
resultCommitted: false,
}),
false,
);
});
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, /^> 〔已過時〕/);
});