24 Commits
Author SHA1 Message Date
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
JefferyandClaude Opus 4.8 deb20bb078 chore(ai-review 狀態): 寫回議題 #9 待人工處理問題至 findings
node-actions/template: CI / BUILD (pull_request) Successful in 9s
CI / TEST (Claude) (pull_request) Successful in 27s
CI / TEST (Antigravity) (pull_request) Successful in 51s
CI / TEST (Codex) (pull_request) Successful in 3m4s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 17:40:50 +08:00
JefferyandClaude Opus 4.8 c63b1a6da2 perf(resolveMergeBase): 先 deepen base/HEAD,--unshallow 降為最後手段
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 17:40:50 +08:00
19 changed files with 1821 additions and 292 deletions
+847
View File
@@ -944,5 +944,852 @@
"endLine": 84, "endLine": 84,
"problem": "`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 CI log,並依賴 `redactSecrets` 遮罩機密;但新增的遮罩規則與截斷規則沒有測試。這是典型失敗路徑,平常快樂路徑不會跑到,沒測過就無法相信它真的能處理 Authorization、token、URL 帳密、控制字元與長金鑰。", "problem": "`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 CI log,並依賴 `redactSecrets` 遮罩機密;但新增的遮罩規則與截斷規則沒有測試。這是典型失敗路徑,平常快樂路徑不會跑到,沒測過就無法相信它真的能處理 Authorization、token、URL 帳密、控制字元與長金鑰。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 `redactSecrets``agentFailureDetail` 對 Authorization、token、URL 帳密、控制字元、長輸出與空輸出等邊界缺少測試。" "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 `redactSecrets``agentFailureDetail` 對 Authorization、token、URL 帳密、控制字元、長輸出與空輸出等邊界缺少測試。"
},
{
"addedAt": "2026/07/20 17:43:56",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 58,
"endLine": 72,
"problem": "`agentFailureDetail` 現在預設把 AI CLI 的 `stderr` 與 `stdout` 片段寫進 CI log。攻擊者可以在 PR diff 或提示注入內容中放入敏感資料形狀的字串,再誘導 CLI 失敗並回顯 prompt`redactSecrets` 只是盡力遮罩,擋不住短密碼、內部 URL、email、客戶資料或非典型 token。這等於把不可信輸入與可能含機密的工具輸出灌進長期保存、多人可讀的 log。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI stderrstdout 寫入 CI log、黑名單式遮罩可繞過,以及機密或個資外洩風險提出相同問題。"
},
{
"addedAt": "2026/07/20 17:43:56",
"prNumber": 6,
"reviewer": "Bard",
"severity": "警告",
"file": "src/index.js",
"startLine": 119,
"endLine": 151,
"problem": "流程說明把「步驟 2」描述成延後到步驟 8 之後才執行,後面程式又用 `步驟 2(延後執行)` 回頭標示。這段樂譜的拍號倒著走,讀者必須在時間順序與編號順序之間來回換算,註解、log 與 README 流程圖都因此變得不直覺。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出流程步驟編號硬編碼於主流程註解、日誌與 README,調整流程時容易失準;本條是同一類步驟編號與執行順序不直覺問題。"
},
{
"addedAt": "2026/07/20 17:43:56",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 186,
"endLine": 224,
"problem": "`postComment` 與 `ensureIssueCreated` 是 `main()` 內部閉包,卻各自塞入完整 JSDoc,再加上前後多段長註解,使主流程像在旋律中突然插入大段腳註。這會稀釋真正重要的 10 步驟編排,可讀性變得厚重。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 main() 內新增兩個帶完整 JSDoc 的閉包與長段註解,打斷主流程閱讀節奏。"
},
{
"addedAt": "2026/07/20 17:43:56",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 390,
"problem": "`main()` 這次把「一般模式 PR 留言」、「建問題模式暫存留言」、「建立 issue」、「回貼 PR 連結」、「設定 issue dependency」都塞進同一段流程與閉包狀態(`issueBuffer`、`issue`、`postComment`、`ensureIssueCreated`)。半年後要改留言落點或新增第三種輸出模式時,維護者必須同時理解整條 10 步驟流程與這些隱含狀態轉移,出錯點會集中在同一個長函式裡。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 生命週期、緩衝佇列與發布職責耦合,並提出相同的發布器抽離方向。"
},
{
"addedAt": "2026/07/20 17:43:56",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 405,
"endLine": 426,
"problem": "在 `create-issue: true` 且有嚴重問題、但 `exclusionsChanged === false` 的情境下,流程不會產生任何結果 commit`filesToCommit` 會是空陣列,接著進入略過 commit/push 的分支,最後仍固定 `return 0`。最小重現:PR 產生 1 條保留的「嚴重」 finding、防守方沒有排除項目,因此 `exclusions.json` 不變;若問題相依 API 未啟用或設定失敗,程式只記 WRN,沒有 failure commit 觸發下一輪步驟 1,也沒有非 0 exit code,CI 會通過。這和註解宣稱「嚴重問題由 `[failure]` 結果 commit 於下一輪回報」的契約衝突。",
"reason": "Paladin:可排除(重複)。此條與 F001 指涉同一建問題模式 fail-open 問題:嚴重 finding 可能因沒有 failure commit 且 issue dependency 失敗被降級而讓 CI 通過。"
},
{
"addedAt": "2026/07/20 17:43:56",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 102,
"endLine": 155,
"problem": "新增的 `resolveMergeBase` 淺層 checkout 補抓流程沒有看到對應測試驗證。這段現在有多個分支:初次 `merge-base` 成功、`deepen base` 後成功、`deepen HEAD` 後成功、淺層 repo 才跑 `--unshallow`、所有策略失敗時要帶診斷與 `cause`。這些都是會直接影響送審 diff 範圍的核心行為,沒被試煉過我會先當作未完成。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 resolveMergeBase 的多階段 fetch、淺層與非淺層分支、停止條件及最終失敗診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 17:43:56",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 265,
"endLine": 298,
"problem": "push 行為從「先 origin、失敗再帶 token URL」改成「一律透過 `pushWithCredential` 用 `GIT_CONFIG_*` extraheader 推送」,但 diff 沒有新增測試驗證成功路徑、失敗路徑與機密不進 argv。這段一旦組錯環境變數或 refspec,結果 commit 就推不上去;一旦錯誤訊息回顯 argv,也會破壞你想保護 token 的保證。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 findings 推送認證路徑、token push 目標、錯誤不含憑證與相關測試缺口。"
},
{
"addedAt": "2026/07/20 17:43:56",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 184,
"endLine": 397,
"problem": "建問題模式新增了大量流程分支,但沒有看到對應測試驗證留言去向與邊界。現在行為包含:issue 建立前先暫存工具/diff/角色留言、沒有保留問題時靜默通過、有保留問題才建 issue、嚴重問題才設定 PR 相依、只有警告/建議時不阻擋合併、最後回貼 issue 連結到 PR。這些不是單純註解調整,是使用者可觀察到的流程改變,若沒測試很容易在重構時漏發或發錯位置。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、無 finding 靜默通過、有 finding 才建 issue、嚴重與其他問題分流、PR 回貼及相依 API 分支缺少測試。"
},
{
"addedAt": "2026/07/20 17:43:56",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 16,
"endLine": 72,
"problem": "`redactSecrets` / `agentFailureDetail` 新增了把 AI CLI 的 stderr/stdout 寫進 CI log 的行為,但沒有看到測試驗證遮罩規則與截斷邊界。這裡的失敗路徑特別需要測:Authorization、token/password、URL 內嵌帳密、長 token、控制字元與超長輸出都應被處理,否則一個未覆蓋的格式就可能把敏感內容留下來。",
"reason": "Paladin:可排除(重複)。歷史 finding 已記錄 redactSecretsagentFailureDetail 對 Authorization、token、URL 帳密、控制字元、截斷與 debug 開關等邊界缺少測試。"
},
{
"addedAt": "2026/07/20 17:43:56",
"prNumber": 6,
"reviewer": "Maya",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 172,
"endLine": 194,
"problem": "新增的 `addIssueDependency` API wrapper 沒有看到測試確認 endpoint 與 body。這段雖然薄,但語意很容易寫反:URL 上的是被阻擋的 PR/issuebody 的 `index` 才是 dependency。少了測試,未來調整時很可能把相依方向顛倒而不自知。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload、錯誤傳遞與相依方向測試,與本條相同。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 65,
"endLine": 78,
"problem": "AI CLI 失敗時預設把 stderr/stdout 片段寫進 CI log。攻擊者可以讓 CLI 失敗並誘使它回顯 prompt、diff、環境診斷或原始碼內容;`redactSecrets` 只是盡力遮罩,擋不住 PII、內部路徑、專案機密格式、JWT/雲端金鑰變體,或模型輸出的敏感片段。CI log 通常被長期保存且可被多人讀取,這條路徑會把不該外洩的內容變成可搜尋的紀錄。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI stderr/stdout 寫入 CI log、黑名單式遮罩可繞過,以及敏感內容外洩提出相同問題。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "action.yml",
"startLine": 2,
"endLine": 3,
"problem": "檔頭的「更新時間」仍停在 2026/07/17 18:49:58,但本次變更脈絡顯示檔案已在 2026/07/20 更新;這種手動時間戳與實際修改節奏不同拍,會讓讀者懷疑哪一份資訊才可信。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 action.yml 檔頭手動更新時間與實際更新時間不一致,且建議移除或改由 git history 表達。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "readme.md",
"startLine": 3,
"endLine": 3,
"problem": "README 的更新時間被改成 2026/07/17 18:49:58,卻與本次 2026/07/20 的文件變更不一致;文件首頁第一眼就走調,會削弱後續內容的可信度。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 README 手動更新時間與實際更新時間不符,且與其他檔案重複保存易過期資訊。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 7,
"endLine": 7,
"problem": "啟動橫幅的更新時間仍是 2026/07/17 18:49:58,但本檔本次已有大量流程調整;執行 log 會唱出過期的日期,維運者讀 log 時容易被誤導。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 src/index.js 啟動橫幅硬編碼更新時間與程式實際更新時間不一致。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Bard",
"severity": "警告",
"file": "src/index.js",
"startLine": 183,
"endLine": 237,
"problem": "`main()` 內新增 `postComment`、`ensureIssueCreated` 兩個閉包,還各自塞入完整 JSDoc;主流程本來應像總譜一樣清楚推進,現在在步驟前奏就被大量細節註解打斷,閱讀節奏明顯變重。",
"reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 main() 內新增閉包、JSDoc 與建問題模式細節打斷主流程可讀性的問題。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 407,
"problem": "`main()` 這次同時吃下「一般模式留言」、「建問題模式暫存/建 issue/貼回 PR/設定相依」、「舊留言延後解決」與「結果 commit」等多條流程。半年後要改其中一種模式時,維護者必須在同一個長函式裡追蹤 `issue`、`issueBuffer`、`currentRunCommentIds`、`kept/severe/others` 的狀態轉換,任何插入步驟都很容易破壞另一個模式。",
"reason": "Paladin:可排除(重複)。已知排除事項與歷史 findings 已涵蓋 main() 承擔一般模式、建問題模式、issue 狀態、緩衝佇列與發布職責耦合的問題。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 188,
"endLine": 216,
"problem": "`postComment` 以同一個函式名稱包了三種行為:PR 直接留言、issue 直接留言、issue 尚未建立時暫存且回傳 `null`。這個回傳型別與副作用都依 `ctx.createIssue``issue` 閉包狀態改變,未來新增呼叫點時很容易誤以為一定會真的發出留言或一定會拿到留言物件。",
"reason": "Paladin:可排除(重複)。此條仍屬 main() 中留言路由、issue 狀態與緩衝語意集中在閉包狀態的同一設計問題,已由歷史 findings 涵蓋。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 405,
"endLine": 419,
"problem": "在 `create-issue: true` 且本輪有嚴重問題、但 `exclusions.json` 沒有變更時,`filesToCommit` 會是空陣列,因此不會推出帶 `[failure]` 的結果 commit。接著第 419 行仍固定 `return 0`。最小情境:PR 產生 1 條嚴重 finding、沒有任何 excluded finding、Gitea 未啟用 issue dependency 或 `addIssueDependency` 失敗;流程只建立 issue 並記 WRN,CI check 卻成功結束,也沒有下一輪可讀取 `[failure]` commit,嚴重問題不會阻擋合併。",
"reason": "Paladin:可排除(重複)。與 F001 指涉同一個建問題模式嚴重 finding 在 dependency 失敗且無 failure marker 時 fail-open 的問題。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 321,
"endLine": 395,
"problem": "建問題模式的核心流程被大幅改寫,但 diff 沒看到對應測試驗證幾個分支:沒有保留問題時不建 issue、不在 PR 留言;只有警告/建議時建立 issue 但不加 dependency;有嚴重問題時建立 issue、回貼 PR 連結並嘗試加 dependency。這些都是會影響 PR 合併與通知位置的新行為,沒有測試就很容易在後續調整時悄悄退化。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式核心分支缺少測試,包括無保留問題、嚴重與非嚴重分流、PR 回貼與相依 API 失敗降級。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 104,
"endLine": 148,
"problem": "`resolveMergeBase` 新增多段 fetchdeepenunshallow 補救流程與診斷錯誤,但沒有看到測試驗證淺層 checkout、fetch 失敗後改走下一策略、某策略成功後立即停止,以及全部失敗時錯誤訊息含診斷。這裡直接決定送審 diff 的基準,邊界沒驗證就可能把錯誤檔案送審或漏審。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層路徑、停止條件及最終診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 307,
"endLine": 333,
"problem": "`pushWithCredential` 新增以 `GIT_CONFIG_*` 注入 Basic header、先清空 checkout 自動 token、失敗時隱藏 URL/憑證的行為,但 diff 沒看到測試覆蓋成功與失敗路徑。這段是結果 commit 能否觸發下一輪 CI 的關鍵,也承擔避免 token 出現在 argv/錯誤訊息的保護,沒有測試很難防回歸。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushWithCredentialpushToken 推送認證路徑、環境變數注入、成功與失敗路徑及憑證遮蔽缺少測試。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 72,
"problem": "`redactSecrets``agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 CI log,但遮罩規則與截斷策略沒有測試。這是失敗路徑才會觸發的行為,若正規表示式或輸出拼接被改壞,可能不是漏診斷就是把敏感字串帶進 log。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 redactSecretsagentFailureDetail 對 stderr/stdout、遮罩、控制字元、截斷與敏感字串缺少測試的問題。"
},
{
"addedAt": "2026/07/20 17:54:00",
"prNumber": 6,
"reviewer": "Maya",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 169,
"endLine": 175,
"problem": "新增 `addIssueDependency` API wrapper,但沒有看到測試固定 endpoint 與 payload。這種薄封裝看似簡單,卻很容易在 issue/PR 編號、`index` 欄位或 repo owner/name 組合上出錯。",
"reason": "Paladin:可排除(重複)。歷史 findings 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向測試,與本條相同。"
},
{
"addedAt": "2026/07/20 18:00:02",
"prNumber": 6,
"reviewer": "🔮 Mage",
"severity": "嚴重",
"file": "src/lib/gitrepo.js",
"startLine": 276,
"endLine": 276,
"problem": "提供 pushToken 時仍把 Git HTTP 使用者名稱固定為 ai-review-bot;若 PAT 屬於其他帳號,伺服器會以錯誤的帳號/PAT 組合驗證,導致 push 失敗。",
"reason": "誤報(管理員於 issue #10 留言明確指示列為誤報)。Gitea 的 HTTP Basic 認證以密碼欄(token/PAT)判定身分,使用者名稱欄不影響認證結果,故固定為 ai-review-bot 不會造成 push 失敗。"
},
{
"addedAt": "2026/07/20 18:07:26",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 73,
"endLine": 76,
"problem": "攻擊者可以把惡意內容塞進 PR diff,誘導 AI CLI 在失敗時把環境資訊、原始提示、程式碼片段或秘密印到 stdout/stderr;這裡預設把 stderr/stdout 片段寫進 CI log。`redactSecrets` 只是盡力遮罩,擋不住短 token、雲端 access key、JWT 片段、email、PR 內容中的 PII,等於把不可信子程序輸出變成長期保存且多人可讀的洩漏面。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI 失敗時將 stderr/stdout 寫入 CI log、黑名單式遮罩不足,以及機密或個資外洩風險提出相同問題。"
},
{
"addedAt": "2026/07/20 18:07:26",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 199,
"endLine": 237,
"problem": "`main()` 內新增兩個帶完整 JSDoc 的閉包,篇幅與抽象程度已不像局部小工具;主流程原本應像總譜一樣清楚推進,現在在步驟開始前先插入大段支線說明,閱讀節奏被拉長。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 `main()` 內新增兩個帶完整 JSDoc 的閉包,讓留言路由與 issue 建立細節打斷主流程閱讀。"
},
{
"addedAt": "2026/07/20 18:07:26",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "action.yml",
"startLine": 3,
"endLine": 4,
"problem": "檔頭的用途句過長,且「更新時間」仍停在 `2026/07/17 18:49:58`,與本次檔案變更時間脈絡不一致。文件開場若時間與篇幅都失準,後續讀者很難相信這份註解仍被細心維護。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 `action.yml` 檔頭手寫更新時間過期、易與實際變更不同步提出相同問題。"
},
{
"addedAt": "2026/07/20 18:07:26",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 223,
"problem": "`main()` 現在同時負責審查流程編排、PR 留言、issue 暫存、issue 建立與暫存留言 flush。這些模式差異靠 `ctx.createIssue`、`issueBuffer`、`issue` 這幾個閉包狀態散在後續流程判斷;半年後要新增第三種輸出目的地或調整步驟順序時,很容易漏改某個分支,造成留言發錯位置或暫存內容被丟棄。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 `main()` 內留言路由、issue 狀態、緩衝佇列與發布職責耦合的問題。"
},
{
"addedAt": "2026/07/20 18:07:26",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 309,
"endLine": 388,
"problem": "建問題模式的生命週期被拆散在多個區塊:先依 `kept.length` 建 issue、再分別處理 severe/others、最後回貼 PR 連結與設定 dependency。這些區塊都隱含「只要有 severe 或 others`issue` 一定存在」的前置條件,但前置條件沒有被型別或函式邊界保護,只靠讀者追完整個流程才能確認。後續若有人改了 `kept` 分組、靜默通過規則或 issue 建立條件,這段會很容易產生 null issue 或部分內容漏發。",
"reason": "Paladin:可排除(重複)。此條仍指向建問題模式的 issue 生命週期與跨區塊可變狀態耦合,與 F007 及歷史 Leo findings 所述同一設計問題重複。"
},
{
"addedAt": "2026/07/20 18:07:26",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 183,
"endLine": 389,
"problem": "建問題模式新增了完整分流行為,但這次變更沒有看到對應測試驗證。這裡不只是換留言目的地,而是新增「先暫存留言、確定有保留問題才建 issue、無保留問題靜默通過、嚴重問題才掛 PR 相依、PR 回貼 issue 連結」等多個分支;若其中任一條件判斷錯,可能造成 PR 沒有審查結果、issue 漏留言,或警告問題誤阻擋合併。",
"reason": "Paladin:可排除(重複)。歷史 Maya findings 已涵蓋建問題模式暫存留言、無保留問題靜默通過、問題分流、PR 回貼 issue 連結與 dependency 等核心分支缺少測試。"
},
{
"addedAt": "2026/07/20 18:07:26",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 102,
"endLine": 158,
"problem": "`resolveMergeBase` 新增多段 fetch fallback 與診斷彙整,但沒有測試覆蓋淺層 checkout、fetch 失敗、每次補抓後立即重試 merge-base、以及最終失敗時錯誤訊息與 `cause` 的行為。這段是 PR diff 基準的核心邏輯,未驗證時很容易在 shallow clone 或 PR head 歷史不足時漏審/誤審。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 `resolveMergeBase` 多階段 fetch、淺層與非淺層路徑、fetch 失敗後降級、停止條件與最終診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 18:07:26",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 268,
"endLine": 325,
"problem": "`commitAndPushFindings` 改成一律透過 `pushWithCredential` 用 PAT extraheader 推送,且宣稱會清掉 checkout 自動 token、避免 token 進 argv、失敗時不外洩 URL/憑證;但這些安全與觸發 CI 的關鍵行為沒有測試驗證。未測的失敗路徑尤其危險,因為一旦環境變數組錯,可能推送失敗或回到自動 token 而不觸發下一輪檢查。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 `commitAndPushFindings``pushWithCredential` 的推送認證路徑、token 不進 argv、錯誤不外洩憑證與 CI 觸發相關行為缺少測試。"
},
{
"addedAt": "2026/07/20 18:07:26",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 74,
"problem": "`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 CI log,並依賴 `redactSecrets` 遮罩機密與控制字元;這是新增的失敗診斷行為,但沒有看到測試驗證邊界與失敗輸出。若遮罩規則漏掉,測試沒守住就可能把 token、Authorization header 或含換行的偽造 log 直接輸出。",
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄 `agentFailureDetail``redactSecrets` 對 Authorization、token、URL 帳密、控制字元、截斷與空輸出等邊界缺少測試。"
},
{
"addedAt": "2026/07/20 18:07:26",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitea.js",
"startLine": 169,
"endLine": 195,
"problem": "新增 `addIssueDependency` 封裝 Gitea issue dependency endpoint,但沒有對 endpoint、HTTP method 與 body 語意補測試。這個 API 的方向性很重要:是讓 PR 相依於追蹤 issue;若 body 的 `index` 或 URL 上的 issue number 寫反,測試沒抓到就會變成錯誤的阻擋關係,甚至完全沒有阻擋效果。",
"reason": "Paladin:可排除(重複)。歷史 findings 已指出 `addIssueDependency` 缺少 endpoint、HTTP method、payload 與相依方向的契約測試,與本條指控相同。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 68,
"endLine": 75,
"problem": "AI CLI 失敗時會把 stderrstdout 片段預設寫進 CI log。攻擊者只要讓工具失敗,且讓 prompt、diff、CLI 錯誤或環境診斷中夾帶未被正規式涵蓋的秘密格式(例如 JWT、雲端憑證、私鑰片段、較短 token、含符號的密碼或 PII),就能把機密永久留在多人可讀的 workflow log。`redactSecrets` 是盡力遮罩,不足以作為洩漏邊界。",
"reason": "Paladin:可排除(重複)。歷史 findings 已指出 AI CLI 失敗時將 stderr/stdout 寫入 CI log、黑名單式遮罩可繞過,並可能洩漏機密或個資;本條是同一風險。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "警告",
"file": "action.yml",
"startLine": 22,
"endLine": 25,
"problem": "文件改成建議呼叫端傳入可觸發 CI 的 PAT。若 workflow 在 PR head 上執行此 action,或 action 程式碼可被 PR 修改,攻擊者可以把這顆 PAT 當成獵物:改寫 action、讓工具輸出、或藉由執行流程把 token 外送。自動 token 原本的防遞迴限制被 PAT 繞開後,等於把更高權限、更危險的憑證交給不可信變更。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 action.yml 建議使用可觸發 CI 的 PAT,導致不可信 PR 或可修改 action 程式碼時產生憑證外洩與濫用風險提出相同問題。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "action.yml",
"startLine": 3,
"endLine": 3,
"problem": "檔案標頭的「更新時間」仍停在 `2026/07/17 18:49:58`,但本次變更內容與提供的檔案最後更新時間已是 `2026/07/20`。這種手寫時間戳像樂譜上的舊拍號,讀者會懷疑文件到底是否同步。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 action.yml 檔頭手寫更新時間與實際版本不一致,屬同一問題。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "readme.md",
"startLine": 3,
"endLine": 3,
"problem": "README 的更新時間也停在 `2026/07/17 18:49:58`,與本次文件內容大幅改動不相稱。對讀者而言,這個欄位現在不是資訊,而是噪音。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 README 檔頭手寫更新時間與實際更新不符,屬同一問題。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 7,
"endLine": 7,
"problem": "啟動 banner 的更新時間仍是 `2026/07/17 18:49:58`,但同檔本次新增了大量流程與註解。執行時印出的版本感與實際程式節奏不一致,像開場音還停在舊調。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 src/index.js 啟動橫幅硬編碼更新時間失準,屬同一問題。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "readme.md",
"startLine": 42,
"endLine": 50,
"problem": "mermaid 圖用 `S1 -> S3 -> ... -> S8 -> S2 -> S9` 表示延後的步驟 2,節點代號與視覺流程反向交錯。雖然顯示文字能說明「步驟 2 延後」,但閱讀原始 Markdown 時節奏很拗,維護者很容易在後續增修時接錯線。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋流程步驟編號硬編碼於 README 與流程圖,造成文件與流程順序高耦合;本條只是針對 Mermaid 節點 ID 的同類表現。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 182,
"endLine": 238,
"problem": "`queueOrPostComment` 與 `createIssueAndFlushBufferedComments` 這兩段閉包夾在主流程前奏中,註解、狀態變數與模式分流交織在一起,讓 `main()` 的旋律還沒進入步驟 3 就先變成長篇宣敘。可讀性負擔集中在主流程,後面的步驟編排也因此更難掃描。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 main() 在步驟 3 前被 issueBuffer、issue 與兩個閉包函式打斷,留言路由與 issue 建立細節混入主流程,屬同一可讀性問題。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 407,
"problem": "`main()` 這次把留言路由、issue 暫存、標籤挑選、issue 建立、問題發布、PR 回貼、相依設定全部塞進同一個流程函式。半年後要改「建問題模式」其中一段時,維護者必須同時理解 `currentRunCommentIds`、`issueBuffer`、`issue` 閉包狀態與一般模式/建問題模式的分支時序,這會讓錯誤很容易藏在流程順序裡,也很難針對單一行為做單元測試。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列、標籤、相依關係與發布職責耦合的問題。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 32,
"endLine": 48,
"problem": "`agentFailureDetail` 的文件和實作描述互相打架:前面寫「原始輸出預設隱藏」,下一段又寫預設附上經遮罩的 stderr/stdout,後面還提到 `ACTIONS_STEP_DEBUG=true`,但實作沒有任何 debug 開關。這種註解漂移會讓未來維護者誤判 CI log 會不會輸出 agent 內容,進而在除錯或調整遮罩策略時做錯決策。",
"reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 agentFailureDetail 文件宣稱預設隱藏原始輸出,卻又描述預設輸出 stderr/stdout 且提到未實作的 ACTIONS_STEP_DEBUG 分支。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Leo",
"severity": "建議",
"file": "readme.md",
"startLine": 76,
"endLine": 131,
"problem": "README 的功能列表把分支名稱與原始碼行號硬編在數十個連結裡;這次只是程式碼位移就必須大面積同步改 `#Lxx`。這種文件形態維護成本很高,未來只要函式上方增減幾行,文件就會悄悄指到錯誤位置。",
"reason": "Paladin:可排除(重複)。歷史 findings 已指出 README 功能列表硬編分支名稱與行號連結,維護成本高且容易漂移。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 320,
"endLine": 386,
"problem": "建問題模式這次改成「有保留問題才建 issue」、「先暫存工具/diff/角色留言」、「嚴重問題才加相依」、「無保留問題靜默通過」,但 diff 沒看到對應測試。這些是使用者可觀察到的流程分支,尤其 `kept.length === 0`、只有警告/建議、有嚴重問題、`addIssueDependency` 失敗降級,都還沒有被驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的 issue 建立條件、暫存留言、嚴重與非嚴重分流、無 finding 靜默通過、標籤或相依 API 失敗等核心分支缺少測試。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 102,
"endLine": 147,
"problem": "`resolveMergeBase` 新增多段 fetchdeepenunshallow 重試策略與診斷錯誤,但沒有看到測試覆蓋淺層 checkout 的失敗路徑。這段邏輯若順序或 refspec 錯,會直接讓審查抓不到正確 diff 基準。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、deepen、unshallow、停止條件與最終失敗診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 310,
"endLine": 340,
"problem": "`pushWithCredential` 改成用 `GIT_CONFIG_*` 注入 PAT、先清掉 checkout 的 extraheader,且失敗時改丟固定錯誤;這是認證與 CI 觸發的關鍵行為,但缺少測試驗證環境變數、refspec 與錯誤遮蔽。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushWithCredentialcommitAndPushFindings 的 token 認證推送路徑、refspec、環境變數注入與錯誤遮蔽缺少測試。"
},
{
"addedAt": "2026/07/20 18:36:02",
"prNumber": 6,
"reviewer": "Maya",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 73,
"problem": "新增的 `redactSecrets``agentFailureDetail` 會把 AI CLI 的 stderr/stdout 寫進 CI log,雖然有遮罩與限長,但沒有測試驗證常見機密格式、控制字元與長輸出邊界是否真的被處理。",
"reason": "Paladin:可排除(重複)。歷史 findings 已指出 redactSecretsagentFailureDetail 對常見憑證格式、URL 帳密、控制字元、截斷與空輸出等邊界缺少測試。"
},
{
"addedAt": "2026/07/20 18:45:35",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/lib/review.js",
"startLine": 82,
"endLine": 85,
"problem": "攻擊者只要讓 AI CLI 失敗,就有機會把 CLI 的 stderr/stdout 片段寫進 CI log。這些輸出可能回顯送審 diff、prompt、環境診斷、token、JWT、雲端金鑰、私鑰片段或 PII;目前 `redactSecrets` 只是盡力遮罩,漏掉未列舉格式時,秘密會被長期保存在多人可讀的 workflow log。這違反「回應不得含 PII」的邊界,也把失敗路徑變成資料外洩通道。",
"reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI 失敗時將 stderr/stdout 片段寫入 CI log、黑名單式遮罩不足,以及機密或 PII 外洩風險提出相同問題。"
},
{
"addedAt": "2026/07/20 18:45:35",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "警告",
"file": "action.yml",
"startLine": 17,
"endLine": 23,
"problem": "這裡鼓勵呼叫端傳入「能觸發 CI 的 PAT」作為 action token。攻擊者若能提交 PR 並讓 workflow 在不可信程式碼上執行,就會盯上這個高權限 PAT:任何後續工具、腳本、AI CLI 或被 PR 影響的輸出路徑只要有一處外洩,就能拿到可 push、可留言、可觸發 CI 的長效憑證。自動 token 不重觸發 CI 是防遞迴與降權邊界,直接建議 PAT 等於要求使用者拆掉這道邊界。",
"reason": "Paladin:可排除(重複)。歷史 findings 已指出 action.yml 鼓勵使用可觸發 CI 的 PAT,會在不可信 PR 或受 PR 影響流程中擴大高權限憑證外洩與濫用風險。"
},
{
"addedAt": "2026/07/20 18:45:35",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 397,
"problem": "`main()` 這次把留言路由、issue 暫存與建立、標籤挑選、PR 相依設定、舊留言清理、嚴重/非嚴重發布策略全部揉進同一個流程函式。六個月後要調整其中一個落地模式時,維護者必須同時理解 `ctx.createIssue`、`issueBuffer`、`issue`、`currentRunCommentIds` 與步驟順序的隱含關係,很容易在新增一個留言點時漏掉「一般模式要記 id、建問題模式要暫存或發 issue」這類規則。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 生命週期、緩衝佇列、發布策略與審查編排耦合,並提出抽離 publisher 類邊界的相同方向。"
},
{
"addedAt": "2026/07/20 18:45:35",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 21,
"endLine": 78,
"problem": "`redactSecrets()` / `agentFailureDetail()` 新增了不少診斷輸出政策與遮罩規則,但目前是未匯出的私有函式。這段邏輯牽涉多種 token 格式、URL 帳密、控制字元與長度截斷,未來一改 regex 就可能讓 CI log 變得難除錯或遮罩失效;若只能透過攻擊方/防守方整段流程間接驗證,測試成本會很高。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 redactSecretsagentFailureDetail 缺少直接測試、遮罩與截斷邊界難以驗證,以及診斷/遮罩職責放在 review.js 造成模組邊界不清的問題。"
},
{
"addedAt": "2026/07/20 18:45:35",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 184,
"endLine": 406,
"problem": "建問題模式這次新增了多個未驗證的流程分支:留言先暫存到 issueBuffer、確定 kept.length > 0 才建 issue、無保留問題/無可審查變更時靜默通過、嚴重問題才建立 PR 對 issue 的 dependency。這些都是會影響 PR 留言、issue 建立與合併阻擋的核心行為,但 diff 沒看到對應測試;目前如果某個分支漏發、誤發到 PR,或 issue 為 null 時仍呼叫 postSevereToIssue,都不會被試煉攔下來。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、kept 為空靜默通過、有問題才建 issue、嚴重與非嚴重分流、dependency 與 PR 回貼等核心分支缺少測試。"
},
{
"addedAt": "2026/07/20 18:45:35",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 102,
"endLine": 155,
"problem": "resolveMergeBase 新增了淺層 checkout 的多段 fetch fallback、每次成功 fetch 後重試 merge-base、以及最終診斷錯誤,但沒有看到測試覆蓋這些邊界與失敗路徑。這段行為依賴 git 指令順序;只要 deepen base 成功後沒有立刻回傳、HEAD 補抓用錯 ref、或全部失敗時沒有保留診斷,PR diff 基準就可能錯或完全中斷,現在沒有測試能驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 的多階段 fetch、shallow/非 shallow 分支、成功停止條件、降級策略與最終診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/20 18:45:35",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 270,
"endLine": 330,
"problem": "pushWithCredential 改成一律用 GIT_CONFIG_* 注入 Basic extraheader,並刻意清掉 checkout 持久化的自動 token;這是高風險失敗路徑與防洩漏行為,但 diff 沒有測試確認 token 不會出現在 argv、失敗錯誤不含遠端 URL/token、以及 extraheader 的兩筆設定順序正確。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 commitAndPushFindingspushWithCredential 的認證推送策略、GIT_CONFIG 注入、token 遮蔽與失敗路徑缺少測試。"
},
{
"addedAt": "2026/07/20 18:45:35",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 75,
"problem": "agentFailureDetail/redactSecrets 新增了把 AI CLI stderr/stdout 寫入 CI log 的行為,且依賴遮罩、控制字元清理與長度截斷來避免外洩;但沒有看到測試驗證 token、Authorization、URL 內嵌帳密、長字串與換行控制字元都會被處理。這種失敗診斷若測試只看快樂路徑,很容易在某種輸出格式下把秘密寫進 log。",
"reason": "Paladin:可排除(重複)。歷史 findings 已逐項記錄 agentFailureDetailredactSecrets 對 Authorization、token、URL 帳密、控制字元、截斷與 debug 開關缺少測試。"
},
{
"addedAt": "2026/07/20 18:45:35",
"prNumber": 6,
"reviewer": "Maya",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 168,
"endLine": 194,
"problem": "addIssueDependency 新增了 Gitea endpoint 包裝,但沒有測試確認路徑與 body 的語義:URL 上的 issueNumber 是被阻擋的 PR/issuebody.index 才是 dependency。這個方向如果寫反,流程仍可能收到 2xx 或難以從單次手動測試看出錯誤。",
"reason": "Paladin:可排除(重複)。歷史 findings 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向測試,與本條指控相同。"
},
{
"addedAt": "2026/07/20 18:45:35",
"prNumber": 6,
"reviewer": "Rogue",
"severity": "警告",
"file": "src/index.js",
"startLine": 365,
"endLine": 370,
"problem": "建問題模式下把 `others` 交給 `review.postOthersToIssue` 逐條發 issue 留言,這是在拿遠端 API latency 燒時間。警告/建議通常可能比嚴重問題多很多,若有 50 條、每次 Gitea API 往返 200ms,光留言就可能多花 10 秒以上,還會增加 rate limit 壓力。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出建問題模式將警告/建議逐條發成 issue 留言,會造成 O(n) 遠端 POST、增加 CI 時間與 rate limit 壓力。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "警告",
"file": "action.yml",
"startLine": 19,
"endLine": 24,
"problem": "文件鼓勵呼叫端傳入「能觸發 CI 的 PAT」。這把原本自動 token 的防遞迴與範圍限制換成長效、較高權限的秘密。若 workflow 會在不受信任的 PR 程式碼或可被 PR 修改的 action 版本上執行,攻擊者可以在 CI 中讀取或外送 PAT,接著用它 push commit、改 issue、或觸發更多 workflow。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 action.yml 建議使用可觸發 CI 的 PAT 所帶來的長效高權限憑證風險提出相同問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "action.yml",
"startLine": 2,
"endLine": 3,
"problem": "檔頭 `更新時間` 仍寫著 `2026/07/17 18:49:58`,但本次變更描述顯示此檔最後更新為 `2026/07/20 17:24:18`。文件時間戳若成了舊拍子,讀者會開始懷疑整份 manifest 的註解是否也同樣過期。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 action.yml 檔頭手動更新時間容易過期且與實際內容不同。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 7,
"endLine": 7,
"problem": "啟動 banner 的 `更新時間` 被改成 `2026/07/17 18:49:58`,但這次主流程明顯新增了建 issue、延後解決舊留言等大量段落。這個時間戳像留在舊譜上的小節號,和目前程式內容不再合拍。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 src/index.js 啟動 banner 的手動更新時間與程式實際變更不同,屬同一問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 390,
"problem": "`main()` 這次把建問題模式的暫存留言、issue 建立、標籤挑選、PR 回貼、相依設定、一般模式舊留言清理全部塞進同一個流程函式。半年後要改任何一個輸出通道時,維護者必須重新推演 `ctx.createIssue`、`issue`、`issueBuffer`、`currentRunCommentIds` 與步驟 2 延後執行之間的狀態關係,這會讓主流程變成難以安全修改的編排巨石。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。main() 內建問題模式狀態、留言路由、issue 建立與發布職責耦合,已由既有排除事項與歷史 findings 涵蓋。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 199,
"endLine": 232,
"problem": "`queueOrPostComment` 透過閉包同時讀寫 `ctx.createIssue`、`issue`、`issueBuffer`、`currentRunCommentIds`,回傳型別也會依狀態變成 `Object|null`。這種隱性狀態機短期能跑,但未來要追「這則留言到底會發到 PR、issue,還是只進 buffer」時,必須靠讀整個 `main()` 的執行順序才能理解。",
"reason": "Paladin:可排除(重複)。queueOrPostComment 的閉包狀態與留言目的地隱性分流,與既有 main() 留言路由、issue 狀態與緩衝佇列耦合問題相同。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Leo",
"severity": "建議",
"file": "readme.md",
"startLine": 76,
"endLine": 131,
"problem": "README 的功能列表繼續維護大量硬編碼到 `develop` 分支與精確行號的連結。這次 diff 已經為了行號漂移更新一整片表格;之後任何新增註解、移動函式或插入測試都會讓文件連結過期,維護成本會隨每次重構累積。",
"reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 README 大量硬編 develop 分支與精確行號連結,會隨程式碼移動失準。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 409,
"endLine": 418,
"problem": "建問題模式下若有嚴重問題但 `exclusions.json` 沒變更,`filesToCommit` 會是空陣列,流程會走到「略過 commit/push」後直接 `return 0`。最小重現:`create-issue=true`、攻擊方保留 1 條 `severity: '嚴重'`、防守方沒有排除任何 finding,因此 `exclusionsChanged=false`。此時本輪檢查成功,且沒有推出 `[ai-review-bot][failure]` commit 觸發下一輪步驟 1 回報失敗;若 issue dependency API 未啟用或設定失敗,嚴重問題不會阻擋合併。",
"reason": "Paladin:可排除(重複)。與 F001 指涉同一個建問題模式下嚴重 finding 可能未產生可阻擋失敗訊號、最後回傳成功的問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 323,
"endLine": 390,
"problem": "建問題模式的核心流程新增了多個可觀察行為,但目前測試沒有驗證:有保留問題才建立 issue、無保留問題靜默通過、暫存的工具/diff/角色留言會 flush 到 issue、PR 只回貼 issue 連結、且只有嚴重問題才呼叫 `addIssueDependency`。這些都是本次 PR 新增/改動的主要行為,沒有試煉過就等於還沒完成。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式有問題才建 issue、無問題靜默通過、暫存留言 flush、PR 回貼與嚴重問題相依設定等核心分支缺少測試。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 130,
"endLine": 190,
"problem": "`resolveMergeBase` 新增了多段 fetch 補歷史策略、每步後重試 merge-base、淺層 repo 才 `--unshallow`、以及失敗診斷彙整,但新增測試只驗證不安全 `baseRef` 會被拒絕,沒有覆蓋這些成功與失敗路徑。這裡最怕的是淺層 checkout 或 PR head 歷史不足時,實際 CI 才發現 fetch 順序或 refspec 錯了。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 的多階段 fetch、淺層/非淺層分支、停止條件、降級與最終失敗診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 303,
"endLine": 358,
"problem": "推送結果 commit 的認證方式改成 `pushWithCredential`:不走 origin、自訂 `GIT_CONFIG_*` extraheader、先清掉 checkout token、URL origin 必須相符、失敗時隱藏遠端與 token。這些安全與 CI 觸發相關行為目前完全沒有測試;現有 `gitrepo.test.js` 只測分支名稱驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄 commitAndPushFindingspushWithCredential 的 token 認證推送路徑、推送目標、錯誤遮蔽與安全行為缺少測試。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Assassin",
"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 的人。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出除錯診斷輸出中 Authorization 遮罩規則可能只遮到 scheme、留下後方憑證,並造成 CI log 洩漏,與本條同一問題。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 181,
"endLine": 239,
"problem": "`main()` 這次把「PR 留言」、「issue 暫存」、「issue 建立後 flush」三種狀態直接塞進閉包變數 `currentRunCommentIds`、`issueBuffer`、`issue` 與 `queueOrPostComment()`。未來只要再新增一種落地方式或調整留言順序,維護者必須同時理解整個主流程的時序,否則很容易出現留言漏發、舊留言誤標、或 issue 尚未建立卻被使用的狀態錯誤。這段已經不是單純編排,而是隱含一個留言發布狀態機。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列與發布狀態機耦合的同一問題。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "readme.md",
"startLine": 56,
"endLine": 132,
"problem": "README 這次把大量連結從 `branch/master` 改成 `branch/develop`,同時保留許多硬編碼行號。這種文件會隨分支名稱、預設分支、或任一檔案插入幾行就整批失準;未來維護者每次調整程式都得同步修幾十個 URL,否則文件看起來精準、實際上卻導到錯誤位置。",
"reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 README 大量硬編 branch 與行號深連結,造成文件與原始碼高同步成本;本條為同一問題。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Mage",
"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 與狀態不一致。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。issue 建立後 flush 暫存留言失敗、重跑又建立重複 issue,正是已知排除事項與多筆歷史 findings 已記錄的問題。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 241,
"problem": "建問題模式新增了「先暫存留言、確定有保留問題才建立 issue、再 flush 暫存留言」的流程,但目前新增測試只驗到低階 helper,沒有驗證主流程在 create-issue=true 時的留言去向與靜默通過行為。這裡若順序錯了,可能會把原本應該留在 issue 的工具/diff/角色留言發到 PR,或在無保留問題時留下不該出現的 PR 留言。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 create-issue 模式中留言先暫存、issue 建立後依序寫入、無 finding 靜默通過與一般模式留言目的地缺少主流程測試。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 354,
"endLine": 395,
"problem": "新增的建問題模式會依嚴重度分流:嚴重問題改發到 issue、PR 回貼 issue 連結,且只有嚴重問題才呼叫 `addIssueDependency`。目前測試沒有覆蓋這個決策矩陣,等於沒有驗證「警告/建議不阻擋合併」與「嚴重問題要建立相依」這兩個關鍵行為。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的嚴重/非嚴重分流、PR 回貼、相依 API 降級與核心分支缺少測試;本條屬同一測試缺口。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 130,
"endLine": 188,
"problem": "`resolveMergeBase` 新增了多段 fetch 補歷史策略、每次補抓後重試 merge-base、以及淺層 repo 才執行 `--unshallow` 的行為,但測試只驗證不安全 baseRef 會被拒絕,沒有驗證這些成功/失敗路徑。這段是 diff 基準的核心,一旦 fallback 順序或條件壞掉,review 可能拿錯範圍或在 shallow checkout 直接失敗。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、shallow 與非 shallow 分支、停止條件、降級與最終失敗診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 331,
"endLine": 366,
"problem": "`pushWithCredential` 是這次結果 commit 能否用 PAT 觸發下一輪 CI 的關鍵,但目前沒有測試驗證它真的清掉 checkout 的自動 token、再注入 PAT Authorization,也沒有測失敗時不外洩 remote URL/token。這條失敗路徑沒被試煉過,可能讓 `[failure]` 結果 commit 無法觸發快速回報,或在錯誤訊息裡洩漏認證資訊。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushWithCredentialcommitAndPushFindings 的認證推送策略、token 注入、推送目標與失敗時不外洩 token 或 URL 的測試缺口。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Maya",
"severity": "建議",
"file": "src/lib/diagnostics.js",
"startLine": 21,
"endLine": 32,
"problem": "`redactSecrets` 新增多個遮罩規則與控制字元單行化,但測試只透過 `agentFailureDetail` 間接覆蓋 Authorization 與 token 兩種格式。URL 內嵌帳密、長 token、GitHub token 樣式、控制字元注入與 null/undefined 邊界都還沒被直接驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄失敗診斷與 redactSecrets 對 Authorization、URL 帳密、token 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。"
} }
] ]
@@ -8,19 +8,6 @@
"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", "id": "F002",
"reviewer": "Maya", "reviewer": "Maya",
@@ -34,19 +21,6 @@
"suggestion": "新增主流程測試並 mock Gitea、agent 與 git 操作,至少涵蓋:`kept=[]` 時不建立 issue 且不留言;僅嚴重問題;僅警告/建議;混合問題;建立 issue 或寫入暫存留言失敗;標籤查詢、AI 選標籤及補掛標籤失敗時仍回貼 issue 連結。除了呼叫次數,也應斷言 API 呼叫順序、目標 issue 編號及留言內容。", "suggestion": "新增主流程測試並 mock Gitea、agent 與 git 操作,至少涵蓋:`kept=[]` 時不建立 issue 且不留言;僅嚴重問題;僅警告/建議;混合問題;建立 issue 或寫入暫存留言失敗;標籤查詢、AI 選標籤及補掛標籤失敗時仍回貼 issue 連結。除了呼叫次數,也應斷言 API 呼叫順序、目標 issue 編號及留言內容。",
"suggestedCode": "" "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", "id": "F004",
"reviewer": "Maya", "reviewer": "Maya",
@@ -59,19 +33,6 @@
"problem": "`resolveMergeBase` 新增多階段 fetch 與淺層 checkout 修復邏輯,但沒有看到測試驗證成功、降級與最終失敗路徑。尤其初次 merge-base 失敗後,淺層與非淺層 repository 會走不同路徑,且多個 `tryGit` 失敗會被刻意吞掉;若參數、refspec 或重試順序有誤,只會在實際 CI checkout 深度不足時才暴露。", "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 與執行順序。", "suggestion": "以 stub 的 git 執行器或暫存 repository 補齊案例:首次 merge-base 成功;淺層 repository 經 `--unshallow` 後成功;`--unshallow` 失敗但 `--deepen=1000` 後成功;非淺層首次失敗後重試成功;所有策略失敗時拋出含 `baseRef` 且保留原始 `cause` 的錯誤。並斷言 base/head fetch 的 refspec 與執行順序。",
"suggestedCode": "" "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": []
@@ -8,32 +8,6 @@
"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", "id": "F003",
"reviewer": "Maya", "reviewer": "Maya",
@@ -60,19 +34,6 @@
"suggestion": "以參數化測試覆蓋 findings 四種組合,斷言 API 呼叫順序/次數/issue number/統計;並分別讓 listLabelsselectLabelscreateIssue/問題留言/PR 連結留言/addIssueDependency 拋錯,驗證哪些中止、哪些僅記警告續行;零 findings 時斷言所有寫入 API 皆不呼叫。屬測試架構決策。", "suggestion": "以參數化測試覆蓋 findings 四種組合,斷言 API 呼叫順序/次數/issue number/統計;並分別讓 listLabelsselectLabelscreateIssue/問題留言/PR 連結留言/addIssueDependency 拋錯,驗證哪些中止、哪些僅記警告續行;零 findings 時斷言所有寫入 API 皆不呼叫。屬測試架構決策。",
"suggestedCode": "" "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", "id": "F006",
"reviewer": "Maya", "reviewer": "Maya",
@@ -98,45 +59,6 @@
"problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)", "problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)",
"suggestion": "mock git 執行器與 URLenv 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。", "suggestion": "mock git 執行器與 URLenv 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。",
"suggestedCode": "" "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": []
@@ -0,0 +1,104 @@
{
"generatedAt": "2026/07/20 17:37:54",
"commitSha": "3ef8a302911c60b9f71024e1f4b60e0d4578b8fd",
"prNumber": 6,
"tool": {
"name": "code-review-resolve",
"version": "0.0.8",
"model": "(工具預設)"
},
"findings": [
{
"id": "F001",
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 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": "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": []
}
@@ -0,0 +1,39 @@
{
"generatedAt": "2026/07/20 18:00:02",
"commitSha": "d354f30c270e99baa9dcfc6aa76510710c0193ca",
"prNumber": null,
"tool": {
"name": "code-review-resolve",
"version": "0.0.8",
"model": "(工具預設)"
},
"findings": [
{
"id": "F001",
"reviewer": "🧰 Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 257,
"endLine": 307,
"problem": "流程步驟編號同時硬編碼在 log 字串、區段註解、JSDoc、README 與多個函式庫中。插入或調整一個步驟就必須跨大量檔案全面改號,容易讓文件與實際紀錄不一致。",
"suggestion": "程式內改用穩定的語意階段名稱(如 diff、attack、defend、publish),由單一流程定義集中決定顯示順序;JSDoc 以階段名稱互相引用,README 流程圖由同一份階段資料產生或僅在文件層維護展示編號。",
"suggestedCode": "const PHASE = Object.freeze({\n RESOLVE_OLD: '清理舊留言',\n DETECT_TOOL: '偵測工具',\n COLLECT_DIFF: '整理差異',\n ATTACK: '攻擊方審查',\n DEFEND: '防守方裁決',\n});\n\nlog(PHASE.COLLECT_DIFF, 'INF', `變更檔案 ${allFiles.length} 個…`);"
},
{
"id": "F002",
"reviewer": "🧪 Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 252,
"endLine": 289,
"problem": "commitAndPushFindings 的推送行為(有無變更、認證方式、空 commit 防護、是否觸發 CI)缺少測試證明。此為本次變更的核心行為,卻沒有任何測試覆蓋。",
"suggestion": "mock git 命令,斷言:無 staged diff 時回傳 false 且不執行 commit/push;有變更時以認證方式推送到正確 refspec 與分支。注意:原 finding 描述的 pushToken 對比 origin 雙軌邏輯已於重構後移除(現行一律以 token 經 pushWithCredential 認證推送),撰寫測試前需依現行程式碼重新界定情境。",
"suggestedCode": ""
}
],
"excluded": []
}
@@ -0,0 +1,209 @@
{
"generatedAt": "2026/07/20 18:31:07",
"commitSha": "0c29daa01434d6b6fa1b20ce81c513c75b1b1c6a",
"prNumber": null,
"tool": {
"name": "code-review-resolve",
"version": "0.0.9",
"model": "(工具預設)"
},
"findings": [
{
"id": "F001",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 119,
"endLine": 139,
"problem": "流程步驟編號被當成跨模組識別值,散落在 `index.js`、各 library 的日誌與 JSDoc、README 流程圖及功能表。這次僅因插入並延後一步,就必須同步修改大量 `步驟2`~`步驟8` 字串,而且實際執行順序已變成 1、3~8、2、9~10;未來再調整流程時非常容易讓文件、日誌與程式碼脫節。",
"suggestion": "以穩定的語意階段名稱取代硬編碼序號,例如 `TOOL_DETECTION`、`COLLECT_DIFF`、`RESOLVE_OLD_COMMENTS`,由單一常數表集中決定顯示名稱;README 的流程順序則從同一份定義產生,或至少不要在各函式文件重複紀錄易變的數字。",
"suggestedCode": "```\nconst PHASE = Object.freeze({\n FAST_RESULT: '快速回報',\n DETECT_TOOL: '偵測 AI 工具',\n COLLECT_DIFF: '整理變更',\n RESOLVE_OLD: '處理舊留言',\n PUBLISH_FINDINGS: '發布審查結果',\n});\n\nlog(PHASE.DETECT_TOOL, 'INF', `選用工具:${tool.name}。`);\n```",
"sourceIssue": 11
},
{
"id": "F002",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "警告",
"file": "readme.md",
"startLine": 59,
"endLine": 130,
"problem": "功能索引把分支名稱與原始碼行號硬編碼在數十個連結中;本次僅因程式碼增行,就必須人工把大量 `#L...` 全面更新,已直接顯示這份文件存在高同步成本。之後任一檔案前段增刪程式碼,都會讓這些連結再次漂移,而且指向會持續變動的 `develop` 分支,使舊版 README 與實際連結內容無法穩定對應。",
"suggestion": "不要手動維護原始碼行號。若只需導覽,連到檔案並由右欄既有章節錨點提供函式級定位;若必須精確指向定義,應由 AST/文件產生工具在 CI 自動建立索引,並連到固定 commit SHA 或版本 tag。至少增加連結檢查,避免半年後整張功能表悄悄失準。",
"suggestedCode": "```\n| 功能名稱 | 功能描述 |\n| --- | --- |\n| [gitrepo.resolveMergeBase](src/lib/gitrepo.js) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) |\n```",
"sourceIssue": 12
},
{
"id": "F006",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 134,
"endLine": 134,
"problem": "在檢查是否為淺層 repository (shallow repository) 時,呼叫了外部子行程執行 `git rev-parse --is-shallow-repository`。建立與啟動 OS 子行程是非常昂貴的操作,會白白浪費數十毫秒的 CPU 週期與系統資源。",
"suggestion": "Git 在淺層 clone 時會在 `.git` 目錄下建立一個 `shallow` 檔案。我們可以使用 Node.js 內建的 `fs.existsSync` 進行本地檔案檢查,不需啟動額外的 Git 子行程,執行速度可快上百倍。",
"suggestedCode": "```\nconst fs = require('fs');\n// ...\nif (fs.existsSync(path.join(cwd, '.git', 'shallow'))) {\n strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']);\n}\n```",
"sourceIssue": 14
},
{
"id": "F007",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 293,
"endLine": 305,
"problem": "為了防止 Git 推送失敗時在例外訊息中回顯包含 Token 與遠端 URL 的命令列參數,pushWithCredential 的 catch 區塊直接拋出一個固定的 Error('推送審查結果 commit 失敗...')。但這樣一來,它完全吞掉了原始的錯誤(例如 non-fast-forward 非快轉、分支保護規則阻擋、或連線逾時),六個月後的維護者在 CI log 中看到此錯誤時,完全無從判斷失敗的原因。",
"suggestion": "建議在保留安全遮罩的前提下,保留原始 exception 的排錯線索。例如可以檢查並安全地過濾 err.message 或 err.stderr 中所有的敏感字串(如 Token/URL),然後將其作為新錯誤的 cause 屬性或附加訊息傳遞下去。",
"suggestedCode": "```\n} catch (err) {\n // 過濾敏感資訊後保留錯誤細節\n const safeMessage = err.message ? redactSecrets(err.message) : '未知錯誤';\n const error = new Error(`推送審查結果 commit 失敗(${safeMessage})。`);\n error.cause = err;\n throw error;\n }\n```",
"sourceIssue": 14
},
{
"id": "F009",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 308,
"endLine": 368,
"problem": "新增或修改的步驟分隔註解(如步驟 8 分組、步驟 2 延後、步驟 9、步驟 10、及建問題模式收束)其尾隨的水平分隔線(─)長度不一或僅存單一字元,破壞了專案既有程式碼中整齊劃一的長分隔線視覺排版,視覺上顯得雜亂、走調。",
"suggestion": "補足尾隨的水平線 ─,使其與鄰近步驟分隔註解的長度(約 70~80 字元寬度)與視覺風格保持一致,維持排版的美觀。",
"suggestedCode": "```\n// ── 步驟 8(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ────────────────\n```",
"sourceIssue": 14
},
{
"id": "F010",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "建議",
"file": "src/index.js",
"startLine": 366,
"endLine": 382,
"problem": "在建問題模式收束時,先 `await gitea.createIssueComment` 再 `await gitea.addIssueDependency`,這兩個 Gitea API 呼叫是獨立且無資料相依性的,卻以序列(Sequential)方式執行,白白浪費了一次網路往返(RTT)的等待時間。",
"suggestion": "使用 `Promise.all` 同時發起這兩個請求,並行處理以減少整體 execution 的等待時間。",
"suggestedCode": "```\nconst commentPromise = gitea.createIssueComment(\n ctx,\n templates.issueLinkComment({\n issueNumber: issue.number,\n issueUrl: issue.html_url,\n severeCount: severe.length,\n otherCount: others.length,\n })\n );\n const dependencyPromise = gitea.addIssueDependency(ctx, ctx.prNumber, issue.number)\n .then(() => log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}。`))\n .catch((err) => log('建問題', 'WRN', `設定 PR 相依失敗:${err.message}。`));\n \n await Promise.all([commentPromise, dependencyPromise]);\n```",
"sourceIssue": 14
},
{
"id": "F012",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 48,
"endLine": 53,
"problem": "在 `agentFailureDetail` 之中,進行 stderr 與 stdout 的遮罩處理時,是先截斷至 2,000 字元,然後執行多次複雜的 `redactSecrets` 正規表示式替換,最後再截斷至 500 字元輸出。這會造成 1,500 字元的複雜 regex 運算結果在下一步被直接丟棄,白白浪費了 CPU 進行字串比對與取代的週期。",
"suggestion": "應在呼叫 `redactSecrets` 之前,就先將字串截斷至目標長度(500 字元),再進行遮罩,可大幅減少 regex 運算負擔。",
"suggestedCode": "```\nconst stderr = redactSecrets(String((res && res.stderr) || '').slice(0, 500));\n if (stderr) parts.push(`stderr${stderr}`);\n const stdout = redactSecrets(String((res && res.output) || '').slice(0, 500));\n if (stdout) parts.push(`stdout${stdout}`);\n```",
"sourceIssue": 14
},
{
"id": "F014",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "readme.md",
"startLine": 56,
"endLine": 132,
"problem": "README 的功能表大量手動維護 `src/branch/develop/...#Lxx` 深連結,這次光是分支與行號就改了整排。這類文件會隨任何插入註解、重排函式、換預設分支而失準,未來維護者必須在改程式時同步更新一大段文件,維護成本偏高。",
"suggestion": "改成不依賴行號的相對連結,或用文件產生腳本從原始碼 JSDoc 自動產出這張表。若仍要指向 Gitea,建議至少移除 `#Lxx`,或集中定義分支名稱,避免每次改分支都要全表搜尋替換。",
"suggestedCode": "",
"sourceIssue": 15
},
{
"id": "F022",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "警告",
"file": "readme.md",
"startLine": 76,
"endLine": 132,
"problem": "README 內大量函式清單同時硬編分支名稱與行號錨點,這次 diff 已經整批從 `master` 改成 `develop` 並同步調整行號。這類文件和原始碼結構高度重複,後續只要插入幾行程式,文件連結就會失準,維護者必須靠人工記得同步整張表。",
"suggestion": "改成不含行號的穩定檔案連結,或把這份 API/功能表改由 JSDoc/腳本產生。若一定要保留行號,建議把產生流程寫入 npm script,避免每次程式碼位移都人工批次修改 README。",
"suggestedCode": "",
"sourceIssue": 18
},
{
"id": "F025",
"reviewer": "Assassin",
"focus": "",
"badge": "🗡️",
"severity": "警告",
"file": "action.yml",
"startLine": 21,
"endLine": 24,
"problem": "這裡建議呼叫端傳入「能觸發 CI 的 PAT」作為 action token。攻擊者最愛這種長效、可推送、可觸發 workflow 的憑證:只要此 action 在不受信任 PR 上執行,或 PR 能影響 action/workflow 執行內容,惡意變更就可能讀取 `INPUT_TOKEN`、推送結果 commit、再藉由可觸發 CI 的身分製造後續執行鏈。自動 token 原本不觸發 CI 是一道防線,這個建議等於要求使用者把防線拆掉。",
"suggestion": "不要泛稱建議使用可觸發 CI 的 PAT。文件與介面應明確要求最小權限、repo 限定、短效或可輪替 token,並禁止在 fork/不受信任 PR context 暴露 PAT。更穩的設計是分離 API 留言 token 與 push token,且只有在明確受信任事件或受保護分支才允許 push token 存在;否則拒絕 commit/push,只做留言或 artifact。",
"suggestedCode": "",
"sourceIssue": 19
},
{
"id": "F026",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "readme.md",
"startLine": 59,
"endLine": 132,
"problem": "README 的功能列表手動維護了大量 `src/branch/develop/...#Lxx` 深連結與行號。這次 PR 已經一次改動數十個 branch/line anchor,代表文件和原始碼行號高度耦合;下一次只要插入幾行程式,文件就會悄悄過期,維護者很難知道哪些連結還準。",
"suggestion": "避免在手寫 README 綁定行號,改連到函式所在檔案或穩定章節錨點;若必須保留行號,請把這段改成產生式文件,讓 CI 或腳本從原始碼/JSDoc 重新生成,減少人工同步成本。",
"suggestedCode": "",
"sourceIssue": 19
},
{
"id": "F031",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "readme.md",
"startLine": 56,
"endLine": 132,
"problem": "README 的功能表大量硬編遠端分支名稱與行號,這次只是從 `master` 改成 `develop` 並同步行號,但這種文件很容易在下一次函式移動、預設分支更名或重排時再次整批失準。未來維護者會被迫反覆做低價值的連結校正,文件也可能在沒人注意時指到錯誤位置。",
"suggestion": "若 README 是 repo 內文件,優先改成相對路徑連結,並避免固定行號;若必須保留行號,建議用產生腳本統一輸出這張表,讓分支名與行號只從單一來源計算。",
"suggestedCode": "",
"sourceIssue": 21
},
{
"id": "F040",
"reviewer": "Assassin",
"focus": "",
"badge": "🗡️",
"severity": "警告",
"file": "action.yml",
"startLine": 19,
"endLine": 22,
"problem": "這段新增說明鼓勵呼叫端傳入「能觸發 CI 的 PAT」。攻擊者最喜歡這種長效、高權限、可觸發 workflow 的憑證:若 action 跑在不可信 PR、AI CLI 被 prompt injection 誘導讀環境變數,或同 repo PR 可改動本 action 程式碼,就可能把 PAT 外送或濫用成寫入 repo/觸發 CI 的跳板。",
"suggestion": "不要把長效 PAT 當建議預設。改用最小權限、短效的 GitHub AppGitea App token,並明確禁止在不可信 fork PR 傳入可寫 token。若目標只是回報檢查結果,優先用 status/check API 寫結果,不要靠 PAT push 再觸發下一輪 CI。",
"suggestedCode": "```\ndescription: 'Gitea API tokenPR/issue 留言與 push findings 用;請使用最小權限、短效 token,勿在不可信 PR 傳入長效 PAT'\n```",
"sourceIssue": 23
},
{
"id": "F041",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "readme.md",
"startLine": 58,
"endLine": 131,
"problem": "README 的功能表把分支名稱與行號大量硬編在外部 URL 裡,這次已經需要整批 `master` 改 `develop` 並同步多個 `#Lxx`。這類文件會隨任何程式碼插行、函式移動或預設分支變更而失準,維護成本會線性累積,最後讀者點到的文件比沒有文件更誤導。",
"suggestion": "改用 repo 相對連結、不固定行號,或把這段功能表改由腳本從 JSDoc 自動產生。若需要連到特定實作,優先連到檔案或錨點,避免每次重排程式碼都要同步更新幾十個行號。",
"suggestedCode": "```\n| log.taipeiNow | [src/lib/log.js](src/lib/log.js) | 取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串 |\n| review.runAttackers | [src/lib/review.js](src/lib/review.js) | 攻擊方 sub agent 並行找問題並合併列表 |\n```",
"sourceIssue": 23
}
],
"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 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。"
}
}
}
]
}
+1 -3
View File
@@ -19,9 +19,7 @@ inputs:
# Gitea API token:用於對 PRissue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。 # Gitea API token:用於對 PRissue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。
token: token:
# 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。 # 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。
# 建議傳入能觸發 CI 的 PAT」:以自動 tokengitea.token / GITHUB_TOKEN推送結果 commit 不會 # 建議傳入能觸發 CI 的 PAT自動 token 推送結果 commit 時可能不會再觸發 workflow。
# 再觸發 CI,導致新 head 缺檢查而卡合併;改用 PAT 推送會讓 PR 的 synchronize 事件再觸發 CI
# 由主程式步驟 1 快速回報([success]/[failure])廉價地把結果蓋到新 head。
description: 'Gitea API tokenPR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)' description: 'Gitea API tokenPR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)'
# 必填:缺少 token 無法呼叫 Gitea APIaction 無法運作。 # 必填:缺少 token 無法呼叫 Gitea APIaction 無法運作。
required: true required: true
+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"
+10 -10
View File
@@ -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]
``` ```
## 專案列表 ## 專案列表
+50 -64
View File
@@ -117,31 +117,11 @@ function commitFindings({ cwd, ctx, files, result }) {
} }
/** /**
* 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:本輪「審查」一律回傳 0(不因嚴重問題直接讓檢查失敗——
* 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報); * 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報);
@@ -182,14 +162,14 @@ 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 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 +178,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 (ctx.createIssue) {
if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body); if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);
issueBuffer.push(body); pendingIssueCommentBodies.push(body);
return null; return null;
} }
const created = await gitea.createIssueComment(ctx, body); const created = await gitea.createIssueComment(ctx, body);
@@ -210,24 +190,25 @@ async function main() {
}; };
/** /**
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立), * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
* 並把 `issueBuffer` 內暫存的情境留言依序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 * 並把 `pendingIssueCommentBodies` 內暫存的情境留言依流程順序寫入 issue
* 設定閉包變數 `trackingIssue` 供後續留言直接發到 issue。
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。 * 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
* *
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選); * @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
* 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。 * 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。
* @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。 * @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `trackingIssue` 與 issue 留言。
*/ */
const ensureIssueCreated = async (labelIds = []) => { const createIssueAndFlushBufferedComments = 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) { for (const body of pendingIssueCommentBodies) {
await gitea.createCommentOnIssue(ctx, issue.number, body); await gitea.createCommentOnIssue(ctx, trackingIssue.number, body);
} }
issueBuffer.length = 0; pendingIssueCommentBodies.length = 0;
}; };
// ── 步驟 2:延後執行 ─────────────────────────────────────────────────── // ── 步驟 2:延後執行 ───────────────────────────────────────────────────
@@ -245,7 +226,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,
@@ -270,7 +251,7 @@ async function main() {
if (ctx.createIssue) { if (ctx.createIssue) {
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 });
} }
@@ -286,23 +267,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 });
@@ -335,7 +318,7 @@ async function main() {
} catch (err) { } catch (err) {
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`); log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
} }
await ensureIssueCreated(labelIds); await createIssueAndFlushBufferedComments(labelIds);
} else { } else {
// 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。 // 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。
log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。'); log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。');
@@ -353,7 +336,7 @@ async function main() {
// ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue)─ // ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue)─
if (severe.length > 0) { if (severe.length > 0) {
if (ctx.createIssue) { if (ctx.createIssue) {
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 });
} }
@@ -363,21 +346,21 @@ async function main() {
// 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ── // 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ──
if (others.length > 0) { if (others.length > 0) {
if (ctx.createIssue) { if (ctx.createIssue) {
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 → createIssueAndFlushBufferedComments),此處不再補掛。
if (ctx.createIssue && issue) { if (ctx.createIssue && trackingIssue) {
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,24 +369,27 @@ 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: ctx.createIssue,
filesToCommit.push(path.join('.gitea', 'ai-review', 'exclusions.json')); severeCount: severe.length,
} relativePath,
exclusionsChanged,
});
if (filesToCommit.length > 0) { if (filesToCommit.length > 0) {
commitFindings({ cwd, ctx, files: filesToCommit, result }); commitFindings({ cwd, ctx, files: filesToCommit, result });
} else { } else {
+71
View File
@@ -0,0 +1,71 @@
'use strict';
// AI CLI 失敗診斷與機密遮罩工具:供 review 流程記錄安全、限長的一行錯誤摘要。
// 遮罩前先截去的輸入上限,避免對數 MB 失敗輸出跑整份 O(k*n) 正規掃描。
const AGENT_DIAGNOSTIC_INPUT_LIMIT = 2_000;
// 每段診斷片段(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(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***')
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***')
.trim();
}
/**
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
*
* 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或
* 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。因此本函式預設只輸出退出碼、
* 訊號與逾時狀態;只有 `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') {
const stderr = redactSecrets(String((agentResult && agentResult.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
if (stderr) parts.push(`stderr${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
const stdout = redactSecrets(String((agentResult && agentResult.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
if (stdout) parts.push(`stdout${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
}
if (parts.length === 0) {
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
}
return parts.join('');
}
module.exports = {
AGENT_DIAGNOSTIC_INPUT_LIMIT,
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,
}); });
+69 -16
View File
@@ -4,6 +4,34 @@ const { execFileSync } = require('child_process');
// git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。 // git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。
/**
* 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。
*
* @param {string} refName - 使用者或事件 payload 提供的分支名稱。
* @param {string} fieldName - 錯誤訊息中的欄位名稱。
* @returns {string} 原樣回傳通過驗證的分支名稱。
* @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。
* @remarks
* 使用情境:`resolveMergeBase` 的 `baseRef` 與 `commitAndPushFindings` 的
* `headRef` 會被組進 refspec;先驗證可避免惡意 payload 影響本地 refs 路徑。
*/
function assertSafeBranchRef(refName, fieldName) {
const value = String(refName || '').trim();
if (!value) throw new Error(`${fieldName} 不可為空。`);
if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) {
throw new Error(`${fieldName} 不是安全的分支名稱:${value}`);
}
try {
execFileSync('git', ['check-ref-format', '--branch', value], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
} catch {
throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`);
}
return value;
}
/** /**
* 同步執行 git 指令並回傳原始 stdout 輸出。 * 同步執行 git 指令並回傳原始 stdout 輸出。
* *
@@ -102,6 +130,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 原始輸出,避免洩漏遠端資訊)。
@@ -130,17 +159,19 @@ function resolveMergeBase(cwd, baseRef) {
diagnostics.push('merge-base(首次):失敗'); diagnostics.push('merge-base(首次):失敗');
} }
// 資料驅動的補抓策略:淺層才 unshallow;其後依序 deepen base 與 HEAD。 // 資料驅動的補抓策略:先以固定深度分批加深 base 與 HEAD(每步後重試 merge-base,成功即回傳);
// 每個策略成功後立即重試 merge-base,成功即回傳,避免多餘往返。 // 只有仍失敗且為淺層 repo 時,才把成本最高的 --unshallow(下載完整歷史)當最後手段,
const strategies = []; // 避免大型/長壽 repo 只為找共同祖先就無謂拉全史。
// 加深 HEAD 側須以「目前 HEAD 的 commit SHA」補抓——遠端符號 `HEAD` 由伺服器解析為
// 遠端預設分支(非目前 checkout 的 PR head),只加深它並不會補到 PR head 的歷史。
const headSha = gitTrim(cwd, 'rev-parse', 'HEAD');
const strategies = [
['deepen base', 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`],
['deepen PR HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', headSha],
];
if (gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true') { if (gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true') {
strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']); strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']);
} }
strategies.push([
'deepen base',
'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`,
]);
strategies.push(['deepen HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD']);
for (const [label, ...args] of strategies) { for (const [label, ...args] of strategies) {
if (!runFetch(label, ...args)) continue; // fetch 失敗就換下一個策略。 if (!runFetch(label, ...args)) continue; // fetch 失敗就換下一個策略。
@@ -249,6 +280,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);
@@ -270,7 +302,7 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
const remoteUrl = `${serverUrl}/${repository}.git`; const remoteUrl = `${serverUrl}/${repository}.git`;
// 一律以 token 的身分明確認證推送(不走 origin 的自動 token)——只要 token 是能觸發 CI 的 PAT // 一律以 token 的身分明確認證推送(不走 origin 的自動 token)——只要 token 是能觸發 CI 的 PAT
// 結果 commit 就會讓 PR 的 synchronize 事件再觸發 CI,由步驟 1 快速回報把結果蓋到新 head。 // 結果 commit 就會讓 PR 的 synchronize 事件再觸發 CI,由步驟 1 快速回報把結果蓋到新 head。
pushWithCredential(cwd, remoteUrl, token, refspec); pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl);
return true; return true;
} }
@@ -279,20 +311,35 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
* *
* 認證方式:等同 `https://ai-review-bot:<secret>@host/...` 的 HTTP Basicgit 會把 * 認證方式:等同 `https://ai-review-bot:<secret>@host/...` 的 HTTP Basicgit 會把
* URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的 * URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的
* `GIT_CONFIG_*` 環境變數注入 `http.<url>.extraheader`,使 base64 憑證**不出現在 argv** * `GIT_CONFIG_*` 環境變數注入 `http.<serverUrl>/.extraheader`,使 base64 憑證**不出現在 argv**
* (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。 * (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。
*
* 觸發 CI 關鍵:`actions/checkout` 會把「自動 Actions token」持久化在同一個
* `http.<serverUrl>/.extraheader` scope;若沿用它推送,Gitea 會視為「自動 token 觸發」而
* **不再觸發 workflow**(防遞迴)。故本函式對這次 push 於該 scope**先以空值重置**(清掉自動
* token——git 對 extraHeader 給空值即清空既有清單),**再注入 PAT 的 Authorization**,讓推送以
* PAT 身分進行、觸發 PR 的 synchronize;作用範圍僅限本次 push 的環境變數,不影響 action 其他
* 仰賴 checkout 持久化憑證的 fetch(如 {@link resolveMergeBase})。
* 推送失敗時**不重拋原始例外**(其 message 會含命令列與遠端 URL),改拋固定訊息。 * 推送失敗時**不重拋原始例外**(其 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} 成功即返回;失敗拋出不含 URL/argv/token 的固定錯誤。 * @returns {void} 成功即返回;失敗拋出不含 URL/argv/token 的固定錯誤。
* @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,
@@ -301,9 +348,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', // 兩筆同 scope 設定:先空值清掉 checkout 的自動 token,再注入 PAT 的 Authorization。
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 {
@@ -318,4 +368,7 @@ module.exports = {
fileDiff, fileDiff,
fileLastUpdatedIso, fileLastUpdatedIso,
commitAndPushFindings, commitAndPushFindings,
__test: {
assertSafeBranchRef,
},
}; };
+24 -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 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或
* 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。權衡「可除錯性」後:本函式
* 於失敗時**預設**附上經 {@link redactSecrets} 遮罩且去除控制字元的 **stderr 與 stdout**
* 片段(各先截去過長輸入再取前 500 字)——只印 exit code 幾乎無從判斷 CLI 為何失敗,
* 且部分 CLI(如 claude-code 的 `-p` 模式)將錯誤寫到 stdout 而非 stderr。純函式、不拋例外。
*
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
* `runAgent` 的回傳物件。
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
* @remarks
* 使用情境:{@link runAttackers}{@link runDefenders}{@link fillPurposes}{@link selectLabels}
* 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗;
* 需要原始輸出診斷時,於 workflow 設定 secret `ACTIONS_STEP_DEBUG=true` 再重跑。
* 本函式未匯出,僅供模組內部使用。
*/
function agentFailureDetail(res) {
const parts = [];
const err = res && res.error;
if (err) {
if (err.killed) parts.push('已逾時終止');
if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
else if (err.code) parts.push(`code ${err.code}`);
else if (err.signal) parts.push(`signal ${err.signal}`);
}
// 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描;
// 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。
const INPUT_LIMIT = 2_000;
// 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 與 stdout 片段——CLI 失敗時
// 只印 exit code 幾乎無從除錯(見 test-claude 秒失敗案例);且部分 CLI(如 claude-code 的
// -p 模式)會把錯誤寫到 stdout 而非 stderr,故兩者都輸出。redactSecrets 為盡力防線。
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT));
if (stderr) parts.push(`stderr${stderr.slice(0, 500)}`);
const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT));
if (stdout) parts.push(`stdout${stdout.slice(0, 500)}`);
if (parts.length === 0) {
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
}
return parts.join('');
}
/** /**
* 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。 * 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。
* *
@@ -622,6 +557,28 @@ 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;
}
/** /**
* 就地排序 findings:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。 * 就地排序 findings:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。
* *
@@ -922,6 +879,7 @@ module.exports = {
runDefenders, runDefenders,
sortFindings, sortFindings,
appendExclusions, appendExclusions,
resultFilesToCommit,
selectLabels, selectLabels,
postSevereToIssue, postSevereToIssue,
postOthersToIssue, postOthersToIssue,
+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 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以 * 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以
* {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。 * {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。
*/ */
function issueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) { function prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {
return `${MARK} return `${MARK}
## 🔍 AI Code Review|已建立追蹤問題 ## 🔍 AI Code Review|已建立追蹤問題
@@ -421,5 +421,5 @@ module.exports = {
issueBody, issueBody,
issueFindingComment, issueFindingComment,
nothingToReviewComment, nothingToReviewComment,
issueLinkComment, prIssueLinkComment,
}; };
+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',
});
});
});
+24
View File
@@ -0,0 +1,24 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const gitrepo = require('../src/lib/gitrepo');
test('assertSafeBranchRef 接受一般分支名稱', () => {
assert.equal(gitrepo.__test.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123');
});
test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => {
assert.throws(
() => gitrepo.__test.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'),
/不是安全的分支名稱/,
);
});
test('resolveMergeBase 會在 git fetch 前拒絕不安全 baseRef', () => {
assert.throws(
() => gitrepo.resolveMergeBase(process.cwd(), '../../hooks/pre-push'),
/不是安全的分支名稱/,
);
});
+99
View File
@@ -0,0 +1,99 @@
'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('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'],
);
});