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
13 changed files with 347 additions and 196 deletions
+99
View File
@@ -1692,5 +1692,104 @@
"endLine": 358, "endLine": 358,
"problem": "推送結果 commit 的認證方式改成 `pushWithCredential`:不走 origin、自訂 `GIT_CONFIG_*` extraheader、先清掉 checkout token、URL origin 必須相符、失敗時隱藏遠端與 token。這些安全與 CI 觸發相關行為目前完全沒有測試;現有 `gitrepo.test.js` 只測分支名稱驗證。", "problem": "推送結果 commit 的認證方式改成 `pushWithCredential`:不走 origin、自訂 `GIT_CONFIG_*` extraheader、先清掉 checkout token、URL origin 必須相符、失敗時隱藏遠端與 token。這些安全與 CI 觸發相關行為目前完全沒有測試;現有 `gitrepo.test.js` 只測分支名稱驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄 commitAndPushFindingspushWithCredential 的 token 認證推送路徑、推送目標、錯誤遮蔽與安全行為缺少測試。" "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 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。"
} }
] ]
@@ -59,19 +59,6 @@
"problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)", "problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)",
"suggestion": "mock git 執行器與 URLenv 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。", "suggestion": "mock git 執行器與 URLenv 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。",
"suggestedCode": "" "suggestedCode": ""
},
{
"id": "F009",
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "action.yml",
"startLine": 47,
"endLine": 61,
"problem": "push-token 的用途與退回行為在區塊註解、欄位描述、required 與 default 註解中反覆說明,且單行 description 過長,資訊雖完整但重複,日後修改語意易只改到一處。",
"suggestion": "保留一段「為何需要 PAT」的必要背景,其餘讓欄位名稱、required、default 自行表意,將 description 收斂成呼叫端真正需要知道的契約。註:本專案採 doc-funcs 高密度註解慣例,是否精簡屬慣例取捨,需維護者確認。",
"suggestedCode": " # 專用推送 PAT;以 PAT 推送可重新觸發 CI。留空時沿用 token。\n push-token:\n description: '推送審查結果 commit 的 PAT(留空時沿用 token'\n required: false\n default: ''"
} }
], ],
"excluded": [] "excluded": []
@@ -120,20 +120,6 @@
"suggestedCode": "", "suggestedCode": "",
"sourceIssue": 15 "sourceIssue": 15
}, },
{
"id": "F015",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 190,
"endLine": 197,
"problem": "`dependency` 作為參數名太薄,與 `issueNumber` 並列時看不出它也是 issue 編號。讀到 `addIssueDependency(ctx, ctx.prNumber, issue.number)` 時,語意要靠上下文補拍子。",
"suggestion": "改用更完整的名稱,例如 `dependencyIssueNumber` 或 `blockingIssueNumber`,讓「誰被誰阻擋」在簽名裡就清楚成形。",
"suggestedCode": "```\nfunction addIssueDependency(ctx, issueNumber, dependencyIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {\n index: dependencyIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```",
"sourceIssue": 15
},
{ {
"id": "F022", "id": "F022",
"reviewer": "Leo", "reviewer": "Leo",
@@ -148,34 +134,6 @@
"suggestedCode": "", "suggestedCode": "",
"sourceIssue": 18 "sourceIssue": 18
}, },
{
"id": "F023",
"reviewer": "Mage",
"focus": "",
"badge": "🔮",
"severity": "警告",
"file": "src/index.js",
"startLine": 374,
"endLine": 387,
"problem": "建問題模式下只要有任何保留 finding 就會建立 issue,且後續一律把 PR 設為相依於該 issue;但收尾結果仍是 `severe.length === 0 ? 'success' : 'failure'`。最小情境:攻擊方只產生 1 條「建議」,`severe.length` 為 0action commit `[success]`,但 PR 被 issue dependency 擋住無法合併。這讓「success=可通過」與「非嚴重問題也阻擋合併」兩個語義互相矛盾。",
"suggestion": "明確對齊語義:若只有嚴重問題才應阻擋合併,則只在 `severe.length > 0` 時建立 dependency;若所有保留問題都要阻擋合併,則 result/exit code 不應只看嚴重問題。",
"suggestedCode": "```\nif (severe.length > 0) {\n try {\n await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number);\n log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}。`);\n } catch (err) {\n log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`);\n }\n}\n```",
"sourceIssue": 18
},
{
"id": "F024",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 179,
"endLine": 181,
"problem": "`issueBuffer` 的旋律太含糊:讀者會以為裡面放的是 issue,實際上暫存的是尚未送出的留言 body。命名沒有把資料形狀唱清楚。",
"suggestion": "改成能描述內容與用途的名稱,例如 `pendingIssueCommentBodies`,並同步調整註解與迴圈變數。",
"suggestedCode": "```\nconst pendingIssueCommentBodies = [];\nlet issue = null;\n```",
"sourceIssue": 18
},
{ {
"id": "F025", "id": "F025",
"reviewer": "Assassin", "reviewer": "Assassin",
@@ -204,34 +162,6 @@
"suggestedCode": "", "suggestedCode": "",
"sourceIssue": 19 "sourceIssue": 19
}, },
{
"id": "F027",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 181,
"endLine": 185,
"problem": "`issue` 這個變數名太素,像樂譜上只寫「音符」卻不說是哪一聲部。此處承載的是建問題模式建立出的追蹤 issue,後面還會與 PR issue 編號、Gitea issue API 參數交錯出現,名稱過泛會讓閱讀節奏變濁。",
"suggestion": "改成能表明角色的名稱,例如 `trackingIssue`。對應的 `ensureIssueCreated` 也可改為 `ensureTrackingIssueCreated`,讓閉包狀態與用途一眼對上。",
"suggestedCode": "```\nconst issueBuffer = [];\nlet trackingIssue = null;\n\nconst postComment = async (body) => {\n if (ctx.createIssue) {\n if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n issueBuffer.push(body);\n return null;\n }\n const created = await gitea.createIssueComment(ctx, body);\n currentRunCommentIds.add(created.id);\n return created;\n};\n```",
"sourceIssue": 19
},
{
"id": "F029",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 388,
"endLine": 405,
"problem": "`issueLinkComment` 產生的是 PR 上唯一的建問題模式回貼留言,但名稱少了 PR 的聲部;同檔已有 `issueBody`、`issueFindingComment`,乍看會以為這也是 issue 內留言模板,命名層次不夠分明。",
"suggestion": "改名為 `prIssueLinkComment` 或 `trackingIssueLinkComment`,讓模板的投遞位置與用途直接寫在名稱裡,避免與 issue 內文、issue finding 留言混成一團。",
"suggestedCode": "```\nfunction prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {\n return `${MARK}\n## 🔍 AI Code Review|已建立追蹤問題\n\n本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`;\n}\n```",
"sourceIssue": 19
},
{ {
"id": "F031", "id": "F031",
"reviewer": "Leo", "reviewer": "Leo",
@@ -246,34 +176,6 @@
"suggestedCode": "", "suggestedCode": "",
"sourceIssue": 21 "sourceIssue": 21
}, },
{
"id": "F032",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 183,
"endLine": 224,
"problem": "`issue` 與 `issueBuffer` 的命名過於泛泛;在 Gitea 裡 PR 也是 issue,追蹤問題也是 issue,單靠 `issue` 這個名字無法唱出它究竟是哪一個聲部。",
"suggestion": "建議改成更具語義的名稱,例如 `trackingIssue`、`trackingIssueCommentBuffer`,讓讀者不用回讀 create-issue 模式的整段脈絡。",
"suggestedCode": "```\nlet trackingIssue = null;\nconst trackingIssueCommentBuffer = [];\n```",
"sourceIssue": 21
},
{
"id": "F033",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 190,
"endLine": 194,
"problem": "`addIssueDependency(ctx, issueNumber, dependency)` 的兩個參數名稱太相似,且 `dependency` 少了 issue 語義。這支 API 的方向性本來就容易讀錯,命名再模糊就像兩個音符共用同一個名字。",
"suggestion": "建議把參數改成能表達方向的名稱,例如 `blockedIssueNumber` 與 `blockingIssueNumber`,呼叫端也會更清楚是誰被誰擋住。",
"suggestedCode": "```\nfunction addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {\n index: blockingIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```",
"sourceIssue": 21
},
{ {
"id": "F040", "id": "F040",
"reviewer": "Assassin", "reviewer": "Assassin",
@@ -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
+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]
``` ```
## 專案列表 ## 專案列表
+29 -48
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 留言物件;
@@ -200,8 +180,8 @@ async function main() {
*/ */
const queueOrPostComment = 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 createIssueAndFlushBufferedComments = 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:延後執行 ───────────────────────────────────────────────────
@@ -355,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 });
} }
@@ -365,7 +346,7 @@ 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 queueOrPostComment(templates.othersComment(others)); await queueOrPostComment(templates.othersComment(others));
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
@@ -374,12 +355,12 @@ async function main() {
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─ // ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。 // 標籤已於建立 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,
}), }),
@@ -388,15 +369,15 @@ async function main() {
// 僅有警告/建議時,issue 仍建立供追蹤,但不掛相依、不阻擋 PR 合併。 // 僅有警告/建議時,issue 仍建立供追蹤,但不掛相依、不阻擋 PR 合併。
if (severe.length > 0) { if (severe.length > 0) {
try { try {
await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number); await gitea.addIssueDependency(ctx, ctx.prNumber, trackingIssue.number);
log('建問題', 'INF', `有嚴重問題:已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}issue 關閉前無法合併。`); log('建問題', 'INF', `有嚴重問題:已將 PR #${ctx.prNumber} 設為相依於 issue #${trackingIssue.number}issue 關閉前無法合併。`);
} catch (err) { } catch (err) {
log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}`); log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}`);
} }
} else { } else {
log('建問題', 'INF', `無嚴重問題(僅警告/建議):issue #${issue.number} 僅供追蹤,不阻擋 PR 合併。`); log('建問題', 'INF', `無嚴重問題(僅警告/建議):issue #${trackingIssue.number} 僅供追蹤,不阻擋 PR 合併。`);
} }
log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`); log('建問題', 'INF', `issue #${trackingIssue.number} 已寫入審查內容,並在 PR 回貼連結。`);
} }
// ── 收尾:commit 並 pushsuccess=無嚴重問題、failure=有嚴重問題)─────── // ── 收尾:commit 並 pushsuccess=無嚴重問題、failure=有嚴重問題)───────
+5 -5
View File
@@ -37,13 +37,13 @@ function redactSecrets(text) {
* 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets} * 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets}
* 遮罩且去除控制字元的 stderr/stdout 片段。 * 遮罩且去除控制字元的 stderr/stdout 片段。
* *
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} agentResult
* `runAgent` 的回傳物件。 * `runAgent` 的回傳物件。
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。 * @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
*/ */
function agentFailureDetail(res) { function agentFailureDetail(agentResult) {
const parts = []; const parts = [];
const err = res && res.error; const err = agentResult && agentResult.error;
if (err) { if (err) {
if (err.killed) parts.push('已逾時終止'); if (err.killed) parts.push('已逾時終止');
if (typeof err.code === 'number') parts.push(`exit ${err.code}`); if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
@@ -52,9 +52,9 @@ function agentFailureDetail(res) {
} }
// 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。 // 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。
if (process.env.ACTIONS_STEP_DEBUG === 'true') { if (process.env.ACTIONS_STEP_DEBUG === 'true') {
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); 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)}`); if (stderr) parts.push(`stderr${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
const stdout = redactSecrets(String((res && res.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_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 (stdout) parts.push(`stdout${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
} }
if (parts.length === 0) { if (parts.length === 0) {
+7 -7
View File
@@ -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,
}); });
+6 -6
View File
@@ -690,9 +690,9 @@ ${JSON.stringify(brief)}
* 不使用 {@link templates.othersComment} 的單一表格——表格僅用於一般模式(PR)。 * 不使用 {@link templates.othersComment} 的單一表格——表格僅用於一般模式(PR)。
*/ */
async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
await Promise.all( for (const finding of severe) {
severe.map((finding) => gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding))), await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding));
); }
log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}`); log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}`);
} }
@@ -716,9 +716,9 @@ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
* 以本函式把警告+建議逐條留言到追蹤 issue,確保 issue 上每條問題都是可個別回覆的留言。 * 以本函式把警告+建議逐條留言到追蹤 issue,確保 issue 上每條問題都是可個別回覆的留言。
*/ */
async function postOthersToIssue({ ctx, gitea, issueNumber, others }) { async function postOthersToIssue({ ctx, gitea, issueNumber, others }) {
await Promise.all( for (const finding of others) {
others.map((finding) => gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding))), await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding));
); }
log('步驟10', 'INF', `已將 ${others.length} 條警告+建議逐條留言到 issue #${issueNumber}`); log('步驟10', 'INF', `已將 ${others.length} 條警告+建議逐條留言到 issue #${issueNumber}`);
} }
+2 -2
View File
@@ -400,7 +400,7 @@ function nothingToReviewComment(ignoredCount) {
* 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以 * 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以
* {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。 * {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。
*/ */
function issueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) { function prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {
return `${MARK} return `${MARK}
## 🔍 AI Code Review|已建立追蹤問題 ## 🔍 AI Code Review|已建立追蹤問題
@@ -421,5 +421,5 @@ module.exports = {
issueBody, issueBody,
issueFindingComment, issueFindingComment,
nothingToReviewComment, nothingToReviewComment,
issueLinkComment, prIssueLinkComment,
}; };
+2 -2
View File
@@ -5,7 +5,7 @@ const test = require('node:test');
const gitea = require('../src/lib/gitea'); const gitea = require('../src/lib/gitea');
function withFetchStub(handler, fn) { function withFetchStub(handler, callback) {
const originalFetch = global.fetch; const originalFetch = global.fetch;
const calls = []; const calls = [];
global.fetch = async (url, options = {}) => { global.fetch = async (url, options = {}) => {
@@ -13,7 +13,7 @@ function withFetchStub(handler, fn) {
return handler(url, options); return handler(url, options);
}; };
return Promise.resolve() return Promise.resolve()
.then(() => fn(calls)) .then(() => callback(calls))
.finally(() => { .finally(() => {
global.fetch = originalFetch; global.fetch = originalFetch;
}); });
+2 -2
View File
@@ -44,7 +44,7 @@ test('agentFailureDetail 在 debug 模式輸出遮罩後片段', () => {
} }
}); });
test('postOthersToIssue 批次送出 issue 留言', async () => { test('postOthersToIssue 依序送出 issue 留言以維持排序', async () => {
const calls = []; const calls = [];
let active = 0; let active = 0;
let maxActive = 0; let maxActive = 0;
@@ -71,7 +71,7 @@ test('postOthersToIssue 批次送出 issue 留言', async () => {
assert.equal(calls.length, 2); assert.equal(calls.length, 2);
assert.equal(calls[0].issueNumber, 7); assert.equal(calls[0].issueNumber, 7);
assert.ok(maxActive > 1); assert.equal(maxActive, 1);
}); });
test('resultFilesToCommit 在建問題模式有嚴重問題時仍提交 findings', () => { test('resultFilesToCommit 在建問題模式有嚴重問題時仍提交 findings', () => {