Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d690bfc7e | ||
|
|
a44122e288 |
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"generatedAt": "2026/07/17 19:03:13",
|
|
||||||
"commitSha": "c4c23d45314bb260d7ec350db4f968021076ee17",
|
|
||||||
"prNumber": 4,
|
|
||||||
"tool": {
|
|
||||||
"name": "codex",
|
|
||||||
"version": "codex-cli 0.144.5",
|
|
||||||
"model": "(工具預設)"
|
|
||||||
},
|
|
||||||
"findings": [],
|
|
||||||
"excluded": []
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"generatedAt": "2026/07/20 09:46:49",
|
|
||||||
"commitSha": "8662e8ca801c3dbf7f74ed7e758a7a55176735b8",
|
|
||||||
"prNumber": 4,
|
|
||||||
"tool": {
|
|
||||||
"name": "claude",
|
|
||||||
"version": "2.1.215 (Claude Code)",
|
|
||||||
"model": "(工具預設)"
|
|
||||||
},
|
|
||||||
"findings": [],
|
|
||||||
"excluded": []
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"generatedAt": "2026/07/20 15:20:13",
|
|
||||||
"commitSha": "c980add8077dd1316a6e4d62be481bc4f1c94e25",
|
|
||||||
"prNumber": 6,
|
|
||||||
"tool": {
|
|
||||||
"name": "code-review-resolve",
|
|
||||||
"version": "0.0.8",
|
|
||||||
"model": "(工具預設)"
|
|
||||||
},
|
|
||||||
"findings": [
|
|
||||||
{
|
|
||||||
"id": "F002",
|
|
||||||
"reviewer": "Maya",
|
|
||||||
"focus": "testing",
|
|
||||||
"badge": "🧪",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 306,
|
|
||||||
"endLine": 359,
|
|
||||||
"problem": "建問題模式的核心流程已大幅改變,但 diff 中沒有對應測試驗證各分支:有保留問題時才建立 issue、暫存留言依序送出、無問題時靜默通過、嚴重與非嚴重問題送往正確位置,以及標籤失敗後仍須回貼 PR 連結。這些分支牽涉多次外部 API 呼叫與狀態切換,未測試時很容易出現漏留言、留言送錯 PR/issue,或在 `issue` 尚未建立時解參考的回歸。",
|
|
||||||
"suggestion": "新增主流程測試並 mock Gitea、agent 與 git 操作,至少涵蓋:`kept=[]` 時不建立 issue 且不留言;僅嚴重問題;僅警告/建議;混合問題;建立 issue 或寫入暫存留言失敗;標籤查詢、AI 選標籤及補掛標籤失敗時仍回貼 issue 連結。除了呼叫次數,也應斷言 API 呼叫順序、目標 issue 編號及留言內容。",
|
|
||||||
"suggestedCode": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "F004",
|
|
||||||
"reviewer": "Maya",
|
|
||||||
"focus": "testing",
|
|
||||||
"badge": "🧪",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/lib/gitrepo.js",
|
|
||||||
"startLine": 100,
|
|
||||||
"endLine": 122,
|
|
||||||
"problem": "`resolveMergeBase` 新增多階段 fetch 與淺層 checkout 修復邏輯,但沒有看到測試驗證成功、降級與最終失敗路徑。尤其初次 merge-base 失敗後,淺層與非淺層 repository 會走不同路徑,且多個 `tryGit` 失敗會被刻意吞掉;若參數、refspec 或重試順序有誤,只會在實際 CI checkout 深度不足時才暴露。",
|
|
||||||
"suggestion": "以 stub 的 git 執行器或暫存 repository 補齊案例:首次 merge-base 成功;淺層 repository 經 `--unshallow` 後成功;`--unshallow` 失敗但 `--deepen=1000` 後成功;非淺層首次失敗後重試成功;所有策略失敗時拋出含 `baseRef` 且保留原始 `cause` 的錯誤。並斷言 base/head fetch 的 refspec 與執行順序。",
|
|
||||||
"suggestedCode": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"excluded": []
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
{
|
|
||||||
"generatedAt": "2026/07/20 15:57:46",
|
|
||||||
"commitSha": "a6534ee0764fe2363ebbb5525586ad3868dc6720",
|
|
||||||
"prNumber": 6,
|
|
||||||
"tool": {
|
|
||||||
"name": "code-review-resolve",
|
|
||||||
"version": "0.0.8",
|
|
||||||
"model": "(工具預設)"
|
|
||||||
},
|
|
||||||
"findings": [
|
|
||||||
{
|
|
||||||
"id": "F003",
|
|
||||||
"reviewer": "Maya",
|
|
||||||
"focus": "testing",
|
|
||||||
"badge": "🧪",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 184,
|
|
||||||
"endLine": 245,
|
|
||||||
"problem": "建問題模式新增「issue 建立前暫存留言、建立後依序沖刷」的狀態流程,但沒有對應測試。尚未驗證 createIssue 或沖刷途中拋錯、空 buffer、重複呼叫、以及一般/建問題模式留言目的地是否正確;失敗路徑可能造成 issue 已建立但內容不完整或留言誤發到 PR。",
|
|
||||||
"suggestion": "補主流程整合測試並 mock Gitea API,驗證:一般模式直接發 PR 且記錄留言 id;issue 未建立時只暫存;建立後依序沖刷並清空 buffer;createIssue 與第 N 則沖刷留言失敗時以失敗結束且不再發布後續內容。屬測試架構決策(專案目前無測試框架)。",
|
|
||||||
"suggestedCode": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "F004",
|
|
||||||
"reviewer": "Maya",
|
|
||||||
"focus": "testing",
|
|
||||||
"badge": "🧪",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 309,
|
|
||||||
"endLine": 395,
|
|
||||||
"problem": "建問題模式核心分支被大幅改寫,但沒有測試驗證各種 findings 組合與 API 失敗行為。kept.length===0、只有嚴重、只有警告/建議、兩者皆有,以及標籤挑選/issue 建立/PR 回貼連結/相依 API 失敗等路徑尚未試煉,無法確認「無問題完全靜默」「有問題才建 issue」及相依失敗僅降級等契約成立。",
|
|
||||||
"suggestion": "以參數化測試覆蓋 findings 四種組合,斷言 API 呼叫順序/次數/issue number/統計;並分別讓 listLabels/selectLabels/createIssue/問題留言/PR 連結留言/addIssueDependency 拋錯,驗證哪些中止、哪些僅記警告續行;零 findings 時斷言所有寫入 API 皆不呼叫。屬測試架構決策。",
|
|
||||||
"suggestedCode": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "F006",
|
|
||||||
"reviewer": "Maya",
|
|
||||||
"focus": "testing",
|
|
||||||
"badge": "🧪",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/lib/gitrepo.js",
|
|
||||||
"startLine": 103,
|
|
||||||
"endLine": 157,
|
|
||||||
"problem": "resolveMergeBase 新增多階段 fetch/重試策略,卻沒有測試鎖定淺層與失敗路徑:首次成功提早返回、非 shallow 不 unshallow、某次 fetch 失敗後仍嘗試下一策略、補抓成功立即停止、全部失敗時診斷與 cause 是否完整;易因呼叫順序或 off-by-one 在 runner 上才暴露。",
|
|
||||||
"suggestion": "將 git 執行器注入或 stub,建立表格化案例覆蓋首次成功/unshallow 後成功/deepen base 後成功/deepen HEAD 後成功/各 fetch 個別失敗/全部失敗;逐案斷言 git 參數與順序、成功後不再額外 fetch,並檢查最終錯誤含各策略成敗摘要且保留首次錯誤為 cause。屬可測性重構+測試架構決策。",
|
|
||||||
"suggestedCode": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "F007",
|
|
||||||
"reviewer": "Maya",
|
|
||||||
"focus": "testing",
|
|
||||||
"badge": "🧪",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/lib/gitrepo.js",
|
|
||||||
"startLine": 249,
|
|
||||||
"endLine": 289,
|
|
||||||
"problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)",
|
|
||||||
"suggestion": "mock git 執行器與 URL/env 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。",
|
|
||||||
"suggestedCode": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"excluded": []
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
{
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
{
|
|
||||||
"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 App/Gitea App token,並明確禁止在不可信 fork PR 傳入可寫 token。若目標只是回報檢查結果,優先用 status/check API 寫結果,不要靠 PAT push 再觸發下一輪 CI。",
|
|
||||||
"suggestedCode": "```\ndescription: 'Gitea API token(PR/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": []
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
{
|
|
||||||
"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 已涵蓋 pushWithCredential/commitAndPushFindings 的認證推送策略、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,204 +0,0 @@
|
|||||||
{
|
|
||||||
"generatedAt": "2026/07/21 14:42:39",
|
|
||||||
"commitSha": "d424447d1502f49538fcb3093d23b5ac7bfa16c5",
|
|
||||||
"prNumber": 6,
|
|
||||||
"tool": {
|
|
||||||
"name": "codex",
|
|
||||||
"version": "codex-cli 0.144.6",
|
|
||||||
"model": "gpt-5.5"
|
|
||||||
},
|
|
||||||
"findings": [
|
|
||||||
{
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "style",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/lib/gitrepo.js",
|
|
||||||
"startLine": 139,
|
|
||||||
"endLine": 160,
|
|
||||||
"problem": "`runFetch`、`tryMergeBase`、`firstError`、`strategies` 夾在 `resolveMergeBase` 主旋律中,使這個函式同時負責驗證、fetch 策略編排、診斷文字組裝與錯誤包裝。即使邏輯可行,閱讀節奏已偏密,維護者很難一眼分辨「主要流程」與「補救策略」。",
|
|
||||||
"suggestion": "建議將 fetch 策略與診斷收集抽成小函式,例如 `fetchAndRecord`、`resolveMergeBaseWithStrategies`,讓 `resolveMergeBase` 保留高階流程:驗證 baseRef → fetch base → 嘗試 merge-base → 補抓歷史。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F003",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": false,
|
|
||||||
"reason": "保留。歷史 findings 主要涵蓋 resolveMergeBase 缺測試與診斷不足,本條指向函式內策略編排、診斷組裝與錯誤包裝混雜的可維護性問題,未被既有排除事項完整涵蓋。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"excluded": [
|
|
||||||
{
|
|
||||||
"reviewer": "Bard",
|
|
||||||
"focus": "style",
|
|
||||||
"badge": "🎼",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 179,
|
|
||||||
"endLine": 212,
|
|
||||||
"problem": "`queueOrPostComment` 與 `createIssueAndFlushBufferedComments` 這兩個閉包名稱像兩段不同旋律:一個強調「排隊或發布」,另一個卻把「建立 issue、flush 暫存留言、設定狀態」全塞進名稱與實作。讀者要來回追 `trackingIssue`、`pendingIssueCommentBodies`,節奏偏長且語意負擔重。",
|
|
||||||
"suggestion": "建議統一命名語彙,例如把「暫存/發布」集中成 `postReviewComment`、`flushPendingIssueComments`,讓函式名稱只描述一件事;建立 issue 與 flush 留言也可拆成兩段,讀起來會更像樂句而不是長句。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F002",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": true,
|
|
||||||
"reason": "可排除(重複)。歷史 finding 已指出 main() 內新增閉包與 issue/comment 路由細節讓主流程閱讀負擔加重;本條只是改以目前函式名稱描述同一維護性問題。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Leo",
|
|
||||||
"focus": "maintainability",
|
|
||||||
"badge": "🧰",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 165,
|
|
||||||
"endLine": 213,
|
|
||||||
"problem": "`main()` 這次把「留言目的地切換、issue 建立、暫存留言 flush、標籤挑選前置狀態」都塞進閉包與可變狀態(`pendingIssueCommentBodies`、`trackingIssue`、`currentRunCommentIds`)。半年後要改建問題模式時,維護者得同時追蹤主流程步驟、閉包副作用與留言落點,任何新增留言點都可能忘記處理 queue/flush 或 PR 排除清單,長期會讓流程編排變得很脆弱。",
|
|
||||||
"suggestion": "把留言目的地抽成明確的小型物件或模組,例如 `ReviewCommentSink` / `IssueCommentSink`,由它負責 `post()`、`flush()`、`currentRunCommentIds`。`main()` 只保留流程順序,不直接管理留言暫存與 issue 狀態。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F006",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": true,
|
|
||||||
"reason": "可排除(命中已知排除事項且重複)。已知與歷史 findings 已涵蓋 main() 內留言目的地、issue 狀態、緩衝佇列與可變閉包耦合,及應抽出發布器/sink 的方向。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Leo",
|
|
||||||
"focus": "maintainability",
|
|
||||||
"badge": "🧰",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 302,
|
|
||||||
"endLine": 353,
|
|
||||||
"problem": "建問題模式的收束邏輯把「挑標籤、建立 issue、發 finding、回貼 PR、設定 issue dependency、決定是否阻擋合併」全部展開在 `main()`。這段和前面的 `createIssueAndFlushBufferedComments` 共同組成一條隱性的 issue workflow,但邊界沒有被封裝;未來要新增 issue 模板、改排序、改阻擋條件或重試策略時,會在主流程裡到處補條件,維護成本會快速上升。",
|
|
||||||
"suggestion": "把建問題模式整理成單一高階函式,例如 `review.publishIssueModeResults({ ctx, gitea, tool, model, cwd, kept, severe, others, bufferedComments })`,主流程只接收 `trackingIssue` / `filesToCommit` 等結果。這樣 PR 模式與 issue 模式的責任邊界會清楚很多。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F007",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": true,
|
|
||||||
"reason": "可排除(重複)。此條與 F006 及歷史 findings 同樣指向建問題模式在 main() 中承擔 issue workflow、發布分流與狀態管理,屬同一維護性問題的延伸描述。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Leo",
|
|
||||||
"focus": "maintainability",
|
|
||||||
"badge": "🧰",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "readme.md",
|
|
||||||
"startLine": 56,
|
|
||||||
"endLine": 132,
|
|
||||||
"problem": "README 內大量維護到原始碼行號的連結,這次 diff 只是分支與行號位移就需要同步改一整片表格。這種文件和程式碼行號的硬耦合很容易在後續修改時過期,讀者點到錯誤位置,維護者也要花時間做機械式同步。",
|
|
||||||
"suggestion": "若這份表格是 API 索引,建議改成自動產生,或至少移除 `#Lxx` 行號錨點,改連到檔案或穩定章節錨點。讓文件描述 API,而不是跟著每次程式碼行號漂移。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F008",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": true,
|
|
||||||
"reason": "可排除(重複)。歷史 findings 已多次指出 README 功能表硬編分支與行號連結,導致文件與原始碼高度耦合且容易失準。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Mage",
|
|
||||||
"focus": "logic",
|
|
||||||
"badge": "🔮",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 251,
|
|
||||||
"endLine": 253,
|
|
||||||
"problem": "無可審查變更的分支在一般模式下會先把舊留言標為過時,之後才保存 findings 與 commit/push 結果。最小重現:`files.length === 0`、`nothingToReviewComment` 成功、`resolveOldComments` 成功,但接著 `saveFindings` 因檔案系統錯誤或 `commitFindings` 因 push 失敗拋例外。此時舊審查結果已被標過時,但沒有成功落地本回合的 success 結果 commit,狀態會停在半更新。",
|
|
||||||
"suggestion": "把 `review.resolveOldComments` 延後到 `saveFindings` 與必要的 `commitFindings` 成功之後;或至少在空變更路徑也遵守同一個交易順序:先產生並提交本回合結果,再清理舊留言。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F010",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": true,
|
|
||||||
"reason": "可排除(命中已知排除事項)。維護者已裁示舊留言應刻意在本回合結果發布前標為過時;本條要求延後清理,落在同一已排除範圍。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Maya",
|
|
||||||
"focus": "testing",
|
|
||||||
"badge": "🧪",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/index.js",
|
|
||||||
"startLine": 160,
|
|
||||||
"endLine": 353,
|
|
||||||
"problem": "建問題模式的主流程被大幅改寫,但目前測試只驗證了少數 helper,沒有驗證「留言先暫存、確定有 kept finding 才建 issue、無保留問題時靜默通過、嚴重問題才加 issue dependency、最後回貼 PR 連結」這些新增流程。這些行為一旦順序或條件寫錯,測試不會擋下來。",
|
|
||||||
"suggestion": "請補 `main()` 層級的流程測試,stub `agents`、`review`、`gitea`、`gitrepo`,至少覆蓋:`createIssue=true && kept=[]` 不建立 issue/不貼 PR 留言;`kept` 只有警告/建議時建立 issue、flush 暫存留言、回貼 PR 連結但不呼叫 `addIssueDependency`;`kept` 含嚴重時呼叫 `addIssueDependency`,且 dependency 失敗時流程仍完成並 commit failure findings。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F011",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": true,
|
|
||||||
"reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式 main() 層級流程缺測試,包括暫存留言、有 finding 才建 issue、無 finding 靜默、PR 回貼、嚴重與非嚴重分流及 dependency 失敗降級。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Maya",
|
|
||||||
"focus": "testing",
|
|
||||||
"badge": "🧪",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/lib/gitrepo.js",
|
|
||||||
"startLine": 307,
|
|
||||||
"endLine": 361,
|
|
||||||
"problem": "`pushWithCredential` 新增了關鍵認證行為,但沒有測試驗證它真的用 `GIT_CONFIG_*` 清掉 checkout 的 extraheader、再注入 PAT Authorization,也沒有測試遠端 URL 不符與 push 失敗時不洩漏 URL/token 的失敗路徑。這段是結果 commit 能否觸發下一輪 CI 的核心,沒有測試等於這個新契約還沒通過試煉。",
|
|
||||||
"suggestion": "請補推送行為測試。可用可注入的 exec wrapper 或子程序測試方式驗證:`git push` argv 不含 token、env 包含兩筆同 scope extraheader、`GIT_CONFIG_VALUE_0` 為空值、`GIT_CONFIG_VALUE_1` 為 Basic header;再補 `remote.origin !== server.origin` 與 exec 失敗時錯誤訊息不含 token/remoteUrl 的案例。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F012",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": true,
|
|
||||||
"reason": "可排除(重複)。歷史 findings 已記錄 pushWithCredential/pushToken 認證推送路徑缺少測試,包含 token 不進 argv、認證遮蔽、推送目標與失敗訊息不洩漏敏感資訊。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Maya",
|
|
||||||
"focus": "testing",
|
|
||||||
"badge": "🧪",
|
|
||||||
"severity": "警告",
|
|
||||||
"file": "src/lib/gitrepo.js",
|
|
||||||
"startLine": 130,
|
|
||||||
"endLine": 192,
|
|
||||||
"problem": "`resolveMergeBase` 新增多段 fetch fallback:先抓 base、再 deepen base、deepen PR HEAD、最後 shallow repo 才 unshallow,但現有測試只驗證不安全 baseRef 會被拒絕,沒有驗證淺層歷史補抓順序、每次 fetch 後會重試 merge-base、成功後會短路,也沒有覆蓋所有策略失敗時的診斷錯誤。",
|
|
||||||
"suggestion": "請補針對 `resolveMergeBase` 的失敗與邊界測試。建議把 git 執行函式抽成可替換依賴,或用臨時 git repo 模擬 shallow 狀態,斷言:首次 merge-base 成功不走 fallback;deepen base 成功後立即回傳;deepen PR HEAD 使用目前 HEAD SHA 而不是遠端 `HEAD`;非 shallow repo 不呼叫 `--unshallow`;全部失敗時錯誤包含策略診斷。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F013",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": true,
|
|
||||||
"reason": "可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、成功短路、降級順序與最終失敗診斷缺測試提出相同問題。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"reviewer": "Maya",
|
|
||||||
"focus": "testing",
|
|
||||||
"badge": "🧪",
|
|
||||||
"severity": "建議",
|
|
||||||
"file": "src/lib/diagnostics.js",
|
|
||||||
"startLine": 18,
|
|
||||||
"endLine": 69,
|
|
||||||
"problem": "`redactSecrets` 與 `agentFailureDetail` 是新加入的安全診斷防線,但測試只覆蓋 Authorization/token 的基本遮罩。控制字元單行化、URL 內嵌帳密、長 token/hex、輸入與輸出長度截斷、以及沒有 error code 時的 fallback 訊息都還沒被驗證。",
|
|
||||||
"suggestion": "請補診斷邊界測試,至少包含:含換行與 tab 的輸出不會產生多行 log;`https://user:pass@host` 會被遮罩;40 字元以上 token 會被遮罩;debug 模式下 stderr/stdout 只輸出到限制長度;沒有 `error.code`、`signal`、`stderr/stdout` 時仍回傳安全的 fallback 訊息。",
|
|
||||||
"suggestedCode": "",
|
|
||||||
"id": "F014",
|
|
||||||
"verdicts": {
|
|
||||||
"Paladin": {
|
|
||||||
"exclude": true,
|
|
||||||
"reason": "可排除(重複)。歷史 findings 已涵蓋 redactSecrets/agentFailureDetail 對控制字元、URL 帳密、token 格式、截斷與 fallback 診斷等邊界缺少測試。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,110 +1,45 @@
|
|||||||
# ============================================================================
|
|
||||||
# 用途:在 pull request 針對 develop 分支開啟或同步時,分別以 Antigravity、Codex、Claude 環境執行本機 AI Code Review action。
|
|
||||||
# 更新時間:2026/07/17 18:49:58
|
|
||||||
# ============================================================================
|
|
||||||
# Workflow 顯示名稱。
|
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
# Workflow 觸發條件區塊。
|
|
||||||
on:
|
on:
|
||||||
# 以 pull request 事件觸發。
|
|
||||||
pull_request:
|
pull_request:
|
||||||
# 限定 pull request 目標分支。
|
|
||||||
branches:
|
branches:
|
||||||
# 僅在目標分支為 develop 時執行。
|
|
||||||
- develop
|
- develop
|
||||||
# 限定 pull request 開啟與同步更新時執行。
|
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
|
||||||
# Job 定義區塊。
|
|
||||||
jobs:
|
jobs:
|
||||||
# Antigravity 工具環境測試 job。
|
|
||||||
test-antigravity:
|
test-antigravity:
|
||||||
# Job 在 workflow UI 顯示的名稱。
|
|
||||||
name: TEST (Antigravity)
|
name: TEST (Antigravity)
|
||||||
# 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。
|
|
||||||
runs-on: ubuntu
|
runs-on: ubuntu
|
||||||
# Job 執行步驟。
|
|
||||||
steps:
|
steps:
|
||||||
# 取得存取庫內容供後續 action 使用。
|
|
||||||
- name: 取得存取庫資訊
|
- name: 取得存取庫資訊
|
||||||
# 使用呼叫端 vars 指定的 checkout action 版本。
|
|
||||||
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
|
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
|
||||||
# 安裝 Antigravity 工具環境。
|
|
||||||
- name: 安裝工具
|
- name: 安裝工具
|
||||||
# 使用 Gitea composite action 安裝 Antigravity,版本由 vars 指定。
|
|
||||||
uses: https://gitea.jsc.idv.tw/composite-actions/setup-antigravity@${{ vars.ACTION_SETUP_ANTIGRAVITY_VERSION }}
|
uses: https://gitea.jsc.idv.tw/composite-actions/setup-antigravity@${{ vars.ACTION_SETUP_ANTIGRAVITY_VERSION }}
|
||||||
with:
|
|
||||||
oauth: ${{ secrets.ANTIGRAVITY_OAUTH }}
|
|
||||||
# 執行目前存取庫的 AI Code Review action。
|
|
||||||
- name: 程式碼審查
|
- name: 程式碼審查
|
||||||
# 以目前存取庫根目錄的 action.yml 作為 action 來源。
|
|
||||||
uses: ./
|
uses: ./
|
||||||
# 傳入 action inputs。
|
|
||||||
with:
|
with:
|
||||||
# PR 留言與 findings 寫回用的 token(呼叫端以 secrets 傳入)。
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
token: ${{ secrets.TOKEN }}
|
|
||||||
# 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。
|
|
||||||
create-issue: 'true'
|
|
||||||
# 指定 AI 模型:CI 測試固定用 Antigravity 最省模型以降低消耗。
|
|
||||||
model: gemini-3.5-flash
|
|
||||||
# Codex 工具環境測試 job。
|
|
||||||
test-codex:
|
test-codex:
|
||||||
# Job 在 workflow UI 顯示的名稱。
|
|
||||||
name: TEST (Codex)
|
name: TEST (Codex)
|
||||||
# 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。
|
|
||||||
runs-on: ubuntu
|
runs-on: ubuntu
|
||||||
# Job 執行步驟。
|
|
||||||
steps:
|
steps:
|
||||||
# 取得存取庫內容供後續 action 使用。
|
|
||||||
- name: 取得存取庫資訊
|
- name: 取得存取庫資訊
|
||||||
# 使用呼叫端 vars 指定的 checkout action 版本。
|
|
||||||
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
|
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
|
||||||
# 安裝 Codex 工具環境。
|
|
||||||
- name: 安裝工具
|
- name: 安裝工具
|
||||||
# 使用 Gitea composite action 安裝 Codex,版本由 vars 指定。
|
|
||||||
uses: https://gitea.jsc.idv.tw/composite-actions/setup-codex@${{ vars.ACTION_SETUP_CODEX_VERSION }}
|
uses: https://gitea.jsc.idv.tw/composite-actions/setup-codex@${{ vars.ACTION_SETUP_CODEX_VERSION }}
|
||||||
with:
|
|
||||||
oauth: ${{ secrets.CODEX_OAUTH }}
|
|
||||||
# 執行目前存取庫的 AI Code Review action。
|
|
||||||
- name: 程式碼審查
|
- name: 程式碼審查
|
||||||
# 以目前存取庫根目錄的 action.yml 作為 action 來源。
|
|
||||||
uses: ./
|
uses: ./
|
||||||
# 傳入 action inputs。
|
|
||||||
with:
|
with:
|
||||||
# PR 留言與 findings 寫回用的 token(呼叫端以 secrets 傳入)。
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
token: ${{ secrets.TOKEN }}
|
|
||||||
# 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。
|
|
||||||
create-issue: 'true'
|
|
||||||
# 指定 AI 模型:CI 測試固定用 Codex 最省模型以降低消耗。
|
|
||||||
model: gpt-5.5
|
|
||||||
# Claude 工具環境測試 job。
|
|
||||||
test-claude:
|
test-claude:
|
||||||
# Job 在 workflow UI 顯示的名稱。
|
|
||||||
name: TEST (Claude)
|
name: TEST (Claude)
|
||||||
# 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。
|
|
||||||
runs-on: ubuntu
|
runs-on: ubuntu
|
||||||
# Job 執行步驟。
|
|
||||||
steps:
|
steps:
|
||||||
# 取得存取庫內容供後續 action 使用。
|
|
||||||
- name: 取得存取庫資訊
|
- name: 取得存取庫資訊
|
||||||
# 使用呼叫端 vars 指定的 checkout action 版本。
|
|
||||||
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
|
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
|
||||||
# 安裝 Claude 工具環境。
|
|
||||||
- name: 安裝工具
|
- name: 安裝工具
|
||||||
# 使用 Gitea composite action 安裝 Claude,版本由 vars 指定。
|
|
||||||
uses: https://gitea.jsc.idv.tw/composite-actions/setup-claude@${{ vars.ACTION_SETUP_CLAUDE_VERSION }}
|
uses: https://gitea.jsc.idv.tw/composite-actions/setup-claude@${{ vars.ACTION_SETUP_CLAUDE_VERSION }}
|
||||||
with:
|
|
||||||
oauth: ${{ secrets.CLAUDE_OAUTH }}
|
|
||||||
# 執行目前存取庫的 AI Code Review action。
|
|
||||||
- name: 程式碼審查
|
- name: 程式碼審查
|
||||||
# 以目前存取庫根目錄的 action.yml 作為 action 來源。
|
|
||||||
uses: ./
|
uses: ./
|
||||||
# 傳入 action inputs。
|
|
||||||
with:
|
with:
|
||||||
# PR 留言與 findings 寫回用的 token(呼叫端以 secrets 傳入)。
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
token: ${{ secrets.TOKEN }}
|
|
||||||
# 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。
|
|
||||||
create-issue: 'true'
|
|
||||||
# 指定 AI 模型:Claude job 使用 Opus 4.8。
|
|
||||||
model: claude-opus-4-8
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
# Workflow 文件草稿
|
|
||||||
|
|
||||||
更新時間:2026/07/17 18:49:58
|
|
||||||
|
|
||||||
## Workflow 總覽
|
|
||||||
|
|
||||||
| 名稱 | 檔案位置 | 用途 | 觸發條件 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| CI | `.gitea/workflows/ci.yaml` | 在 PR 事件中分別以 Antigravity、Codex、Claude 工具環境執行本存取庫的 AI Code Review action,驗證 action 可在不同 AI 工具環境下運作。 | `pull_request` 目標分支為 `develop`,事件類型為 `opened` 或 `synchronize`。 |
|
|
||||||
|
|
||||||
## CI
|
|
||||||
|
|
||||||
- Workflow 名稱:`CI`
|
|
||||||
- 檔案位置:`.gitea/workflows/ci.yaml`
|
|
||||||
- 用途:針對送往 `develop` 的 pull request 執行三組測試 job,分別安裝 Antigravity、Codex、Claude 環境後呼叫 `uses: ./` 執行目前 action。
|
|
||||||
- 觸發條件:`pull_request.branches` 為 `develop`;`pull_request.types` 為 `opened`、`synchronize`。
|
|
||||||
|
|
||||||
## Job
|
|
||||||
|
|
||||||
| Job ID | 顯示名稱 | Runner | 主要流程 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `test-antigravity` | `TEST (Antigravity)` | `ubuntu` | checkout 存取庫、安裝 Antigravity、執行本地 action。 |
|
|
||||||
| `test-codex` | `TEST (Codex)` | `ubuntu` | checkout 存取庫、安裝 Codex、執行本地 action。 |
|
|
||||||
| `test-claude` | `TEST (Claude)` | `ubuntu` | checkout 存取庫、安裝 Claude、執行本地 action。 |
|
|
||||||
|
|
||||||
## 主要輸入 / 環境參數
|
|
||||||
|
|
||||||
| 名稱 | 來源 | 使用位置 | 說明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `vars.ACTION_CHECKOUT_VERSION` | Gitea / GitHub repository 或 organization vars | `actions/checkout@...` | 指定 checkout action 版本。 |
|
|
||||||
| `vars.ACTION_SETUP_ANTIGRAVITY_VERSION` | Gitea / GitHub vars | `setup-antigravity@...` | 指定 Antigravity 安裝 action 版本。 |
|
|
||||||
| `vars.ACTION_SETUP_CODEX_VERSION` | Gitea / GitHub vars | `setup-codex@...` | 指定 Codex 安裝 action 版本。 |
|
|
||||||
| `vars.ACTION_SETUP_CLAUDE_VERSION` | Gitea / GitHub vars | `setup-claude@...` | 指定 Claude 安裝 action 版本。 |
|
|
||||||
| `secrets.GITHUB_TOKEN` | Gitea / GitHub secrets | 本地 action input `token` | 提供 action 呼叫 Gitea API 留言與寫回 findings 所需 token。 |
|
|
||||||
|
|
||||||
## 注意事項
|
|
||||||
|
|
||||||
- `.gitea/workflows/readme.md` 目前不存在;本檔為新增 workflow README 的草稿。
|
|
||||||
- `runs-on: ubuntu` 是否符合實際 Gitea runner 標籤需人工確認。
|
|
||||||
- `vars.*` 與 `secrets.GITHUB_TOKEN` 必須由呼叫端環境提供,否則 checkout、工具安裝或程式碼審查步驟可能失敗。
|
|
||||||
- 三個 job 均以 `uses: ./` 呼叫目前存取庫根目錄的 `action.yml`;因此 `action.yml` 的 `runs.main` 需保持可被 runner 直接執行。
|
|
||||||
+27
-27
@@ -1,53 +1,53 @@
|
|||||||
# ============================================================================
|
# =====================================================
|
||||||
# 用途:定義 AI Code Review Node action 的名稱、輸入參數與 Node.js 24 進入點,供 Gitea / GitHub workflow 以 uses 引用。
|
# 用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings
|
||||||
# 更新時間:2026/07/17 18:49:58
|
# 更新時間: 2026/07/17 16:49:21
|
||||||
# ============================================================================
|
# =====================================================
|
||||||
# Gitea / GitHub node action 的 manifest(action.yml):
|
# Gitea / GitHub node action 的 manifest(action.yml):
|
||||||
# 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs),
|
# 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs),
|
||||||
# 供呼叫端 workflow 以 `uses:` 引用;runner 讀取此檔後以 node24 執行 src/index.js。
|
# 供呼叫端 workflow 以 `uses:` 引用;runner 讀取此檔後以 node24 執行 src/index.js。
|
||||||
|
|
||||||
# action 顯示名稱:呼叫端 workflow log 與 marketplace 列表上看到的名稱。
|
# action 顯示名稱:呼叫端 workflow log 與 marketplace 列表上看到的名稱
|
||||||
name: 'AI Code Review'
|
name: 'AI Code Review'
|
||||||
# action 用途說明:多角色 AI code review 流程(攻擊方找問題、防守方裁決誤報),
|
# action 用途說明:多角色 AI code review 流程(攻擊方找問題、防守方裁決誤報),
|
||||||
# 審查結果會留言到 PR 並保存 findings(.gitea/ai-review/findings/)。
|
# 審查結果會留言到 PR 並保存 findings(.gitea/ai-review/findings/)
|
||||||
description: 'AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings'
|
description: 'AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings'
|
||||||
# action 作者資訊(僅供辨識,不影響執行)。
|
# action 作者資訊(僅供辨識,不影響執行)
|
||||||
author: 'Jeffery'
|
author: 'Jeffery'
|
||||||
# 輸入參數區塊:呼叫端 workflow 以 `with:` 傳入,
|
# 輸入參數區塊:呼叫端 workflow 以 `with:` 傳入,
|
||||||
# runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取。
|
# runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取
|
||||||
inputs:
|
inputs:
|
||||||
# Gitea API token:用於對 PR/issue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。
|
# Gitea API token:用於對 PR 留言審查結果、以及 push 審查結果檔回 repo
|
||||||
token:
|
token:
|
||||||
# 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。
|
# 參數用途說明:secrets/vars context 在 action 內不可用,
|
||||||
# 建議傳入能觸發 CI 的 PAT;自動 token 推送結果 commit 時可能不會再觸發 workflow。
|
# 故由呼叫端 workflow 以 secrets.GITHUB_TOKEN 傳入
|
||||||
description: 'Gitea API token(PR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)'
|
description: 'Gitea API token(PR 留言與 push findings 用;呼叫端以 secrets.GITHUB_TOKEN 傳入)'
|
||||||
# 必填:缺少 token 無法呼叫 Gitea API,action 無法運作。
|
# 必填:缺少 token 無法呼叫 Gitea API,action 無法運作
|
||||||
required: true
|
required: true
|
||||||
# 指定 AI 工具使用的模型名稱。
|
# 指定 AI 工具使用的模型名稱
|
||||||
model:
|
model:
|
||||||
# 參數用途說明:留空表示使用各 AI 工具自身的預設模型。
|
# 參數用途說明:留空表示使用各 AI 工具自身的預設模型
|
||||||
description: '指定 AI 工具使用的模型(空值=各工具預設)'
|
description: '指定 AI 工具使用的模型(空值=各工具預設)'
|
||||||
# 選填:未指定時採用預設值。
|
# 選填:未指定時採用預設值
|
||||||
required: false
|
required: false
|
||||||
# 預設為空字串,代表不覆寫各工具的預設模型。
|
# 預設為空字串,代表不覆寫各工具的預設模型
|
||||||
default: ''
|
default: ''
|
||||||
# 建問題模式開關:是否把審查保留的問題另建 issue 追蹤。
|
# 建問題模式開關:是否把審查保留的問題另建 issue 追蹤
|
||||||
create-issue:
|
create-issue:
|
||||||
# 參數用途說明:字串 'true' 時建立 issue(標題=PR 標題、描述=PR 描述、AI 挑標籤),
|
# 參數用途說明:字串 'true' 時建立 issue(標題=PR 標題、描述=PR 描述、AI 挑標籤)
|
||||||
# 並逐條留言問題明細,findings 檔不進版控、收尾只 commit exclusions.json;
|
# 並逐條留言問題明細,findings 檔不進版控、收尾只 commit exclusions.json;
|
||||||
# 預設 'false' 走原流程(findings 檔與 exclusions.json 一併 commit 回 PR 來源分支)。
|
# 預設 'false' 走原流程(findings 檔與 exclusions.json 一併 commit 回 PR 來源分支)
|
||||||
description: '是否將問題建到存取庫的問題追蹤(true 時建立 issue 逐條留言問題明細,最後只 commit exclusions.json;預設 false 走原流程)'
|
description: '是否將問題建到存取庫的問題追蹤(true 時建立 issue 逐條留言問題明細,最後只 commit exclusions.json;預設 false 走原流程)'
|
||||||
# 選填:未指定時採用預設值。
|
# 選填:未指定時採用預設值
|
||||||
required: false
|
required: false
|
||||||
# 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用)。
|
# 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用)
|
||||||
default: 'false'
|
default: 'false'
|
||||||
# 執行方式區塊:宣告本 action 為 node action 及其進入點。
|
# 執行方式區塊:宣告本 action 為 node action 及其進入點
|
||||||
runs:
|
runs:
|
||||||
# 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite)。
|
# 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite)
|
||||||
using: 'node24'
|
using: 'node24'
|
||||||
# 主程式進入點:直接指向 src/index.js(entry point)。
|
# 主程式進入點:直接指向 src/index.js(entry point)
|
||||||
# 主程式為零外部相依(package.json 無 dependencies,src 僅 require Node 內建模組與本地 lib),
|
# 主程式為零外部相依(package.json 無 dependencies,src 僅 require Node 內建模組與本地 lib),
|
||||||
# runner 不會自動 npm install,零相依時依 node action 慣例 main 直接指向 src/index.js 即正確;
|
# runner 不會自動 npm install,零相依時依 node action 慣例 main 直接指向 src/index.js 即正確;
|
||||||
# 日後若新增外部相依,需改以 @vercel/ncc 打包(package.json 已備有 build script),
|
# 日後若新增外部相依,需改以 @vercel/ncc 打包(package.json 已備有 build script)
|
||||||
# 並將 main 改指 dist/index.js、把 dist/ commit 進 repo。
|
# 並將 main 改指 dist/index.js、把 dist/ commit 進 repo
|
||||||
main: 'src/index.js'
|
main: 'src/index.js'
|
||||||
|
|||||||
+1
-2
@@ -4,8 +4,7 @@
|
|||||||
"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"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# AI Code Review
|
# AI Code Review
|
||||||
|
|
||||||
> 更新時間:2026/07/17 18:49:58
|
> 更新時間:2026/07/17 16:49:21
|
||||||
|
|
||||||
AI 多角色 code review 的 Gitea **node action**(`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]`/`[failure]`)供下次觸發快速回報。
|
AI 多角色 code review 的 Gitea **node action**(`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]`/`[failure]`)供下次觸發快速回報。
|
||||||
|
|
||||||
@@ -38,16 +38,16 @@ jobs:
|
|||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
S1[1 判斷 bot commit 標記] -->|命中| E0[直接回報 success/failure]
|
S1[1 判斷 bot commit 標記] -->|命中| E0[直接回報 success/failure]
|
||||||
S1 -->|未命中| N3[3 偵測 AI 工具並留言]
|
S1 -->|未命中| S2[2 偵測 AI 工具並留言]
|
||||||
N3 --> N4[4 讀 .reviewignore 整理 diff 並留言]
|
S2 --> S3[3 讀 .reviewignore 整理 diff 並留言]
|
||||||
N4 --> N5[5 攻擊方登場留言]
|
S3 --> S4[4 攻擊方登場留言]
|
||||||
N5 --> N6[6 攻擊方 sub agent 並行找問題]
|
S4 --> S5[5 攻擊方 sub agent 並行找問題]
|
||||||
N6 --> N7[7 防守方登場留言]
|
S5 --> S6[6 防守方登場留言]
|
||||||
N7 --> N8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json]
|
S6 --> S7[7 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json]
|
||||||
N8 --> N2[2 延後將舊留言標記解決(成功產生結果後才執行)]
|
S7 --> S8[8 舊留言標記解決]
|
||||||
N2 --> N9[9 嚴重問題逐條掛行留言]
|
S8 --> S9[9 嚴重問題逐條掛行留言]
|
||||||
N9 --> N10[10 警告+建議彙整表格留言]
|
S9 --> S10[10 警告+建議彙整表格留言]
|
||||||
N10 --> E1[收尾 commit/push + exit code]
|
S10 --> E1[收尾 commit/push + exit code]
|
||||||
```
|
```
|
||||||
|
|
||||||
## 專案列表
|
## 專案列表
|
||||||
@@ -56,19 +56,19 @@ flowchart TD
|
|||||||
|
|
||||||
| 專案名稱 | 專案描述 |
|
| 專案名稱 | 專案描述 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | Gitea node action:提供台北時區日誌工具、runner 上下文載入、git diff/commit 操作、Gitea REST API 客戶端(留言/review/issue/標籤)、AI CLI 工具偵測與 sub agent 執行、角色提示載入、固定留言模板,以及多角色審查編排(攻擊方找問題、防守方裁決、findings 保存、誤判回寫、建問題模式) |
|
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | Gitea node action:提供台北時區日誌工具、runner 上下文載入、git diff/commit 操作、Gitea REST API 客戶端(留言/review/issue/標籤)、AI CLI 工具偵測與 sub agent 執行、角色提示載入、固定留言模板,以及多角色審查編排(攻擊方找問題、防守方裁決、findings 保存、誤判回寫、建問題模式) |
|
||||||
|
|
||||||
### 參考專案表
|
### 參考專案表
|
||||||
|
|
||||||
| 專案名稱 | 參考專案列表 |
|
| 專案名稱 | 參考專案列表 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | 無 |
|
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | 無 |
|
||||||
|
|
||||||
### NuGet 套件表
|
### NuGet 套件表
|
||||||
|
|
||||||
| 專案名稱 | NuGet 套件列表 |
|
| 專案名稱 | NuGet 套件列表 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | 無 |
|
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | 無 |
|
||||||
|
|
||||||
## 功能列表
|
## 功能列表
|
||||||
|
|
||||||
@@ -76,56 +76,56 @@ flowchart TD
|
|||||||
|
|
||||||
| 功能名稱 | 功能描述 |
|
| 功能名稱 | 功能描述 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L20) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) |
|
| [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L20) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) |
|
||||||
| [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L41) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) |
|
| [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L41) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) |
|
||||||
| [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) |
|
| [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) |
|
||||||
| [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) |
|
| [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) |
|
||||||
| [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) |
|
| [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) |
|
||||||
| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L79) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) |
|
| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L60) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) |
|
||||||
| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L100) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) |
|
| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L81) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) |
|
||||||
| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L136) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) |
|
| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L104) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) |
|
||||||
| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L157) | [取得單一檔案的 git diff 內容](#gitrepofilediff) |
|
| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L125) | [取得單一檔案的 git diff 內容](#gitrepofilediff) |
|
||||||
| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L175) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) |
|
| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L143) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) |
|
||||||
| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L213) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) |
|
| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L181) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) |
|
||||||
| [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) |
|
| [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) |
|
||||||
| [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) |
|
| [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) |
|
||||||
| [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) |
|
| [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) |
|
||||||
| [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L141) | [列出存取庫可用標籤](#gitealistlabels) |
|
| [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L141) | [列出存取庫可用標籤](#gitealistlabels) |
|
||||||
| [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L164) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) |
|
| [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L164) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) |
|
||||||
| [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L184) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) |
|
| [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L184) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) |
|
||||||
| [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L202) | [編輯既有一般留言](#giteaeditissuecomment) |
|
| [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L202) | [編輯既有一般留言](#giteaeditissuecomment) |
|
||||||
| [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L223) | [建立 code review 並掛行內留言](#giteacreatereview) |
|
| [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L223) | [建立 code review 並掛行內留言](#giteacreatereview) |
|
||||||
| [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L243) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) |
|
| [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L243) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) |
|
||||||
| [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L264) | [列出某 review 的全部行內留言](#gitealistreviewcomments) |
|
| [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L264) | [列出某 review 的全部行內留言](#gitealistreviewcomments) |
|
||||||
| [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L287) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) |
|
| [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L287) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) |
|
||||||
| [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L53) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) |
|
| [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L53) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) |
|
||||||
| [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L98) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) |
|
| [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L98) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) |
|
||||||
| [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L144) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) |
|
| [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L144) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) |
|
||||||
| [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) |
|
| [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) |
|
||||||
| [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) |
|
| [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) |
|
||||||
| [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) |
|
| [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) |
|
||||||
| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L88) | [產生步驟 3 審查工具留言](#templatestoolcomment) |
|
| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L88) | [產生步驟 2 審查工具留言](#templatestoolcomment) |
|
||||||
| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L132) | [產生步驟 4 變更摘要留言](#templatesdiffcomment) |
|
| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L132) | [產生步驟 3 變更摘要留言](#templatesdiffcomment) |
|
||||||
| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L173) | [產生步驟 5/7 角色登場留言](#templatesrolescomment) |
|
| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L173) | [產生步驟 4/6 角色登場留言](#templatesrolescomment) |
|
||||||
| [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) |
|
| [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) |
|
||||||
| [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) |
|
| [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) |
|
||||||
| [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) |
|
| [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) |
|
||||||
| [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L309) | [產生建問題模式的 issue 本文](#templatesissuebody) |
|
| [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L309) | [產生建問題模式的 issue 本文](#templatesissuebody) |
|
||||||
| [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L341) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) |
|
| [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L341) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) |
|
||||||
| [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L379) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) |
|
| [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L379) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) |
|
||||||
| [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L29) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) |
|
| [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L29) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) |
|
||||||
| [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L53) | [判斷檔案是否忽略不送審](#reviewisignored) |
|
| [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L53) | [判斷檔案是否忽略不送審](#reviewisignored) |
|
||||||
| [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L77) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) |
|
| [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L77) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) |
|
||||||
| [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L127) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) |
|
| [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L127) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) |
|
||||||
| [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L276) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) |
|
| [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L276) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) |
|
||||||
| [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L448) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) |
|
| [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L448) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) |
|
||||||
| [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L572) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) |
|
| [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L572) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) |
|
||||||
| [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L521) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) |
|
| [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L521) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) |
|
||||||
| [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L596) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) |
|
| [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L596) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) |
|
||||||
| [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L627) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) |
|
| [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L627) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) |
|
||||||
| [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L694) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) |
|
| [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L694) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) |
|
||||||
| [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L778) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) |
|
| [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L778) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) |
|
||||||
| [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L855) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) |
|
| [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L855) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) |
|
||||||
|
|
||||||
## 使用範例
|
## 使用範例
|
||||||
|
|
||||||
@@ -168,8 +168,8 @@ taipeiFromIso(''); // '—'
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
const { log } = require('./src/lib/log');
|
const { log } = require('./src/lib/log');
|
||||||
log('步驟4', 'INF', '變更檔案 5 個,送審 3 個。');
|
log('步驟3', 'INF', '變更檔案 5 個,送審 3 個。');
|
||||||
// [2026/07/17 16:46:13][步驟4][INF]: 變更檔案 5 個,送審 3 個。
|
// [2026/07/17 16:46:13][步驟3][INF]: 變更檔案 5 個,送審 3 個。
|
||||||
```
|
```
|
||||||
|
|
||||||
<a id="contextloadcontext"></a>
|
<a id="contextloadcontext"></a>
|
||||||
@@ -246,7 +246,7 @@ const committed = gitrepo.commitAndPushFindings(cwd, {
|
|||||||
<a id="giteawhoami"></a>
|
<a id="giteawhoami"></a>
|
||||||
### gitea.whoAmI
|
### gitea.whoAmI
|
||||||
|
|
||||||
取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 2 以 `login` 比對留言作者辨識本 action 發過的留言。
|
取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 8 以 `login` 比對留言作者辨識本 action 發過的留言。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const gitea = require('./src/lib/gitea');
|
const gitea = require('./src/lib/gitea');
|
||||||
@@ -269,7 +269,7 @@ await gitea.createCommentOnIssue(ctx, issue.number, '🔴 嚴重|...');
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
const created = await gitea.createIssueComment(ctx, '## 📋 變更摘要 ...');
|
const created = await gitea.createIssueComment(ctx, '## 📋 變更摘要 ...');
|
||||||
// created.id 記入本回合留言集合,步驟 2 標註過時時跳過
|
// created.id 記入本回合留言集合,步驟 8 標註過時時跳過
|
||||||
```
|
```
|
||||||
|
|
||||||
<a id="gitealistlabels"></a>
|
<a id="gitealistlabels"></a>
|
||||||
@@ -294,7 +294,7 @@ const issue = await gitea.createIssue(ctx, { title: 'PR 標題', body: '…', la
|
|||||||
<a id="gitealistissuecomments"></a>
|
<a id="gitealistissuecomments"></a>
|
||||||
### gitea.listIssueComments
|
### gitea.listIssueComments
|
||||||
|
|
||||||
列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 2 據此找出 bot 舊留言標註〔已過時〕。
|
列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 8 據此找出 bot 舊留言標註〔已過時〕。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const comments = await gitea.listIssueComments(ctx);
|
const comments = await gitea.listIssueComments(ctx);
|
||||||
@@ -303,7 +303,7 @@ const comments = await gitea.listIssueComments(ctx);
|
|||||||
<a id="giteaeditissuecomment"></a>
|
<a id="giteaeditissuecomment"></a>
|
||||||
### gitea.editIssueComment
|
### gitea.editIssueComment
|
||||||
|
|
||||||
以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 2 用來替舊留言加上〔已過時〕前綴。
|
以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 8 用來替舊留言加上〔已過時〕前綴。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await gitea.editIssueComment(ctx, comment.id, `> 〔已過時〕…\n\n${comment.body}`);
|
await gitea.editIssueComment(ctx, comment.id, `> 〔已過時〕…\n\n${comment.body}`);
|
||||||
@@ -323,7 +323,7 @@ await gitea.createReview(ctx, '## 🔴 嚴重問題(共 2 條)…', [
|
|||||||
<a id="gitealistreviews"></a>
|
<a id="gitealistreviews"></a>
|
||||||
### gitea.listReviews
|
### gitea.listReviews
|
||||||
|
|
||||||
列出 PR 全部 review(自動分頁);步驟 2 據此逐一取出行內留言嘗試解決。
|
列出 PR 全部 review(自動分頁);步驟 8 據此逐一取出行內留言嘗試解決。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const reviews = await gitea.listReviews(ctx);
|
const reviews = await gitea.listReviews(ctx);
|
||||||
@@ -411,7 +411,7 @@ const defenders = defendersOf(roles); // [Paladin]
|
|||||||
<a id="templatestoolcomment"></a>
|
<a id="templatestoolcomment"></a>
|
||||||
### templates.toolComment
|
### templates.toolComment
|
||||||
|
|
||||||
產生步驟 3 的審查工具留言:工具/版本/模型/審查 commit/Run Job 連結表格+審查管線 mermaid 流程圖;開頭含隱藏標記供步驟 2 辨識。
|
產生步驟 2 的審查工具留言:工具/版本/模型/審查 commit/Run Job 連結表格+審查管線 mermaid 流程圖;開頭含隱藏標記供步驟 8 辨識。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const body = templates.toolComment({
|
const body = templates.toolComment({
|
||||||
@@ -424,7 +424,7 @@ const body = templates.toolComment({
|
|||||||
<a id="templatesdiffcomment"></a>
|
<a id="templatesdiffcomment"></a>
|
||||||
### templates.diffComment
|
### templates.diffComment
|
||||||
|
|
||||||
產生步驟 4 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。
|
產生步驟 3 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const body = templates.diffComment(diffRows, ignoredCount);
|
const body = templates.diffComment(diffRows, ignoredCount);
|
||||||
@@ -433,7 +433,7 @@ const body = templates.diffComment(diffRows, ignoredCount);
|
|||||||
<a id="templatesrolescomment"></a>
|
<a id="templatesrolescomment"></a>
|
||||||
### templates.rolesComment
|
### templates.rolesComment
|
||||||
|
|
||||||
產生步驟 5/7 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。
|
產生步驟 4/6 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const body = templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers });
|
const body = templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers });
|
||||||
@@ -534,7 +534,7 @@ await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows });
|
|||||||
<a id="reviewrunattackers"></a>
|
<a id="reviewrunattackers"></a>
|
||||||
### review.runAttackers
|
### review.runAttackers
|
||||||
|
|
||||||
步驟 6:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。
|
步驟 5:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows });
|
const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows });
|
||||||
@@ -543,7 +543,7 @@ const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attack
|
|||||||
<a id="reviewrundefenders"></a>
|
<a id="reviewrundefenders"></a>
|
||||||
### review.runDefenders
|
### review.runDefenders
|
||||||
|
|
||||||
步驟 8:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。
|
步驟 7:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
|
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
|
||||||
@@ -600,7 +600,7 @@ if (ctx.createIssue && kept.length > 0) {
|
|||||||
<a id="reviewresolveoldcomments"></a>
|
<a id="reviewresolveoldcomments"></a>
|
||||||
### review.resolveOldComments
|
### review.resolveOldComments
|
||||||
|
|
||||||
步驟 2:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。
|
步驟 8:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
||||||
|
|||||||
+69
-219
@@ -4,7 +4,7 @@
|
|||||||
console.log('================================================');
|
console.log('================================================');
|
||||||
console.log('Action : AI Code Review');
|
console.log('Action : AI Code Review');
|
||||||
console.log('用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings');
|
console.log('用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings');
|
||||||
console.log('更新時間: 2026/07/17 18:49:58');
|
console.log('更新時間: 2026/07/17 16:49:21');
|
||||||
console.log('================================================');
|
console.log('================================================');
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -42,9 +42,9 @@ const BOT_COMMIT_PREFIX = 'chore: update ai-review findings [ai-review-bot]';
|
|||||||
* @param {Array<Object>} params.excluded - 被裁決為誤報而排除的問題清單。
|
* @param {Array<Object>} params.excluded - 被裁決為誤報而排除的問題清單。
|
||||||
* @returns {string} findings JSON 檔相對於 repo 根目錄的路徑(例如 `.gitea/ai-review/findings/xxx.json`)。
|
* @returns {string} findings JSON 檔相對於 repo 根目錄的路徑(例如 `.gitea/ai-review/findings/xxx.json`)。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:`main()` 步驟 8 於防守方裁決、`review.sortFindings(kept)` 排序後呼叫本函式保存結果,
|
* 使用情境:`main()` 步驟 7 於防守方裁決、`review.sortFindings(kept)` 排序後呼叫本函式保存結果,
|
||||||
* 再將回傳的相對路徑交給 `commitFindings` commit 並 push 回 PR 來源分支;
|
* 再將回傳的相對路徑交給 `commitFindings` commit 並 push 回 PR 來源分支;
|
||||||
* 另在步驟 4 判定無可審查變更時,也會以空清單保存一份空 findings 後以 success 收場。
|
* 另在步驟 3 判定無可審查變更時,也會以空清單保存一份空 findings 後以 success 收場。
|
||||||
* 本函式無 try/catch,檔案系統錯誤會往上拋出,由 `main().catch` 以 exit code 1 收場。
|
* 本函式無 try/catch,檔案系統錯誤會往上拋出,由 `main().catch` 以 exit code 1 收場。
|
||||||
*/
|
*/
|
||||||
function saveFindings({ cwd, ctx, tool, kept, excluded }) {
|
function saveFindings({ cwd, ctx, tool, kept, excluded }) {
|
||||||
@@ -61,7 +61,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) {
|
|||||||
};
|
};
|
||||||
fs.writeFileSync(findingsPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
fs.writeFileSync(findingsPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
||||||
const relativePath = path.relative(cwd, findingsPath);
|
const relativePath = path.relative(cwd, findingsPath);
|
||||||
log('步驟8', 'INF', `findings 已保存:${relativePath}(保留 ${kept.length} 條、排除 ${excluded.length} 條)。`);
|
log('步驟7', 'INF', `findings 已保存:${relativePath}(保留 ${kept.length} 條、排除 ${excluded.length} 條)。`);
|
||||||
return relativePath;
|
return relativePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) {
|
|||||||
* 供下一回合 `main()` 步驟 1 比對辨識、直接回報結果而不重複審查。
|
* 供下一回合 `main()` 步驟 1 比對辨識、直接回報結果而不重複審查。
|
||||||
* 依 `commitAndPushFindings` 的回傳值記錄不同日誌:true=已 commit/push;
|
* 依 `commitAndPushFindings` 的回傳值記錄不同日誌:true=已 commit/push;
|
||||||
* false=檔案無實際變更(空 commit 防護),記「略過 commit/push」。
|
* false=檔案無實際變更(空 commit 防護),記「略過 commit/push」。
|
||||||
* commit/push 失敗(例如與開發者新 commit 競態)時僅記 WRN log,不拋出例外;呼叫端可依回傳值決定是否阻擋。
|
* commit/push 失敗(例如與開發者新 commit 競態)時僅記 WRN log,不拋出例外、不改變審查結果。
|
||||||
*
|
*
|
||||||
* @param {Object} params - 解構參數。
|
* @param {Object} params - 解構參數。
|
||||||
* @param {string} params.cwd - repo 根目錄(workspace)絕對路徑,git 操作在此目錄執行。
|
* @param {string} params.cwd - repo 根目錄(workspace)絕對路徑,git 操作在此目錄執行。
|
||||||
@@ -84,14 +84,12 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) {
|
|||||||
* @param {string} params.ctx.repository - `owner/repo` 形式的 repo 名稱。
|
* @param {string} params.ctx.repository - `owner/repo` 形式的 repo 名稱。
|
||||||
* @param {string[]} params.files - 要 commit 的檔案 repo 相對路徑陣列(如 findings 檔、`.gitea/ai-review/exclusions.json`);全數無變更時只記 INF 略過。
|
* @param {string[]} params.files - 要 commit 的檔案 repo 相對路徑陣列(如 findings 檔、`.gitea/ai-review/exclusions.json`);全數無變更時只記 INF 略過。
|
||||||
* @param {'success'|'failure'} params.result - 本回合審查結果:success=無嚴重問題、failure=有嚴重問題;會拼進 commit 訊息尾端。
|
* @param {'success'|'failure'} params.result - 本回合審查結果:success=無嚴重問題、failure=有嚴重問題;會拼進 commit 訊息尾端。
|
||||||
* @returns {boolean} true=已 commit/push;false=無變更或 commit/push 失敗。
|
* @returns {void} 無回傳值;成敗僅反映在 log 上。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、
|
* 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、
|
||||||
* 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json;
|
* 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json;
|
||||||
* 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 4 判定無可審查變更且非建問題模式時,
|
* 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 3 判定無可審查變更且非建問題模式時,
|
||||||
* 也會以 result: 'success' 提交空 findings。
|
* 也會以 result: 'success' 提交空 findings。
|
||||||
* 推送一律以 `ctx.token` 的身分進行(不走 runner 的 origin 自動 token);只要 token 是能觸發 CI 的
|
|
||||||
* PAT,結果 commit 就會再觸發 CI、由步驟 1 快速回報;
|
|
||||||
* 注意 commit 訊息與模組常數 `BOT_COMMIT_PREFIX` 耦合,修改前綴會使步驟 1 的快速回報失效。
|
* 注意 commit 訊息與模組常數 `BOT_COMMIT_PREFIX` 耦合,修改前綴會使步驟 1 的快速回報失效。
|
||||||
*/
|
*/
|
||||||
function commitFindings({ cwd, ctx, files, result }) {
|
function commitFindings({ cwd, ctx, files, result }) {
|
||||||
@@ -107,35 +105,41 @@ function commitFindings({ cwd, ctx, files, result }) {
|
|||||||
});
|
});
|
||||||
if (committed) {
|
if (committed) {
|
||||||
log('收尾', 'INF', `審查結果檔已 commit 並 push 回 ${ctx.headRef}(結果:${result})。`);
|
log('收尾', 'INF', `審查結果檔已 commit 並 push 回 ${ctx.headRef}(結果:${result})。`);
|
||||||
return true;
|
|
||||||
} else {
|
} else {
|
||||||
log('收尾', 'INF', '審查結果檔無實際變更,略過 commit/push。');
|
log('收尾', 'INF', '審查結果檔無實際變更,略過 commit/push。');
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// push 失敗(例如與開發者新 commit 競態)時只記錄,交由呼叫端依嚴重度決定是否阻擋。
|
// push 失敗(例如與開發者新 commit 競態)時只記錄,不改變審查結果。
|
||||||
log('收尾', 'WRN', `commit/push 審查結果檔失敗:${err.message}。`);
|
log('收尾', 'WRN', `commit/push 審查結果檔失敗:${err.message}。`);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI code review 主流程:編排多角色審查、發布審查結果,並回傳 process exit code。
|
* AI code review 主流程:依固定 10 步驟執行多角色審查,回傳 process exit code。
|
||||||
*
|
*
|
||||||
* 一般模式會把審查情境、嚴重問題與警告/建議發布到 PR,並在成功產生本回合結果後才把舊留言標為過時。
|
* 流程概要:
|
||||||
* 建問題模式會把審查情境與每條 finding 發到追蹤 issue;沒有保留 finding 時不建立 issue、PR 也不留言。
|
* 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審;
|
||||||
* 嚴重 finding 會寫入 failure 結果 commit,警告與建議只建立追蹤資訊,不直接阻擋合併。
|
* 2. 偵測 AI 工具(antigravity/codex/claude)並留言;
|
||||||
|
* 3. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings,
|
||||||
|
* 一般模式 commit success、建問題模式略過 commit,回傳 0);
|
||||||
|
* 4–5. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題;
|
||||||
|
* 6–7. 防守方登場留言、裁決誤報後排序並保存 findings JSON,
|
||||||
|
* 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json;
|
||||||
|
* 8. 將 PR 既有舊留言標記為解決(跳過本回合留言);
|
||||||
|
* 9. 嚴重問題逐條掛在程式碼行上留言;
|
||||||
|
* 10. 警告+建議彙整為單一表格留言;
|
||||||
|
* 建問題模式(input: create-issue):保留問題另建 issue(createIssueWithFindings)逐條留言明細;
|
||||||
|
* 收尾:組 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=成功(無嚴重問題或無可審查變更、或偵測到 success 標記);
|
||||||
* 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報);
|
* 1=失敗(有嚴重問題、缺 PR 編號/token、找不到 AI 工具、或偵測到 failure 標記)。
|
||||||
* 回傳 1 僅發生於:步驟 1 偵測到 `[ai-review-bot][failure]` 結果 commit,或前置條件不足
|
|
||||||
* (缺 PR 編號/token、找不到 AI 工具)等無法進行審查的情況。
|
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:由本檔尾端的頂層呼叫端執行 —— `main().then((code) => process.exit(code))`;
|
* 使用情境:由本檔尾端的頂層呼叫端執行 —— `main().then((code) => process.exit(code))`;
|
||||||
* 非預期例外由頂層 `catch` 記 ERR log 後以 exit code 1 收場,且刻意不 commit 結果標記,
|
* 非預期例外由頂層 `catch` 記 ERR log 後以 exit code 1 收場,且刻意不 commit 結果標記,
|
||||||
* 讓下一次 workflow 觸發時重新完整審查。警告+建議等級不影響結果標記,只有「嚴重」會使結果 commit
|
* 讓下一次 workflow 觸發時重新完整審查。警告+建議等級的問題不影響成敗,只有「嚴重」會使結果為 failure;
|
||||||
* 標記為 failure;而「失敗檢查(exit 1)」只由步驟 1 讀到該 failure 結果 commit 時產生,審查本輪不直接 exit 1。
|
* 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變成敗判定。
|
||||||
* 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變上述結果標記判定。
|
|
||||||
*/
|
*/
|
||||||
async function main() {
|
async function main() {
|
||||||
const ctx = loadContext();
|
const ctx = loadContext();
|
||||||
@@ -163,88 +167,23 @@ async function main() {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本回合(一般模式)發出的 PR 留言 id:resolveOldComments 標註過時時要跳過這些。
|
// 本回合發出的一般留言 id:步驟 8 標註過時時要跳過這些。
|
||||||
const currentRunCommentIds = new Set();
|
const currentRunCommentIds = new Set();
|
||||||
// 建問題模式:追蹤 issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色)
|
const postComment = async (body) => {
|
||||||
// 先暫存於 pendingIssueCommentBodies,建立 issue 後一次寫入。
|
|
||||||
const pendingIssueCommentBodies = [];
|
|
||||||
let issueModeActive = ctx.createIssue;
|
|
||||||
let trackingIssue = null;
|
|
||||||
/**
|
|
||||||
* 發布一則審查留言。依模式決定去向:
|
|
||||||
* - 一般模式:發到 PR,並記錄留言 id 供 `resolveOldComments` 排除。
|
|
||||||
* - 建問題模式:追蹤 issue 已建立時發到 issue;尚未建立時先暫存到 `pendingIssueCommentBodies`。
|
|
||||||
*
|
|
||||||
* @param {string} body 要發布的 Markdown 留言內容。
|
|
||||||
* @returns {Promise<Object|null>} 一般模式、或建問題模式且 issue 已建立時回傳 Gitea 留言物件;
|
|
||||||
* 建問題模式尚未建立 issue 而先暫存時回傳 null。
|
|
||||||
* @remarks
|
|
||||||
* 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與
|
|
||||||
* 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。
|
|
||||||
*/
|
|
||||||
const queueOrPostComment = async (body) => {
|
|
||||||
if (issueModeActive) {
|
|
||||||
if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);
|
|
||||||
pendingIssueCommentBodies.push(body);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const created = await gitea.createIssueComment(ctx, body);
|
const created = await gitea.createIssueComment(ctx, body);
|
||||||
currentRunCommentIds.add(created.id);
|
currentRunCommentIds.add(created.id);
|
||||||
return created;
|
return created;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
|
|
||||||
* 並把 `pendingIssueCommentBodies` 內暫存的情境留言依流程順序寫入 issue;
|
|
||||||
* 設定閉包變數 `trackingIssue` 供後續留言直接發到 issue。
|
|
||||||
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
|
|
||||||
*
|
|
||||||
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
|
|
||||||
* 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。
|
|
||||||
* @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `trackingIssue` 與 issue 留言。
|
|
||||||
*/
|
|
||||||
const createIssueAndFlushBufferedComments = async (labelIds = []) => {
|
|
||||||
trackingIssue = await gitea.createIssue(ctx, {
|
|
||||||
title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`,
|
|
||||||
body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
|
|
||||||
labels: labelIds,
|
|
||||||
});
|
|
||||||
log('建問題', 'INF', `已建立追蹤 issue #${trackingIssue.number},寫入 ${pendingIssueCommentBodies.length} 則情境留言。`);
|
|
||||||
for (const body of pendingIssueCommentBodies) {
|
|
||||||
await gitea.createCommentOnIssue(ctx, trackingIssue.number, body);
|
|
||||||
}
|
|
||||||
pendingIssueCommentBodies.length = 0;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 建問題模式降級:追蹤 issue 無法建立或寫入時,改把已暫存的情境留言發回 PR,後續沿用一般模式。
|
|
||||||
*
|
|
||||||
* @returns {Promise<void>} 無回傳值;會關閉建問題模式並把 PR 留言 id 登錄到 `currentRunCommentIds`。
|
|
||||||
*/
|
|
||||||
const fallbackToPrComments = async () => {
|
|
||||||
issueModeActive = false;
|
|
||||||
trackingIssue = null;
|
|
||||||
for (const body of pendingIssueCommentBodies) {
|
|
||||||
const created = await gitea.createIssueComment(ctx, body);
|
|
||||||
currentRunCommentIds.add(created.id);
|
|
||||||
}
|
|
||||||
pendingIssueCommentBodies.length = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── 步驟 2:延後執行 ───────────────────────────────────────────────────
|
// ── 步驟 2:偵測 AI agent 工具並留言 ──────────────────────────────────
|
||||||
// 「將 PR 既有留言標記為解決」原本在此執行,但若工具偵測/diff/攻防裁決任一失敗,
|
|
||||||
// 舊結果會先被清掉卻沒有新結果。故延後到「本回合審查已成功產生結果、發布問題留言前」
|
|
||||||
// 才呼叫 review.resolveOldComments(見下方步驟 4 空變更路徑與步驟 9 前);
|
|
||||||
// 屆時本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時。
|
|
||||||
// 建問題模式不清理 PR 既有審查內容,只在收束時標記舊追蹤 issue 連結。
|
|
||||||
|
|
||||||
// ── 步驟 3:偵測 AI agent 工具並留言 ──────────────────────────────────
|
|
||||||
const tool = agents.detectTool();
|
const tool = agents.detectTool();
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
log('步驟3', 'ERR', '找不到可用的 AI 工具(antigravity/codex/claude)。');
|
log('步驟2', 'ERR', '找不到可用的 AI 工具(antigravity/codex/claude)。');
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
log('步驟3', 'INF', `選用工具:${tool.name}(${tool.version})。`);
|
log('步驟2', '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 queueOrPostComment(
|
await postComment(
|
||||||
templates.toolComment({
|
templates.toolComment({
|
||||||
toolName: tool.name,
|
toolName: tool.name,
|
||||||
version: tool.version,
|
version: tool.version,
|
||||||
@@ -255,26 +194,19 @@ async function main() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── 步驟 4:讀取 .reviewignore、整理 git diff 並留言 ───────────────────
|
// ── 步驟 3:讀取 .reviewignore、整理 git diff 並留言 ───────────────────
|
||||||
const ignores = review.loadReviewIgnore(cwd);
|
const ignores = review.loadReviewIgnore(cwd);
|
||||||
const base = gitrepo.resolveMergeBase(cwd, ctx.baseRef);
|
const base = gitrepo.resolveMergeBase(cwd, ctx.baseRef);
|
||||||
const allFiles = gitrepo.changedFiles(cwd, base);
|
const allFiles = gitrepo.changedFiles(cwd, base);
|
||||||
const files = allFiles.filter((file) => !review.isIgnored(file, ignores));
|
const files = allFiles.filter((file) => !review.isIgnored(file, ignores));
|
||||||
const ignoredCount = allFiles.length - files.length;
|
const ignoredCount = allFiles.length - files.length;
|
||||||
log('步驟4', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`);
|
log('步驟3', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`);
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
// 沒有可審查的變更:保存空 findings、以 success 收場。
|
// 沒有可審查的變更:留言說明、保存空 findings、以 success 收場。
|
||||||
// 一般模式在 PR 留言告知;建問題模式靜默通過(不建 issue、PR 也不留言,暫存的情境留言捨棄)。
|
await postComment(templates.nothingToReviewComment(ignoredCount));
|
||||||
if (issueModeActive) {
|
|
||||||
log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。');
|
|
||||||
} else {
|
|
||||||
await queueOrPostComment(templates.nothingToReviewComment(ignoredCount));
|
|
||||||
// 已成功產生本回合結果留言(無可審查變更),此時才把舊留言標為過時(本回合留言已排除)。
|
|
||||||
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
|
||||||
}
|
|
||||||
const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] });
|
const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] });
|
||||||
if (issueModeActive) {
|
if (ctx.createIssue) {
|
||||||
// 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。
|
// 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。
|
||||||
log('收尾', 'INF', '建問題模式且無可審查變更,略過 commit/push。');
|
log('收尾', 'INF', '建問題模式且無可審查變更,略過 commit/push。');
|
||||||
} else {
|
} else {
|
||||||
@@ -285,152 +217,70 @@ 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 queueOrPostComment(templates.diffComment(diffRows, ignoredCount));
|
await postComment(templates.diffComment(diffRows, ignoredCount));
|
||||||
|
|
||||||
// ── 步驟 5:攻擊方角色登場留言 ─────────────────────────────────────────
|
// ── 步驟 4:攻擊方角色登場留言 ─────────────────────────────────────────
|
||||||
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('步驟4', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`);
|
||||||
await queueOrPostComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers }));
|
await postComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers }));
|
||||||
|
|
||||||
// ── 步驟 6:每個攻擊方一個 sub agent 並行分析,合併問題列表 ────────────
|
// ── 步驟 5:每個攻擊方一個 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:防守方角色登場留言 ─────────────────────────────────────────
|
// ── 步驟 6:防守方角色登場留言 ─────────────────────────────────────────
|
||||||
await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders }));
|
await postComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders }));
|
||||||
|
|
||||||
// ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ──────────────────
|
// ── 步驟 7:防守方裁決 → 排除 → 排序 → 保存 findings ──────────────────
|
||||||
const { kept, excluded } = findings.length === 0
|
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
|
||||||
? { 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 });
|
||||||
|
|
||||||
// 誤判/重複的問題附加到 exclusions.json(之後與審查結果一起 commit)。
|
// 誤判/重複的問題附加到 exclusions.json(之後與審查結果一起 commit)。
|
||||||
const exclusionsChanged = review.appendExclusions({ cwd, excluded, prNumber: ctx.prNumber });
|
const exclusionsChanged = review.appendExclusions({ cwd, excluded, prNumber: ctx.prNumber });
|
||||||
|
|
||||||
// ── 步驟 8(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ─
|
// ── 步驟 7(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ─
|
||||||
const severe = kept.filter((finding) => finding.severity === '嚴重');
|
const severe = kept.filter((finding) => finding.severity === '嚴重');
|
||||||
const others = kept.filter((finding) => finding.severity !== '嚴重');
|
const others = kept.filter((finding) => finding.severity !== '嚴重');
|
||||||
log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`);
|
log('步驟7', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`);
|
||||||
|
|
||||||
// ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入;
|
// ── 步驟 8:將 PR 既有留言標記為解決(本回合留言除外)───────────────────
|
||||||
// 無保留問題則不建 issue、PR 也完全不留言(靜默通過,暫存的情境留言捨棄)。 ──────
|
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
||||||
if (issueModeActive) {
|
|
||||||
if (kept.length > 0) {
|
|
||||||
// 先依保留問題挑好標籤,於建立 issue 時一次帶入(省去「先建空標籤 issue 再補掛」的多餘 API 往返);
|
|
||||||
// 標籤挑選失敗一律降級為不掛標籤,不阻斷建 issue 流程。
|
|
||||||
let labelIds = [];
|
|
||||||
try {
|
|
||||||
const labels = await gitea.listLabels(ctx);
|
|
||||||
labelIds = await review.selectLabels({
|
|
||||||
tool,
|
|
||||||
model: ctx.model,
|
|
||||||
cwd,
|
|
||||||
labels,
|
|
||||||
prTitle: ctx.prTitle,
|
|
||||||
prBody: ctx.prBody,
|
|
||||||
findings: kept,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await createIssueAndFlushBufferedComments(labelIds);
|
|
||||||
} catch (err) {
|
|
||||||
log('建問題', 'WRN', `建立或寫入追蹤 issue 失敗(${err.message}),改用 PR 留言與 findings 檔流程。`);
|
|
||||||
await fallbackToPrComments();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。
|
|
||||||
log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 步驟 2(延後執行,一般模式):審查已成功產生結果,發布問題留言前才把舊留言標為過時 ─
|
// ── 步驟 9:嚴重問題逐條掛在程式碼行上留言(開發者可回覆)──────────────
|
||||||
// 延後到此可避免工具偵測/diff/攻防裁決任一失敗時舊結果先被清掉卻無新結果;
|
|
||||||
// 本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時;
|
|
||||||
// 嚴重/其他問題留言於本步驟之後才發布,同樣不受影響。
|
|
||||||
if (!issueModeActive) {
|
|
||||||
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue)─
|
|
||||||
if (severe.length > 0) {
|
if (severe.length > 0) {
|
||||||
if (issueModeActive && trackingIssue) {
|
await review.postSevereComments({ ctx, gitea, severe, cwd });
|
||||||
await review.postSevereToIssue({ ctx, gitea, issueNumber: trackingIssue.number, severe });
|
|
||||||
} else {
|
|
||||||
await review.postSevereComments({ ctx, gitea, severe, cwd });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 步驟 10:警告+建議——一般模式彙整為單一表格留言到 PR;
|
// ── 步驟 10:警告+建議彙整為單一表格留言 ──────────────────────────────
|
||||||
// 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ──
|
|
||||||
if (others.length > 0) {
|
if (others.length > 0) {
|
||||||
if (issueModeActive && trackingIssue) {
|
await postComment(templates.othersComment(others));
|
||||||
await review.postOthersToIssue({ ctx, gitea, issueNumber: trackingIssue.number, others });
|
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
|
||||||
} else {
|
|
||||||
await queueOrPostComment(templates.othersComment(others));
|
|
||||||
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─
|
// ── 建問題模式(input: create-issue):另建 issue 逐條留言問題明細 ──────
|
||||||
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。
|
if (ctx.createIssue) {
|
||||||
if (issueModeActive && trackingIssue) {
|
if (kept.length > 0) {
|
||||||
await review.resolveOldIssueLinkComments({ ctx, gitea });
|
await review.createIssueWithFindings({ ctx, gitea, tool, model: ctx.model, cwd, findings: kept });
|
||||||
await gitea.createIssueComment(
|
|
||||||
ctx,
|
|
||||||
templates.prIssueLinkComment({
|
|
||||||
issueNumber: trackingIssue.number,
|
|
||||||
issueUrl: trackingIssue.html_url,
|
|
||||||
severeCount: severe.length,
|
|
||||||
otherCount: others.length,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
// 只有「嚴重」問題才讓 PR 相依於追蹤 issue(issue 關閉前無法合併,需 repo 啟用「問題相依」功能);
|
|
||||||
// 僅有警告/建議時,issue 仍建立供追蹤,但不掛相依、不阻擋 PR 合併。
|
|
||||||
if (severe.length > 0) {
|
|
||||||
try {
|
|
||||||
await gitea.addIssueDependency(ctx, ctx.prNumber, trackingIssue.number);
|
|
||||||
log('建問題', 'INF', `有嚴重問題:已將 PR #${ctx.prNumber} 設為相依於 issue #${trackingIssue.number},issue 關閉前無法合併。`);
|
|
||||||
} catch (err) {
|
|
||||||
log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
log('建問題', 'INF', `無嚴重問題(僅警告/建議):issue #${trackingIssue.number} 僅供追蹤,不阻擋 PR 合併。`);
|
log('建問題', 'INF', '沒有保留的問題,略過建立 issue。');
|
||||||
}
|
}
|
||||||
log('建問題', 'INF', `issue #${trackingIssue.number} 已寫入審查內容,並在 PR 回貼連結。`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)───────
|
// ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)───────
|
||||||
// 一般模式:findings+exclusions.json;建問題模式通常只 commit exclusions.json。
|
// 一般模式:findings+exclusions.json;建問題模式:問題明細已在 issue 留言,只 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 = review.resultFilesToCommit({
|
const filesToCommit = ctx.createIssue ? [] : [relativePath];
|
||||||
createIssue: issueModeActive,
|
if (exclusionsChanged) {
|
||||||
severeCount: severe.length,
|
filesToCommit.push(path.join('.gitea', 'ai-review', 'exclusions.json'));
|
||||||
relativePath,
|
}
|
||||||
exclusionsChanged,
|
|
||||||
});
|
|
||||||
let resultCommitted = false;
|
|
||||||
if (filesToCommit.length > 0) {
|
if (filesToCommit.length > 0) {
|
||||||
resultCommitted = commitFindings({ cwd, ctx, files: filesToCommit, result });
|
commitFindings({ cwd, ctx, files: filesToCommit, result });
|
||||||
} else {
|
} else {
|
||||||
log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。');
|
log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。');
|
||||||
}
|
}
|
||||||
// 本輪「審查」一律以成功收場、不直接讓檢查失敗;有嚴重問題時已推出 [failure] 結果 commit,
|
return result === 'success' ? 0 : 1;
|
||||||
// 由它再觸發的下一輪在步驟 1 讀 commit 訊息時才回報失敗(exit 1)。如此失敗檢查落在帶有結果
|
|
||||||
// 標記的最新 head 上,與合併判定一致。(result 僅用於上方 commit 訊息的結果標記。)
|
|
||||||
if (result === 'failure') {
|
|
||||||
if (!resultCommitted) {
|
|
||||||
log('收尾', 'ERR', '本輪有嚴重問題,但未成功產生 [failure] 結果 commit;直接回報失敗避免 fail-open。');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。');
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ const TOOLS = [
|
|||||||
* @returns {{ name: string, buildArgs: Function, resultFrom: string, version: string } | null}
|
* @returns {{ name: string, buildArgs: Function, resultFrom: string, version: string } | null}
|
||||||
* 中選工具的描述物件(TOOLS 項目加上 version 欄位);所有工具皆不可用時回傳 null。
|
* 中選工具的描述物件(TOOLS 項目加上 version 欄位);所有工具皆不可用時回傳 null。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:action 主流程(步驟 3)啟動審查前呼叫一次,取得工具描述後交給
|
* 使用情境:action 主流程(步驟 2)啟動審查前呼叫一次,取得工具描述後交給
|
||||||
* runAgent 執行;若回傳 null,主流程會記 ERR 並以失敗收場(無工具即無法審查)。
|
* runAgent 執行;若回傳 null,主流程會記 ERR 並以失敗收場(無工具即無法審查)。
|
||||||
*/
|
*/
|
||||||
function detectTool() {
|
function detectTool() {
|
||||||
|
|||||||
+1
-2
@@ -37,8 +37,7 @@ const path = require('path');
|
|||||||
* - repository:`owner/repo` 全名(GITHUB_REPOSITORY)。
|
* - repository:`owner/repo` 全名(GITHUB_REPOSITORY)。
|
||||||
* - owner / repo:自 repository 拆出的擁有者與專案名,缺值時為空字串。
|
* - owner / repo:自 repository 拆出的擁有者與專案名,缺值時為空字串。
|
||||||
* - apiBase:Gitea REST API 基底網址(`<serverUrl>/api/v1`)。
|
* - apiBase:Gitea REST API 基底網址(`<serverUrl>/api/v1`)。
|
||||||
* - token:action input `token`(INPUT_TOKEN),用於 Gitea API 認證,以及 push findings/exclusions
|
* - token:action input `token`(INPUT_TOKEN),用於 API 認證,缺值時為空字串。
|
||||||
* commit 回 repo;建議為「能觸發 CI 的 PAT」(自動 token 推送不會再觸發 CI)。缺值時為空字串。
|
|
||||||
* - model:action input `model`(INPUT_MODEL,已 trim),指定 AI 模型,缺值時為空字串。
|
* - model:action input `model`(INPUT_MODEL,已 trim),指定 AI 模型,缺值時為空字串。
|
||||||
* - createIssue:action input `create-issue`(INPUT_CREATE-ISSUE),是否將問題建到
|
* - createIssue:action input `create-issue`(INPUT_CREATE-ISSUE),是否將問題建到
|
||||||
* 存取庫的問題追蹤(建問題模式);trim + 小寫後與字串 'true' 嚴格比對,預設 false。
|
* 存取庫的問題追蹤(建問題模式);trim + 小寫後與字串 'true' 嚴格比對,預設 false。
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
'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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 將 AI CLI 失敗輸出整理成單行、遮罩且限長的診斷片段。
|
|
||||||
*
|
|
||||||
* @param {string[]} parts - 要附加診斷片段的陣列。
|
|
||||||
* @param {string} label - 診斷欄位名稱(如 stderr/stdout)。
|
|
||||||
* @param {*} value - 原始診斷輸出。
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function appendRedactedOutput(parts, label, value) {
|
|
||||||
const redacted = redactSecrets(String(value || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
|
|
||||||
if (redacted) parts.push(`${label}:${redacted.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
|
|
||||||
*
|
|
||||||
* 安全考量: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') {
|
|
||||||
appendRedactedOutput(parts, 'stderr', agentResult && agentResult.stderr);
|
|
||||||
appendRedactedOutput(parts, 'stdout', agentResult && agentResult.output);
|
|
||||||
}
|
|
||||||
if (parts.length === 0) {
|
|
||||||
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
|
|
||||||
}
|
|
||||||
return parts.join('|');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
AGENT_DIAGNOSTIC_INPUT_LIMIT,
|
|
||||||
AGENT_DIAGNOSTIC_OUTPUT_LIMIT,
|
|
||||||
redactSecrets,
|
|
||||||
agentFailureDetail,
|
|
||||||
};
|
|
||||||
+14
-41
@@ -80,7 +80,7 @@ async function listAll(ctx, apiPath) {
|
|||||||
* @returns {Promise<object>} Gitea 使用者物件(含 `id`、`login` 等欄位,
|
* @returns {Promise<object>} Gitea 使用者物件(含 `id`、`login` 等欄位,
|
||||||
* 依 Gitea API 回應而定)。
|
* 依 Gitea API 回應而定)。
|
||||||
* @throws {Error} 請求失敗(非 2xx,例如 token 無效時 401)由底層 `api` 丟出。
|
* @throws {Error} 請求失敗(非 2xx,例如 token 無效時 401)由底層 `api` 丟出。
|
||||||
* @remarks 使用情境:action 步驟 2 先查出 bot 自己的帳號,
|
* @remarks 使用情境:action 步驟 8 先查出 bot 自己的帳號,
|
||||||
* 之後比對 PR 留言的作者,辨識哪些留言是本 action 先前發出的
|
* 之後比對 PR 留言的作者,辨識哪些留言是本 action 先前發出的
|
||||||
* (例如要將舊留言標註為已過時)。
|
* (例如要將舊留言標註為已過時)。
|
||||||
*/
|
*/
|
||||||
@@ -99,8 +99,8 @@ function whoAmI(ctx) {
|
|||||||
* @returns {Promise<object>} 建立成功的留言物件(含 `id`、`body`、`user` 等欄位,
|
* @returns {Promise<object>} 建立成功的留言物件(含 `id`、`body`、`user` 等欄位,
|
||||||
* 依 Gitea API 回應而定)。
|
* 依 Gitea API 回應而定)。
|
||||||
* @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
* @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
||||||
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立 issue 後,
|
* @remarks 使用情境:建問題模式(input: create-issue)下,
|
||||||
* 把工具/diff/角色情境留言與 `review.postSevereToIssue` 的嚴重問題明細留言到該 issue;
|
* `createIssueWithFindings` 建立 issue 後,逐條把 finding 明細留言到該 issue;
|
||||||
* 另外 `createIssueComment` 也委派本函式對 `ctx.prNumber` 留言。
|
* 另外 `createIssueComment` 也委派本函式對 `ctx.prNumber` 留言。
|
||||||
*/
|
*/
|
||||||
function createCommentOnIssue(ctx, issueNumber, body) {
|
function createCommentOnIssue(ctx, issueNumber, body) {
|
||||||
@@ -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()` 的 `queueOrPostComment` 閉包即以本函式實作)。
|
* 以一般留言形式張貼到本次 PR 上(`main()` 的 `postComment` 閉包即以本函式實作)。
|
||||||
*/
|
*/
|
||||||
function createIssueComment(ctx, body) {
|
function createIssueComment(ctx, body) {
|
||||||
return createCommentOnIssue(ctx, ctx.prNumber, body);
|
return createCommentOnIssue(ctx, ctx.prNumber, body);
|
||||||
@@ -134,9 +134,9 @@ function createIssueComment(ctx, body) {
|
|||||||
* @returns {Promise<object[]>} 標籤物件陣列(每筆含 `id`、`name`、`color` 等欄位,
|
* @returns {Promise<object[]>} 標籤物件陣列(每筆含 `id`、`name`、`color` 等欄位,
|
||||||
* 依 Gitea API 回應而定);存取庫無標籤時為空陣列。
|
* 依 Gitea API 回應而定);存取庫無標籤時為空陣列。
|
||||||
* @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
* @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
||||||
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 於確定有保留問題後
|
* @remarks 使用情境:建問題模式(input: create-issue)下,
|
||||||
* 先以本函式取得可用標籤,再交給 `review.selectLabels` 讓 AI 挑出適合的標籤子集合,
|
* `createIssueWithFindings` 先以本函式取得可用標籤,再交給 `selectLabels`
|
||||||
* 最後於建立追蹤 issue 時(`createIssue`)一次帶入這些標籤。
|
* 讓 AI 從中挑選適合掛在新 issue 上的標籤子集合。
|
||||||
*/
|
*/
|
||||||
function listLabels(ctx) {
|
function listLabels(ctx) {
|
||||||
return listAll(ctx, `/repos/${ctx.owner}/${ctx.repo}/labels`);
|
return listAll(ctx, `/repos/${ctx.owner}/${ctx.repo}/labels`);
|
||||||
@@ -156,9 +156,10 @@ 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()` 的 `createIssueAndFlushBufferedComments`
|
* @remarks 使用情境:建問題模式(input: create-issue)下,
|
||||||
* 以 PR 標題/描述為 issue 標題與本文,並帶入 `review.selectLabels` 事先挑好的標籤 id
|
* `createIssueWithFindings` 以 PR 標題/描述為 issue 標題與本文、
|
||||||
* 呼叫本函式一次建立追蹤問題的 issue(連同標籤),之後再把審查內容逐條留言到該 issue。
|
* 配上 `selectLabels` 挑出的標籤 id,呼叫本函式建立追蹤問題的 issue,
|
||||||
|
* 再逐條把 finding 明細留言到該 issue。
|
||||||
*/
|
*/
|
||||||
function createIssue(ctx, { title, body, labels }) {
|
function createIssue(ctx, { title, body, labels }) {
|
||||||
return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues`, {
|
return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues`, {
|
||||||
@@ -168,33 +169,6 @@ function createIssue(ctx, { title, body, labels }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 建立「問題相依」關係:讓 URL 上的 issue/PR 相依於(被阻擋於)表單指定的 issue。
|
|
||||||
* 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/dependencies`
|
|
||||||
* (body 為 IssueMeta:`{index, owner, repo}`)。
|
|
||||||
*
|
|
||||||
* 語義:URL 的 issue(`blockedIssueNumber`)相依於 body 的 issue(`blockingIssueNumber`)——
|
|
||||||
* 在 `blockingIssueNumber` 關閉前,`blockedIssueNumber` 無法合併/關閉。本 endpoint 需 repo 啟用
|
|
||||||
* 「問題相依(issue dependencies)」功能,屬版本/設定相依;未啟用或不支援時 API 會回非 2xx。
|
|
||||||
*
|
|
||||||
* @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、
|
|
||||||
* `owner`(repo 擁有者)、`repo`(repo 名稱)。
|
|
||||||
* @param {number|string} blockedIssueNumber - 要被阻擋的 issue/PR 編號(相依方)。
|
|
||||||
* @param {number} blockingIssueNumber - 作為阻擋來源的 issue 編號(同一 repo)。
|
|
||||||
* @returns {Promise<object>} 建立成功的相依關係物件(依 Gitea API 回應而定)。
|
|
||||||
* @throws {Error} 請求失敗(非 2xx,例如未啟用問題相依功能)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
|
||||||
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立追蹤 issue 後,
|
|
||||||
* 以本函式把「PR(`ctx.prNumber`)相依於追蹤 issue」,讓 issue 完成/關閉前 PR 無法合併;
|
|
||||||
* 呼叫端以 try/catch 降級(功能未啟用時記 WRN、不阻斷流程)。
|
|
||||||
*/
|
|
||||||
function addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {
|
|
||||||
return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {
|
|
||||||
index: blockingIssueNumber,
|
|
||||||
owner: ctx.owner,
|
|
||||||
repo: ctx.repo,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 列出 PR 上的全部一般留言(自動分頁撈取,每頁 50 筆直到取完)。
|
* 列出 PR 上的全部一般留言(自動分頁撈取,每頁 50 筆直到取完)。
|
||||||
* 對應 endpoint:`GET /repos/{owner}/{repo}/issues/{prNumber}/comments`。
|
* 對應 endpoint:`GET /repos/{owner}/{repo}/issues/{prNumber}/comments`。
|
||||||
@@ -204,7 +178,7 @@ function addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {
|
|||||||
* @returns {Promise<Array<object>>} 留言物件陣列(含 `id`、`body`、`user` 等欄位);
|
* @returns {Promise<Array<object>>} 留言物件陣列(含 `id`、`body`、`user` 等欄位);
|
||||||
* 無留言時為空陣列。
|
* 無留言時為空陣列。
|
||||||
* @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
* @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
||||||
* @remarks 使用情境:步驟 2 重跑 review 前,先撈出 PR 全部留言並搭配 `whoAmI`
|
* @remarks 使用情境:步驟 8 重跑 review 前,先撈出 PR 全部留言並搭配 `whoAmI`
|
||||||
* 比對作者,找出本 action(bot)先前發過的留言,以便編輯標註為已過時。
|
* 比對作者,找出本 action(bot)先前發過的留言,以便編輯標註為已過時。
|
||||||
*/
|
*/
|
||||||
function listIssueComments(ctx) {
|
function listIssueComments(ctx) {
|
||||||
@@ -263,7 +237,7 @@ function createReview(ctx, body, comments) {
|
|||||||
* @returns {Promise<Array<object>>} review 物件陣列(含 `id`、`user`、`body` 等欄位);
|
* @returns {Promise<Array<object>>} review 物件陣列(含 `id`、`user`、`body` 等欄位);
|
||||||
* 無 review 時為空陣列。
|
* 無 review 時為空陣列。
|
||||||
* @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
* @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
||||||
* @remarks 使用情境:步驟 2 重跑 review 前,先找出 PR 上既有 review,
|
* @remarks 使用情境:步驟 8 重跑 review 前,先找出 PR 上既有 review,
|
||||||
* 再以 `listReviewComments` 取出其行內留言做後續解決標記。
|
* 再以 `listReviewComments` 取出其行內留言做後續解決標記。
|
||||||
*/
|
*/
|
||||||
function listReviews(ctx) {
|
function listReviews(ctx) {
|
||||||
@@ -306,7 +280,7 @@ function listReviewComments(ctx, reviewId) {
|
|||||||
* @param {number|string} commentId - 要標記為已解決的行內留言 id。
|
* @param {number|string} commentId - 要標記為已解決的行內留言 id。
|
||||||
* @returns {Promise<boolean>} 標記成功回傳 `true`;任何失敗
|
* @returns {Promise<boolean>} 標記成功回傳 `true`;任何失敗
|
||||||
* (版本不支援、權限不足、留言不存在等)一律回傳 `false`,不丟出例外。
|
* (版本不支援、權限不足、留言不存在等)一律回傳 `false`,不丟出例外。
|
||||||
* @remarks 使用情境:步驟 2 嘗試把舊回合的行內留言標記為已解決;若回傳 `false`
|
* @remarks 使用情境:步驟 8 嘗試把舊回合的行內留言標記為已解決;若回傳 `false`
|
||||||
* (例如目標 Gitea 版本無此 API),呼叫端應停止嘗試並記 WRN
|
* (例如目標 Gitea 版本無此 API),呼叫端應停止嘗試並記 WRN
|
||||||
* (由 `resolveOldComments` 實作此降級)。
|
* (由 `resolveOldComments` 實作此降級)。
|
||||||
*/
|
*/
|
||||||
@@ -329,7 +303,6 @@ module.exports = {
|
|||||||
createCommentOnIssue,
|
createCommentOnIssue,
|
||||||
listLabels,
|
listLabels,
|
||||||
createIssue,
|
createIssue,
|
||||||
addIssueDependency,
|
|
||||||
listIssueComments,
|
listIssueComments,
|
||||||
editIssueComment,
|
editIssueComment,
|
||||||
createReview,
|
createReview,
|
||||||
|
|||||||
+22
-176
@@ -4,34 +4,6 @@ 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 輸出。
|
||||||
*
|
*
|
||||||
@@ -72,25 +44,6 @@ function gitTrim(cwd, ...args) {
|
|||||||
return git(cwd, ...args).trim();
|
return git(cwd, ...args).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 嘗試同步執行 git 指令,失敗時回傳 false,成功時回傳 true。
|
|
||||||
*
|
|
||||||
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
|
|
||||||
* @param {...string} args - 傳給 git 的參數。
|
|
||||||
* @returns {boolean} git 指令是否成功結束。
|
|
||||||
* @remarks
|
|
||||||
* 使用情境:修復淺層 checkout 的歷史不足時,部分 fetch 策略可能因 runner
|
|
||||||
* 或遠端版本不同而失敗;呼叫端可依序嘗試多種策略,不讓第一個失敗中斷流程。
|
|
||||||
*/
|
|
||||||
function tryGit(cwd, ...args) {
|
|
||||||
try {
|
|
||||||
git(cwd, ...args);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取得目前 HEAD 最新一筆 commit 的訊息標題(commit message 第一行)。
|
* 取得目前 HEAD 最新一筆 commit 的訊息標題(commit message 第一行)。
|
||||||
*
|
*
|
||||||
@@ -111,18 +64,14 @@ function latestCommitSubject(cwd) {
|
|||||||
/**
|
/**
|
||||||
* 解析 PR base 分支與目前 HEAD 的 merge-base commit SHA。
|
* 解析 PR base 分支與目前 HEAD 的 merge-base commit SHA。
|
||||||
*
|
*
|
||||||
* 先以 refspec 明確更新 `origin/<baseRef>`,再以 `git merge-base origin/<baseRef> HEAD`
|
* 先嘗試 `git fetch origin <baseRef>` 更新 base 分支資料(失敗時靜默忽略,
|
||||||
* 取得共同祖先。若 checkout 是淺層歷史而導致 merge-base 失敗,會依序補抓更完整的
|
* 因 fetch-depth: 0 的 checkout 通常已含 base 分支,可直接沿用本地資料),
|
||||||
* base/head 歷史;**每個補抓策略成功後立即重試 merge-base,一成功即回傳**,
|
* 再以 `git merge-base origin/<baseRef> HEAD` 取得共同祖先。
|
||||||
* 避免在已補到足夠歷史後仍多做無謂的 fetch 往返(例如 `--unshallow` 成功就不再 deepen)。
|
|
||||||
* 各策略採資料驅動依序執行;全部用盡仍失敗時,丟出彙整了「哪個策略成功/失敗」診斷的錯誤,
|
|
||||||
* 方便維護者判斷是哪一步補抓不足(診斷僅含策略名與成敗,不含 git 原始輸出以免洩漏遠端資訊)。
|
|
||||||
*
|
*
|
||||||
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
|
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
|
||||||
* @param {string} baseRef - PR 目標(base)分支名稱,例如 'master' 或 'develop';不含 'origin/' 前綴。
|
* @param {string} baseRef - PR 目標(base)分支名稱,例如 'master' 或 'develop';不含 'origin/' 前綴。
|
||||||
* @returns {string} merge-base 的 commit SHA(40 碼十六進位字串)。
|
* @returns {string} merge-base 的 commit SHA(40 碼十六進位字串)。
|
||||||
* @throws {Error} 補抓歷史後仍無法取得共同祖先時,丟出含 baseRef 與各策略診斷的明確錯誤
|
* @throws {Error} 本地不存在 origin/<baseRef>、或兩者無共同祖先時,`git merge-base` 失敗並拋出(fetch 失敗不會拋出)。
|
||||||
* (`error.cause` 保留首次 merge-base 失敗的原始錯誤)。
|
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:AI code review 以此結果作為 diff 比較基準——
|
* 使用情境:AI code review 以此結果作為 diff 比較基準——
|
||||||
* 先 `resolveMergeBase(cwd, pr.base.ref)` 取得基準 SHA,
|
* 先 `resolveMergeBase(cwd, pr.base.ref)` 取得基準 SHA,
|
||||||
@@ -130,60 +79,12 @@ function latestCommitSubject(cwd) {
|
|||||||
* 避免把 base 分支後續演進誤算進 diff。
|
* 避免把 base 分支後續演進誤算進 diff。
|
||||||
*/
|
*/
|
||||||
function resolveMergeBase(cwd, baseRef) {
|
function resolveMergeBase(cwd, baseRef) {
|
||||||
baseRef = assertSafeBranchRef(baseRef, 'baseRef');
|
|
||||||
const remoteBase = `origin/${baseRef}`;
|
|
||||||
const diagnostics = [];
|
|
||||||
// 執行一個 fetch 策略並記錄成敗(只記策略名與成敗,不含 git 原始輸出,避免洩漏遠端資訊)。
|
|
||||||
const runFetch = (label, ...args) => {
|
|
||||||
const ok = tryGit(cwd, ...args);
|
|
||||||
diagnostics.push(`${label}:${ok ? '成功' : '失敗'}`);
|
|
||||||
return ok;
|
|
||||||
};
|
|
||||||
// 每個補抓策略後重試 merge-base:成功回傳 SHA,失敗記診斷並回傳 null。
|
|
||||||
const tryMergeBase = (label) => {
|
|
||||||
try {
|
|
||||||
return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD');
|
|
||||||
} catch {
|
|
||||||
diagnostics.push(`merge-base(${label}):失敗`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 先明確更新 origin/<baseRef>,再嘗試 merge-base。
|
|
||||||
runFetch(`fetch base(${baseRef})`, 'fetch', '--no-tags', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`);
|
|
||||||
let firstError;
|
|
||||||
try {
|
try {
|
||||||
return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD');
|
git(cwd, 'fetch', 'origin', baseRef);
|
||||||
} catch (err) {
|
} catch {
|
||||||
firstError = err;
|
// fetch-depth: 0 的 checkout 通常已含 base 分支,抓不到時直接沿用本地資料。
|
||||||
diagnostics.push('merge-base(首次):失敗');
|
|
||||||
}
|
}
|
||||||
|
return gitTrim(cwd, 'merge-base', `origin/${baseRef}`, 'HEAD');
|
||||||
// 資料驅動的補抓策略:先以固定深度分批加深 base 與 HEAD(每步後重試 merge-base,成功即回傳);
|
|
||||||
// 只有仍失敗且為淺層 repo 時,才把成本最高的 --unshallow(下載完整歷史)當最後手段,
|
|
||||||
// 避免大型/長壽 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') {
|
|
||||||
strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [label, ...args] of strategies) {
|
|
||||||
if (!runFetch(label, ...args)) continue; // fetch 失敗就換下一個策略。
|
|
||||||
const sha = tryMergeBase(`${label} 後`);
|
|
||||||
if (sha) return sha;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = new Error(
|
|
||||||
`無法解析 origin/${baseRef} 與 HEAD 的 merge-base;請確認 checkout 有足夠歷史,或設定 checkout fetch-depth: 0。診斷:${diagnostics.join(';')}`,
|
|
||||||
);
|
|
||||||
error.cause = firstError;
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -254,9 +155,7 @@ function fileLastUpdatedIso(cwd, file) {
|
|||||||
* 若目前 HEAD 不在 PR head commit(例如 checkout 停在 merge commit),
|
* 若目前 HEAD 不在 PR head commit(例如 checkout 停在 merge commit),
|
||||||
* 會先 `git checkout --detach <headSha>` 站上 head,避免把 merge 內容推回來源分支。
|
* 會先 `git checkout --detach <headSha>` 站上 head,避免把 merge 內容推回來源分支。
|
||||||
* commit 以 `-c` 臨時覆寫 user.name / user.email,不改動 repo 的 git 設定。
|
* commit 以 `-c` 臨時覆寫 user.name / user.email,不改動 repo 的 git 設定。
|
||||||
* push 策略:一律以 `token` 的身分明確認證推送({@link pushWithCredential},不走 runner 的
|
* push 先走 origin;失敗(遠端未帶認證)時改用帶 token 的 URL 重試。
|
||||||
* origin 自動 token)——origin 帶的自動 token(gitea.token / GITHUB_TOKEN)推送不會再觸發 CI,
|
|
||||||
* 改以呼叫端提供的 `token`(建議為 PAT)身分推送,才會讓 PR 的 synchronize 事件再觸發 CI。
|
|
||||||
*
|
*
|
||||||
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
|
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
|
||||||
* @param {object} options - 提交與推送設定。
|
* @param {object} options - 提交與推送設定。
|
||||||
@@ -264,7 +163,7 @@ function fileLastUpdatedIso(cwd, file) {
|
|||||||
* @param {string} [options.headSha] - PR head 的 commit SHA;有提供且與目前 HEAD 不同時會先 detach 到此 commit。可省略(falsy 時不 detach,直接於目前 HEAD 上 commit)。
|
* @param {string} [options.headSha] - PR head 的 commit SHA;有提供且與目前 HEAD 不同時會先 detach 到此 commit。可省略(falsy 時不 detach,直接於目前 HEAD 上 commit)。
|
||||||
* @param {string} options.message - commit 訊息。
|
* @param {string} options.message - commit 訊息。
|
||||||
* @param {string[]} options.files - 要加入 commit 的檔案路徑清單(相對 repo 根目錄);全數無實際變更時不 commit、回傳 false。
|
* @param {string[]} options.files - 要加入 commit 的檔案路徑清單(相對 repo 根目錄);全數無實際變更時不 commit、回傳 false。
|
||||||
* @param {string} options.token - 具該 repo push 權限的 Gitea access token(建議為能觸發 CI 的 PAT);用於 findings commit 的認證推送。
|
* @param {string} options.token - 具該 repo push 權限的 Gitea access token;僅在 origin push 失敗時用於組出帶認證的重試 URL。
|
||||||
* @param {string} options.serverUrl - Gitea 伺服器根網址(例如 https://gitea.example.com),須為合法 URL。
|
* @param {string} options.serverUrl - Gitea 伺服器根網址(例如 https://gitea.example.com),須為合法 URL。
|
||||||
* @param {string} options.repository - repo 完整名稱(owner/repo 格式),與 serverUrl 組成 clone URL。
|
* @param {string} options.repository - repo 完整名稱(owner/repo 格式),與 serverUrl 組成 clone URL。
|
||||||
* @returns {boolean} true=有變更且已 commit 並 push 到來源分支;false=暫存區與 HEAD 無差異,略過 commit/push。
|
* @returns {boolean} true=有變更且已 commit 並 push 到來源分支;false=暫存區與 HEAD 無差異,略過 commit/push。
|
||||||
@@ -274,13 +173,12 @@ function fileLastUpdatedIso(cwd, file) {
|
|||||||
* findings 檔與 `.gitea/ai-review/exclusions.json` 等結果檔提交回 PR 來源分支,
|
* findings 檔與 `.gitea/ai-review/exclusions.json` 等結果檔提交回 PR 來源分支,
|
||||||
* 並依回傳值記錄「已 commit/push」或「無實際變更、略過」的不同日誌。
|
* 並依回傳值記錄「已 commit/push」或「無實際變更、略過」的不同日誌。
|
||||||
*
|
*
|
||||||
* 安全注意:帶認證的推送一律透過 {@link pushWithCredential} 進行——認證只以
|
* 安全注意:push 重試時組出的 URL 內含 token(形如
|
||||||
* 環境變數(`http.<url>.extraheader` 的 base64 Basic)傳入,**不進 argv**,
|
* `https://ai-review-bot:<token>@host/owner/repo.git`),
|
||||||
* 推送目標 URL 本身不含帳密;且 push 失敗時改拋固定訊息,避免 `execFileSync`
|
* 絕對不得將此 URL 輸出到日誌、錯誤訊息或任何 action 輸出,以免洩漏 token;
|
||||||
* 例外把命令列(含 token)回顯到 CI log 或程序清單。
|
* 若需記錄重試行為,只能記載「改用帶認證 URL 重試」而不得包含 URL 本身。
|
||||||
*/
|
*/
|
||||||
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);
|
||||||
@@ -298,66 +196,17 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
|
|||||||
'-c', 'user.email=ai-review-bot@noreply.gitea',
|
'-c', 'user.email=ai-review-bot@noreply.gitea',
|
||||||
'commit', '-m', message,
|
'commit', '-m', message,
|
||||||
);
|
);
|
||||||
const refspec = `HEAD:refs/heads/${headRef}`;
|
|
||||||
const remoteUrl = `${serverUrl}/${repository}.git`;
|
|
||||||
// 一律以 token 的身分明確認證推送,不沿用 origin 的自動 token。
|
|
||||||
pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 以帶認證的方式推送到指定遠端,認證資訊只經環境變數傳入、不進命令列 argv。
|
|
||||||
*
|
|
||||||
* 認證方式:等同 `https://ai-review-bot:<secret>@host/...` 的 HTTP Basic(git 會把
|
|
||||||
* URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的
|
|
||||||
* `GIT_CONFIG_*` 環境變數注入 `http.<serverUrl>/.extraheader`,使 base64 憑證**不出現在 argv**
|
|
||||||
* (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。
|
|
||||||
*
|
|
||||||
* 觸發 CI 關鍵:`actions/checkout` 會把「自動 Actions token」持久化在同一個
|
|
||||||
* `http.<serverUrl>/.extraheader` scope;若沿用它推送,Gitea 會視為「自動 token 觸發」而
|
|
||||||
* **不再觸發 workflow**(防遞迴)。故本函式對這次 push 於該 scope**先以空值重置**(清掉自動
|
|
||||||
* token——git 對 extraHeader 給空值即清空既有清單),**再注入 PAT 的 Authorization**,讓推送以
|
|
||||||
* PAT 身分進行、觸發 PR 的 synchronize;作用範圍僅限本次 push 的環境變數,不影響 action 其他
|
|
||||||
* 仰賴 checkout 持久化憑證的 fetch(如 {@link resolveMergeBase})。
|
|
||||||
* 推送失敗時**不重拋原始例外**(其 message 會含命令列與遠端 URL),改拋固定訊息。
|
|
||||||
*
|
|
||||||
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
|
|
||||||
* @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。
|
|
||||||
* @param {string} token - 具 push 權限的 token/PAT(作為 Basic 認證的密碼)。
|
|
||||||
* @param {string} refspec - push 的 refspec(形如 `HEAD:refs/heads/<branch>`)。
|
|
||||||
* @param {string} serverUrl - Gitea 伺服器根網址(用於定位 checkout 持久化 extraheader 的 scope)。
|
|
||||||
* @returns {void} 成功即返回;失敗拋出不含 URL/argv/token 的固定錯誤。
|
|
||||||
* @throws {Error} 推送失敗時拋出固定訊息(已隱藏遠端 URL 與認證資訊)。
|
|
||||||
* @remarks 本函式未匯出,僅供 {@link commitAndPushFindings} 使用。
|
|
||||||
*/
|
|
||||||
function pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) {
|
|
||||||
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], {
|
git(cwd, 'push', 'origin', `HEAD:refs/heads/${headRef}`);
|
||||||
cwd,
|
|
||||||
encoding: 'utf8',
|
|
||||||
maxBuffer: 64 * 1024 * 1024,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
GIT_TERMINAL_PROMPT: '0',
|
|
||||||
// 先清空 checkout extraheader,再注入本次 PAT header。
|
|
||||||
GIT_CONFIG_COUNT: '2',
|
|
||||||
GIT_CONFIG_KEY_0: headerScope,
|
|
||||||
GIT_CONFIG_VALUE_0: '',
|
|
||||||
GIT_CONFIG_KEY_1: headerScope,
|
|
||||||
GIT_CONFIG_VALUE_1: `Authorization: Basic ${basic}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error('推送審查結果 commit 失敗(已隱藏遠端 URL 與認證資訊)。');
|
// 遠端未帶認證(checkout 未保留 credentials)時,改用帶 token 的 URL 重試。
|
||||||
|
// 注意:不得把這個 URL 輸出到日誌,避免洩漏 token。
|
||||||
|
const url = new URL(`${serverUrl}/${repository}.git`);
|
||||||
|
url.username = 'ai-review-bot';
|
||||||
|
url.password = token;
|
||||||
|
git(cwd, 'push', url.toString(), `HEAD:refs/heads/${headRef}`);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -367,7 +216,4 @@ module.exports = {
|
|||||||
fileDiff,
|
fileDiff,
|
||||||
fileLastUpdatedIso,
|
fileLastUpdatedIso,
|
||||||
commitAndPushFindings,
|
commitAndPushFindings,
|
||||||
__test: {
|
|
||||||
assertSafeBranchRef,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
+122
-156
@@ -5,7 +5,6 @@ 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 整理、攻擊方找問題、防守方裁決、排序分組與舊留言處理。
|
||||||
@@ -23,7 +22,7 @@ const TOTAL_DIFF_LIMIT = 160_000;
|
|||||||
* @param {string} workspace - 工作目錄絕對路徑(`.reviewignore` 所在的 repo 根目錄)。
|
* @param {string} workspace - 工作目錄絕對路徑(`.reviewignore` 所在的 repo 根目錄)。
|
||||||
* @returns {string[]} 忽略用的路徑前綴陣列;檔案不存在時為空陣列。
|
* @returns {string[]} 忽略用的路徑前綴陣列;檔案不存在時為空陣列。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 4」開頭由 `src/index.js` 呼叫,
|
* 使用情境:審查流程「步驟 3」開頭由 `src/index.js` 呼叫,
|
||||||
* 取得前綴清單後搭配 {@link isIgnored} 過濾 `gitrepo.changedFiles` 的結果,
|
* 取得前綴清單後搭配 {@link isIgnored} 過濾 `gitrepo.changedFiles` 的結果,
|
||||||
* 決定哪些變更檔案要納入送審。
|
* 決定哪些變更檔案要納入送審。
|
||||||
*/
|
*/
|
||||||
@@ -47,7 +46,7 @@ function loadReviewIgnore(workspace) {
|
|||||||
* @param {string[]} prefixes - 忽略路徑前綴清單(通常來自 {@link loadReviewIgnore})。
|
* @param {string[]} prefixes - 忽略路徑前綴清單(通常來自 {@link loadReviewIgnore})。
|
||||||
* @returns {boolean} `true` 表示忽略、不納入審查;`false` 表示送審。
|
* @returns {boolean} `true` 表示忽略、不納入審查;`false` 表示送審。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 4」中,`src/index.js` 以
|
* 使用情境:審查流程「步驟 3」中,`src/index.js` 以
|
||||||
* `allFiles.filter((file) => !review.isIgnored(file, ignores))`
|
* `allFiles.filter((file) => !review.isIgnored(file, ignores))`
|
||||||
* 過濾變更檔案清單,被排除的檔案數量會反映在變更摘要留言的排除統計。
|
* 過濾變更檔案清單,被排除的檔案數量會反映在變更摘要留言的排除統計。
|
||||||
*/
|
*/
|
||||||
@@ -71,7 +70,7 @@ function isIgnored(file, prefixes) {
|
|||||||
* @returns {Array<{file: string, purpose: string, lines: number, chars: number, truncated: boolean, lastUpdated: string, diffForPrompt: string}>}
|
* @returns {Array<{file: string, purpose: string, lines: number, chars: number, truncated: boolean, lastUpdated: string, diffForPrompt: string}>}
|
||||||
* 每檔一列的 diff 資料列;`purpose` 初始為「—」,由 {@link fillPurposes} 補齊。
|
* 每檔一列的 diff 資料列;`purpose` 初始為「—」,由 {@link fillPurposes} 補齊。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 4」由 `src/index.js` 呼叫,產出的 rows 同時餵給
|
* 使用情境:審查流程「步驟 3」由 `src/index.js` 呼叫,產出的 rows 同時餵給
|
||||||
* {@link fillPurposes}(補用途)、`templates.diffComment`(變更摘要留言)與
|
* {@link fillPurposes}(補用途)、`templates.diffComment`(變更摘要留言)與
|
||||||
* {@link buildAttackPrompt}(攻擊方提示的變更內容區塊)。
|
* {@link buildAttackPrompt}(攻擊方提示的變更內容區塊)。
|
||||||
*/
|
*/
|
||||||
@@ -87,12 +86,12 @@ function collectDiffRows({ cwd, files, base, gitrepo }) {
|
|||||||
if (diffForPrompt.length > PER_FILE_DIFF_LIMIT) {
|
if (diffForPrompt.length > PER_FILE_DIFF_LIMIT) {
|
||||||
diffForPrompt = `${diffForPrompt.slice(0, PER_FILE_DIFF_LIMIT)}\n...(diff 過長,其餘截斷未送審)`;
|
diffForPrompt = `${diffForPrompt.slice(0, PER_FILE_DIFF_LIMIT)}\n...(diff 過長,其餘截斷未送審)`;
|
||||||
truncated = true;
|
truncated = true;
|
||||||
log('步驟4', 'WRN', `${file} 的 diff 超過單檔上限(${chars} 字元),已截斷送審。`);
|
log('步驟3', 'WRN', `${file} 的 diff 超過單檔上限(${chars} 字元),已截斷送審。`);
|
||||||
}
|
}
|
||||||
if (totalChars + diffForPrompt.length > TOTAL_DIFF_LIMIT) {
|
if (totalChars + diffForPrompt.length > TOTAL_DIFF_LIMIT) {
|
||||||
diffForPrompt = '(全部 diff 總量超過送審上限,本檔內容未送審,僅列出檔名)';
|
diffForPrompt = '(全部 diff 總量超過送審上限,本檔內容未送審,僅列出檔名)';
|
||||||
truncated = true;
|
truncated = true;
|
||||||
log('步驟4', 'WRN', `${file} 因總量上限未送審 diff 內容。`);
|
log('步驟3', 'WRN', `${file} 因總量上限未送審 diff 內容。`);
|
||||||
} else {
|
} else {
|
||||||
totalChars += diffForPrompt.length;
|
totalChars += diffForPrompt.length;
|
||||||
}
|
}
|
||||||
@@ -122,7 +121,7 @@ function collectDiffRows({ cwd, files, base, gitrepo }) {
|
|||||||
* @param {Array<Object>} params.diffRows - {@link collectDiffRows} 產出的資料列;本函式會就地更新其 `purpose` 欄位。
|
* @param {Array<Object>} params.diffRows - {@link collectDiffRows} 產出的資料列;本函式會就地更新其 `purpose` 欄位。
|
||||||
* @returns {Promise<void>} 無回傳值;結果反映在 `diffRows` 的 `purpose` 欄位。
|
* @returns {Promise<void>} 無回傳值;結果反映在 `diffRows` 的 `purpose` 欄位。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 4」在 `collectDiffRows` 之後、發布
|
* 使用情境:審查流程「步驟 3」在 `collectDiffRows` 之後、發布
|
||||||
* `templates.diffComment` 變更摘要留言之前呼叫,讓摘要表格的「用途」欄有內容。
|
* `templates.diffComment` 變更摘要留言之前呼叫,讓摘要表格的「用途」欄有內容。
|
||||||
*/
|
*/
|
||||||
async function fillPurposes({ tool, model, cwd, diffRows }) {
|
async function fillPurposes({ tool, model, cwd, diffRows }) {
|
||||||
@@ -141,12 +140,12 @@ ${sections}
|
|||||||
- 不得輸出個資(PII)。`;
|
- 不得輸出個資(PII)。`;
|
||||||
const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 });
|
const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
log('步驟4', 'WRN', `檔案用途摘要產生失敗,以「—」代替:${agentFailureDetail(res)}。`);
|
log('步驟3', 'WRN', '檔案用途摘要產生失敗,以「—」代替。');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parsed = extractJson(res.output);
|
const parsed = extractJson(res.output);
|
||||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
log('步驟4', 'WRN', '檔案用途摘要回覆無法解析,以「—」代替。');
|
log('步驟3', 'WRN', '檔案用途摘要回覆無法解析,以「—」代替。');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const row of diffRows) {
|
for (const row of diffRows) {
|
||||||
@@ -164,7 +163,7 @@ ${sections}
|
|||||||
* @param {*} value - 攻擊方回覆的 severity 原始值(可能是任何型別;非字串會先轉字串)。
|
* @param {*} value - 攻擊方回覆的 severity 原始值(可能是任何型別;非字串會先轉字串)。
|
||||||
* @returns {'嚴重'|'警告'|'建議'} 收斂後的等級字串。
|
* @returns {'嚴重'|'警告'|'建議'} 收斂後的等級字串。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 6」中 {@link normalizeFinding} 檢核每條 finding 時呼叫,
|
* 使用情境:審查流程「步驟 5」中 {@link normalizeFinding} 檢核每條 finding 時呼叫,
|
||||||
* 確保後續 {@link sortFindings} 的 `templates.SEVERITY_ORDER` 排序、
|
* 確保後續 {@link sortFindings} 的 `templates.SEVERITY_ORDER` 排序、
|
||||||
* 「嚴重」分組(步驟 9 逐條留言 vs 步驟 10 彙整表格)都能以固定用詞比對。
|
* 「嚴重」分組(步驟 9 逐條留言 vs 步驟 10 彙整表格)都能以固定用詞比對。
|
||||||
* 本函式未匯出,僅供模組內部使用。
|
* 本函式未匯出,僅供模組內部使用。
|
||||||
@@ -189,7 +188,7 @@ function normalizeSeverity(value) {
|
|||||||
* @param {Array<Object>} diffRows - {@link collectDiffRows} 產出的送審資料列(file/purpose/lastUpdated/diffForPrompt)。
|
* @param {Array<Object>} diffRows - {@link collectDiffRows} 產出的送審資料列(file/purpose/lastUpdated/diffForPrompt)。
|
||||||
* @returns {string} 可直接餵給 `runAgent` stdin 的完整提示字串。
|
* @returns {string} 可直接餵給 `runAgent` stdin 的完整提示字串。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 6」{@link runAttackers} 為每個攻擊方角色各組一份提示,
|
* 使用情境:審查流程「步驟 5」{@link runAttackers} 為每個攻擊方角色各組一份提示,
|
||||||
* 並行送入 sub agent 找問題。本函式未匯出,僅供模組內部使用。
|
* 並行送入 sub agent 找問題。本函式未匯出,僅供模組內部使用。
|
||||||
*/
|
*/
|
||||||
function buildAttackPrompt(role, diffRows) {
|
function buildAttackPrompt(role, diffRows) {
|
||||||
@@ -234,7 +233,7 @@ ${sections}
|
|||||||
* @returns {?{reviewer: string, focus: string, badge: string, severity: string, file: string, startLine: number, endLine: number, problem: string, suggestion: string, suggestedCode: string}}
|
* @returns {?{reviewer: string, focus: string, badge: string, severity: string, file: string, startLine: number, endLine: number, problem: string, suggestion: string, suggestedCode: string}}
|
||||||
* 標準化後的 finding;輸入不合格時為 `null`。
|
* 標準化後的 finding;輸入不合格時為 `null`。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 6」{@link runAttackers} 解析每個攻擊方的 JSON 回覆後,
|
* 使用情境:審查流程「步驟 5」{@link runAttackers} 解析每個攻擊方的 JSON 回覆後,
|
||||||
* 逐條經本函式檢核,通過者才進入合併列表並編派 id,供防守方裁決與留言使用。
|
* 逐條經本函式檢核,通過者才進入合併列表並編派 id,供防守方裁決與留言使用。
|
||||||
* 本函式未匯出,僅供模組內部使用。
|
* 本函式未匯出,僅供模組內部使用。
|
||||||
*/
|
*/
|
||||||
@@ -257,7 +256,7 @@ function normalizeFinding(fromAgent, role) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 步驟 6:每個攻擊方角色一個 sub agent 並行分析送審 diff,合併為單一問題列表並編派 id。
|
* 步驟 5:每個攻擊方角色一個 sub agent 並行分析送審 diff,合併為單一問題列表並編派 id。
|
||||||
*
|
*
|
||||||
* 單一角色失敗(執行失敗或回覆無法解析為 JSON 陣列)只記 WRN 並以空結果代替,
|
* 單一角色失敗(執行失敗或回覆無法解析為 JSON 陣列)只記 WRN 並以空結果代替,
|
||||||
* 不阻斷其他角色(失敗降級行為);每條回覆先經 {@link normalizeFinding} 檢核,
|
* 不阻斷其他角色(失敗降級行為);每條回覆先經 {@link normalizeFinding} 檢核,
|
||||||
@@ -271,25 +270,25 @@ function normalizeFinding(fromAgent, role) {
|
|||||||
* @param {Array<Object>} params.diffRows - {@link collectDiffRows} 產出的送審資料列。
|
* @param {Array<Object>} params.diffRows - {@link collectDiffRows} 產出的送審資料列。
|
||||||
* @returns {Promise<Array<Object>>} 合併後的標準化 finding 列表(每條含 `id`);全部失敗或無問題時為空陣列。
|
* @returns {Promise<Array<Object>>} 合併後的標準化 finding 列表(每條含 `id`);全部失敗或無問題時為空陣列。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 6」由 `src/index.js` 在攻擊方登場留言後呼叫,
|
* 使用情境:審查流程「步驟 5」由 `src/index.js` 在攻擊方登場留言後呼叫,
|
||||||
* 結果直接交給步驟 8 的 {@link runDefenders} 裁決。
|
* 結果直接交給步驟 7 的 {@link runDefenders} 裁決。
|
||||||
*/
|
*/
|
||||||
async function runAttackers({ tool, model, cwd, attackers, diffRows }) {
|
async function runAttackers({ tool, model, cwd, attackers, diffRows }) {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
attackers.map(async (role) => {
|
attackers.map(async (role) => {
|
||||||
log('步驟6', 'INF', `攻擊方 ${role.meta.name} 開始分析。`);
|
log('步驟5', 'INF', `攻擊方 ${role.meta.name} 開始分析。`);
|
||||||
const res = await runAgent(tool, { model, prompt: buildAttackPrompt(role, diffRows), cwd });
|
const res = await runAgent(tool, { model, prompt: buildAttackPrompt(role, diffRows), cwd });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${agentFailureDetail(res)}。`);
|
log('步驟5', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${(res.error && res.error.message) || '未知錯誤'}。`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const parsed = extractJson(res.output);
|
const parsed = extractJson(res.output);
|
||||||
if (!Array.isArray(parsed)) {
|
if (!Array.isArray(parsed)) {
|
||||||
log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 回覆無法解析為 JSON 陣列,略過該角色結果。`);
|
log('步驟5', 'WRN', `攻擊方 ${role.meta.name} 回覆無法解析為 JSON 陣列,略過該角色結果。`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const list = parsed.map((f) => normalizeFinding(f, role)).filter(Boolean);
|
const list = parsed.map((f) => normalizeFinding(f, role)).filter(Boolean);
|
||||||
log('步驟6', 'INF', `攻擊方 ${role.meta.name} 完成:${list.length} 條問題。`);
|
log('步驟5', 'INF', `攻擊方 ${role.meta.name} 完成:${list.length} 條問題。`);
|
||||||
return list;
|
return list;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -297,7 +296,7 @@ async function runAttackers({ tool, model, cwd, attackers, diffRows }) {
|
|||||||
merged.forEach((finding, index) => {
|
merged.forEach((finding, index) => {
|
||||||
finding.id = `F${String(index + 1).padStart(3, '0')}`;
|
finding.id = `F${String(index + 1).padStart(3, '0')}`;
|
||||||
});
|
});
|
||||||
log('步驟6', 'INF', `全部攻擊方完成,合併後共 ${merged.length} 條問題。`);
|
log('步驟5', 'INF', `全部攻擊方完成,合併後共 ${merged.length} 條問題。`);
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +309,7 @@ async function runAttackers({ tool, model, cwd, attackers, diffRows }) {
|
|||||||
* @param {number} limit - 保留的最大字元數(超過即截斷)。
|
* @param {number} limit - 保留的最大字元數(超過即截斷)。
|
||||||
* @returns {string} 截斷後的檔案內容;檔案不存在時為空字串。
|
* @returns {string} 截斷後的檔案內容;檔案不存在時為空字串。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 8」{@link runDefenders} 以
|
* 使用情境:審查流程「步驟 7」{@link runDefenders} 以
|
||||||
* `readCapped(<cwd>/.gitea/ai-review/exclusions.json, 20_000)`
|
* `readCapped(<cwd>/.gitea/ai-review/exclusions.json, 20_000)`
|
||||||
* 讀取已知排除事項,嵌入 {@link buildDefendPrompt} 的防守方提示,
|
* 讀取已知排除事項,嵌入 {@link buildDefendPrompt} 的防守方提示,
|
||||||
* 避免排除清單過長撐爆提示。本函式未匯出,僅供模組內部使用。
|
* 避免排除清單過長撐爆提示。本函式未匯出,僅供模組內部使用。
|
||||||
@@ -332,7 +331,7 @@ function readCapped(filePath, limit) {
|
|||||||
* @param {string} cwd - 工作目錄(repo 根目錄,findings 目錄位於其下 `.gitea/ai-review/findings`)。
|
* @param {string} cwd - 工作目錄(repo 根目錄,findings 目錄位於其下 `.gitea/ai-review/findings`)。
|
||||||
* @returns {string} 歷史 findings 摘要文字(Markdown 區段 + JSON);無歷史時為空字串。
|
* @returns {string} 歷史 findings 摘要文字(Markdown 區段 + JSON);無歷史時為空字串。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 8」{@link runDefenders} 呼叫本函式取得歷史摘要,
|
* 使用情境:審查流程「步驟 7」{@link runDefenders} 呼叫本函式取得歷史摘要,
|
||||||
* 嵌入 {@link buildDefendPrompt},讓防守方能以「與歷史 findings 重複」為由裁決排除。
|
* 嵌入 {@link buildDefendPrompt},讓防守方能以「與歷史 findings 重複」為由裁決排除。
|
||||||
* 本函式未匯出,僅供模組內部使用。
|
* 本函式未匯出,僅供模組內部使用。
|
||||||
*/
|
*/
|
||||||
@@ -380,7 +379,7 @@ function loadHistory(cwd) {
|
|||||||
* @param {string} historyText - {@link loadHistory} 產出的歷史 findings 摘要;空字串時提示顯示「(無)」。
|
* @param {string} historyText - {@link loadHistory} 產出的歷史 findings 摘要;空字串時提示顯示「(無)」。
|
||||||
* @returns {string} 可直接餵給 `runAgent` stdin 的完整裁決提示字串。
|
* @returns {string} 可直接餵給 `runAgent` stdin 的完整裁決提示字串。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 8」{@link runDefenders} 為每個防守方角色各組一份提示,
|
* 使用情境:審查流程「步驟 7」{@link runDefenders} 為每個防守方角色各組一份提示,
|
||||||
* 並行送入 sub agent 逐條裁決是否可排除(重複或誤判)。本函式未匯出,僅供模組內部使用。
|
* 並行送入 sub agent 逐條裁決是否可排除(重複或誤判)。本函式未匯出,僅供模組內部使用。
|
||||||
*/
|
*/
|
||||||
function buildDefendPrompt(role, findings, exclusionsText, historyText) {
|
function buildDefendPrompt(role, findings, exclusionsText, historyText) {
|
||||||
@@ -425,7 +424,7 @@ ${JSON.stringify(minimal, null, 2)}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 步驟 8:每個防守方角色一個 sub agent 並行裁決 findings;
|
* 步驟 7:每個防守方角色一個 sub agent 並行裁決 findings;
|
||||||
* 「全部防守方都判可排除」才移除該條,其餘一律保留(保守原則)。
|
* 「全部防守方都判可排除」才移除該條,其餘一律保留(保守原則)。
|
||||||
*
|
*
|
||||||
* 失敗降級:某防守方執行失敗或回覆無法解析 → 該角色視為全部保留;
|
* 失敗降級:某防守方執行失敗或回覆無法解析 → 該角色視為全部保留;
|
||||||
@@ -442,7 +441,7 @@ ${JSON.stringify(minimal, null, 2)}
|
|||||||
* `kept`=保留(至少一位防守方不同意排除)、`excluded`=移除(全數防守方判可排除);
|
* `kept`=保留(至少一位防守方不同意排除)、`excluded`=移除(全數防守方判可排除);
|
||||||
* 兩邊元素都已附 `verdicts`。
|
* 兩邊元素都已附 `verdicts`。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 8」由 `src/index.js` 呼叫;`kept` 隨後經
|
* 使用情境:審查流程「步驟 7」由 `src/index.js` 呼叫;`kept` 隨後經
|
||||||
* {@link sortFindings} 排序、依「嚴重」分組發留言(步驟 9/10),
|
* {@link sortFindings} 排序、依「嚴重」分組發留言(步驟 9/10),
|
||||||
* `kept` 與 `excluded` 一併保存進 `.gitea/ai-review/findings/*.json`。
|
* `kept` 與 `excluded` 一併保存進 `.gitea/ai-review/findings/*.json`。
|
||||||
*/
|
*/
|
||||||
@@ -452,7 +451,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) {
|
|||||||
const historyText = loadHistory(cwd);
|
const historyText = loadHistory(cwd);
|
||||||
const verdictsPerDefender = await Promise.all(
|
const verdictsPerDefender = await Promise.all(
|
||||||
defenders.map(async (role) => {
|
defenders.map(async (role) => {
|
||||||
log('步驟8', 'INF', `防守方 ${role.meta.name} 開始裁決。`);
|
log('步驟7', 'INF', `防守方 ${role.meta.name} 開始裁決。`);
|
||||||
const res = await runAgent(tool, {
|
const res = await runAgent(tool, {
|
||||||
model,
|
model,
|
||||||
prompt: buildDefendPrompt(role, findings, exclusionsText, historyText),
|
prompt: buildDefendPrompt(role, findings, exclusionsText, historyText),
|
||||||
@@ -460,7 +459,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) {
|
|||||||
});
|
});
|
||||||
const verdicts = new Map();
|
const verdicts = new Map();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
log('步驟8', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留:${agentFailureDetail(res)}。`);
|
log('步驟7', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留。`);
|
||||||
return { role: role.meta.name, verdicts };
|
return { role: role.meta.name, verdicts };
|
||||||
}
|
}
|
||||||
const parsed = extractJson(res.output);
|
const parsed = extractJson(res.output);
|
||||||
@@ -474,9 +473,9 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log('步驟8', 'WRN', `防守方 ${role.meta.name} 回覆無法解析,該角色視為全部保留。`);
|
log('步驟7', 'WRN', `防守方 ${role.meta.name} 回覆無法解析,該角色視為全部保留。`);
|
||||||
}
|
}
|
||||||
log('步驟8', 'INF', `防守方 ${role.meta.name} 完成裁決。`);
|
log('步驟7', 'INF', `防守方 ${role.meta.name} 完成裁決。`);
|
||||||
return { role: role.meta.name, verdicts };
|
return { role: role.meta.name, verdicts };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -494,7 +493,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) {
|
|||||||
finding.verdicts = verdicts;
|
finding.verdicts = verdicts;
|
||||||
(allExclude ? excluded : kept).push(finding);
|
(allExclude ? excluded : kept).push(finding);
|
||||||
}
|
}
|
||||||
log('步驟8', 'INF', `裁決完成:保留 ${kept.length} 條、排除 ${excluded.length} 條。`);
|
log('步驟7', 'INF', `裁決完成:保留 ${kept.length} 條、排除 ${excluded.length} 條。`);
|
||||||
return { kept, excluded };
|
return { kept, excluded };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,7 +514,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) {
|
|||||||
* false=無排除問題、或既有檔案壞損/非陣列而略過寫入。
|
* false=無排除問題、或既有檔案壞損/非陣列而略過寫入。
|
||||||
* @throws {Error} 檔案系統寫入失敗(如權限不足)時由 fs 拋出,未攔截。
|
* @throws {Error} 檔案系統寫入失敗(如權限不足)時由 fs 拋出,未攔截。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:`main()`(src/index.js)於步驟 8 防守方裁決後呼叫本函式,
|
* 使用情境:`main()`(src/index.js)於步驟 7 防守方裁決後呼叫本函式,
|
||||||
* 並以回傳值決定收尾時是否把 exclusions.json 一併 commit
|
* 並以回傳值決定收尾時是否把 exclusions.json 一併 commit
|
||||||
* (一般模式:findings+exclusions.json;建問題模式:只 commit exclusions.json)。
|
* (一般模式:findings+exclusions.json;建問題模式:只 commit exclusions.json)。
|
||||||
*/
|
*/
|
||||||
@@ -528,11 +527,11 @@ function appendExclusions({ cwd, excluded, prNumber }) {
|
|||||||
try {
|
try {
|
||||||
entries = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
entries = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
} catch {
|
} catch {
|
||||||
log('步驟8', 'WRN', 'exclusions.json 無法解析,為避免破壞既有內容不附加誤判紀錄(需人工確認)。');
|
log('步驟7', 'WRN', 'exclusions.json 無法解析,為避免破壞既有內容不附加誤判紀錄(需人工確認)。');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!Array.isArray(entries)) {
|
if (!Array.isArray(entries)) {
|
||||||
log('步驟8', 'WRN', 'exclusions.json 非 JSON 陣列,為避免破壞既有內容不附加誤判紀錄(需人工確認)。');
|
log('步驟7', 'WRN', 'exclusions.json 非 JSON 陣列,為避免破壞既有內容不附加誤判紀錄(需人工確認)。');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -553,32 +552,10 @@ function appendExclusions({ cwd, excluded, prNumber }) {
|
|||||||
}
|
}
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
fs.writeFileSync(filePath, `${JSON.stringify(entries, null, 2)}\n`, 'utf8');
|
fs.writeFileSync(filePath, `${JSON.stringify(entries, null, 2)}\n`, 'utf8');
|
||||||
log('步驟8', 'INF', `已將 ${excluded.length} 條誤判/重複問題附加到 exclusions.json。`);
|
log('步驟7', 'INF', `已將 ${excluded.length} 條誤判/重複問題附加到 exclusions.json。`);
|
||||||
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:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。
|
||||||
*
|
*
|
||||||
@@ -588,7 +565,7 @@ function resultFilesToCommit({ createIssue, severeCount, relativePath, exclusion
|
|||||||
* @param {Array<{severity: string, file: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為 {@link runDefenders} 回傳的 `kept`)。
|
* @param {Array<{severity: string, file: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為 {@link runDefenders} 回傳的 `kept`)。
|
||||||
* @returns {void} 無回傳值;排序結果反映在傳入陣列本身。
|
* @returns {void} 無回傳值;排序結果反映在傳入陣列本身。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 8」裁決完成後、保存 findings 與分組發留言之前,
|
* 使用情境:審查流程「步驟 7」裁決完成後、保存 findings 與分組發留言之前,
|
||||||
* `src/index.js` 對 `kept` 呼叫本函式,確保步驟 9 逐條留言與步驟 10 彙整表格
|
* `src/index.js` 對 `kept` 呼叫本函式,確保步驟 9 逐條留言與步驟 10 彙整表格
|
||||||
* 都以「嚴重度優先、同檔集中、行號遞增」的穩定順序呈現。
|
* 都以「嚴重度優先、同檔集中、行號遞增」的穩定順序呈現。
|
||||||
*/
|
*/
|
||||||
@@ -601,6 +578,30 @@ function sortFindings(findings) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建問題模式的就地排序:依檔案路徑、再依嚴重等級(嚴重→警告→建議)、再依起始行遞增。
|
||||||
|
*
|
||||||
|
* 與 {@link sortFindings}(嚴重度優先)不同,本排序以檔案路徑為第一鍵,
|
||||||
|
* 讓 issue 上逐條留言的問題「同檔集中」,便於開發者逐檔處理。
|
||||||
|
* 嚴重等級權重取自 `templates.SEVERITY_ORDER`;未知等級排最後。
|
||||||
|
* 注意:直接修改傳入陣列(in-place),無回傳值。
|
||||||
|
*
|
||||||
|
* @param {Array<{file: string, severity: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為保留問題 `kept` 的複本)。
|
||||||
|
* @returns {void} 無回傳值;排序結果反映在傳入陣列本身。
|
||||||
|
* @remarks
|
||||||
|
* 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings}
|
||||||
|
* 先以 `[...findings]` 複製保留問題(不動原陣列的嚴重度排序),
|
||||||
|
* 再對複本呼叫本函式,依「檔案→嚴重度→行號」的順序逐條留言到新 issue。
|
||||||
|
*/
|
||||||
|
function sortFindingsForIssue(findings) {
|
||||||
|
findings.sort(
|
||||||
|
(a, b) =>
|
||||||
|
a.file.localeCompare(b.file) ||
|
||||||
|
(templates.SEVERITY_ORDER[a.severity] ?? 9) - (templates.SEVERITY_ORDER[b.severity] ?? 9) ||
|
||||||
|
a.startLine - b.startLine,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 建問題模式:以 AI 依 PR 標題/描述與問題列表摘要,
|
* 建問題模式:以 AI 依 PR 標題/描述與問題列表摘要,
|
||||||
* 從存取庫可用標籤中挑選適合掛在追蹤 issue 上的標籤子集合。
|
* 從存取庫可用標籤中挑選適合掛在追蹤 issue 上的標籤子集合。
|
||||||
@@ -619,9 +620,9 @@ function sortFindings(findings) {
|
|||||||
* @param {Array<Object>} params.findings - 保留的問題列表;每條取 severity/focus/file 與截斷 120 字的 problem 作為挑選依據。
|
* @param {Array<Object>} params.findings - 保留的問題列表;每條取 severity/focus/file 與截斷 120 字的 problem 作為挑選依據。
|
||||||
* @returns {Promise<number[]>} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。
|
* @returns {Promise<number[]>} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)
|
* 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings}
|
||||||
* 於確定有保留問題後、建立追蹤 issue 前先呼叫 `gitea.listLabels` 取得可用標籤,
|
* 先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式取得標籤 id 子集合,
|
||||||
* 再以本函式依保留問題挑出標籤 id 子集合,於 `gitea.createIssue` 建立 issue 時一次帶入。
|
* 傳給 `gitea.createIssue` 讓新 issue 自動掛上合適標籤。
|
||||||
*/
|
*/
|
||||||
async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) {
|
async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) {
|
||||||
if (labels.length === 0) return [];
|
if (labels.length === 0) return [];
|
||||||
@@ -656,7 +657,7 @@ ${JSON.stringify(brief)}
|
|||||||
- 沒有適合的標籤時輸出 []。`;
|
- 沒有適合的標籤時輸出 []。`;
|
||||||
const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 });
|
const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
log('建問題', 'WRN', `標籤挑選失敗,issue 不掛標籤:${agentFailureDetail(res)}。`);
|
log('建問題', 'WRN', '標籤挑選失敗,issue 不掛標籤。');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const parsed = extractJson(res.output);
|
const parsed = extractJson(res.output);
|
||||||
@@ -669,57 +670,61 @@ ${JSON.stringify(brief)}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 建問題模式:把嚴重 findings 逐條以一般留言發到追蹤 issue。
|
* 建問題模式:把保留的審查問題建成存取庫的追蹤 issue 並逐條留言明細。
|
||||||
*
|
*
|
||||||
* issue 無法把留言掛在程式碼行上(沒有 diff 定位),故改以
|
* 流程:AI 挑標籤(`listLabels` + {@link selectLabels},失敗不掛標籤)
|
||||||
* {@link templates.issueFindingComment} 在內文標明位置逐條發布——
|
* → 建立 issue(標題=PR 標題、本文=PR 描述加追溯資訊;失敗記 ERR 並回傳 null 不阻斷主流程)
|
||||||
* 等同一般模式 PR 步驟 9 的嚴重問題,改以 issue 留言呈現。
|
* → 複製 findings 依「檔案路徑→嚴重等級→起始行」排序({@link sortFindingsForIssue})
|
||||||
* findings 由呼叫端事先以 {@link sortFindings} 排序(嚴重度→檔案→行號),本函式不再排序。
|
* → 逐條以 `templates.issueFindingComment` 留言到 issue。
|
||||||
*
|
*
|
||||||
* @param {Object} params - 解構參數。
|
* @param {Object} params - 解構參數。
|
||||||
* @param {Object} params.ctx - 執行環境 context(`loadContext()` 回傳);供 Gitea API 認證。
|
* @param {Object} params.ctx - 執行環境 context(`loadContext()` 回傳);使用 `prNumber`、`prTitle`、`prBody` 及 Gitea API 認證欄位。
|
||||||
* @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `createCommentOnIssue`。
|
* @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `listLabels`、`createIssue`、`createCommentOnIssue`。
|
||||||
* @param {number} params.issueNumber - 目標追蹤 issue 的編號。
|
* @param {Object} params.tool - `detectTool()` 偵測到的 AI CLI 工具描述物件(挑標籤用)。
|
||||||
* @param {Array<Object>} params.severe - severity 為「嚴重」的 finding 列表(已排序;呼叫端保證非空)。
|
* @param {string} params.model - 指定 AI 模型名稱;空字串=工具預設。
|
||||||
* @returns {Promise<void>} 無回傳值;結果反映在 issue 留言與日誌。
|
* @param {string} params.cwd - agent 的工作目錄(repo 根目錄)。
|
||||||
* @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出,由主流程頂層 catch 收斂。
|
* @param {Array<Object>} params.findings - 要寫進 issue 的問題列表(通常為防守方裁決後保留的 `kept`);本函式以複本排序,不改動原陣列順序。
|
||||||
|
* @returns {Promise<object|null>} 建立成功的 Gitea issue 物件(含 `number` 等欄位);建立 issue 失敗時為 null。
|
||||||
|
* @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出;列標籤與建 issue 的失敗則已於函式內降級處理。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)於防守方裁決後
|
* 使用情境:`main()`(src/index.js)在步驟 10 之後、收尾之前,
|
||||||
* 建立追蹤 issue、寫入情境留言,再以本函式把嚴重問題逐條留言到該 issue;
|
* 於 `ctx.createIssue` 為 true 且 `kept.length > 0` 時呼叫本函式;
|
||||||
* 警告+建議則由 {@link postOthersToIssue} 同樣逐條留言到同一 issue(皆可個別回覆),
|
* 此模式下問題明細已保存在 issue 留言,收尾只 commit exclusions.json、findings 檔不進版控。
|
||||||
* 不使用 {@link templates.othersComment} 的單一表格——表格僅用於一般模式(PR)。
|
|
||||||
*/
|
*/
|
||||||
async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
|
async function createIssueWithFindings({ ctx, gitea, tool, model, cwd, findings }) {
|
||||||
for (const finding of severe) {
|
let labelIds = [];
|
||||||
await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding));
|
try {
|
||||||
|
const labels = await gitea.listLabels(ctx);
|
||||||
|
labelIds = await selectLabels({
|
||||||
|
tool,
|
||||||
|
model,
|
||||||
|
cwd,
|
||||||
|
labels,
|
||||||
|
prTitle: ctx.prTitle,
|
||||||
|
prBody: ctx.prBody,
|
||||||
|
findings,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
log('建問題', 'WRN', `取得存取庫標籤失敗(${err.message}),issue 不掛標籤。`);
|
||||||
}
|
}
|
||||||
log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}。`);
|
let issue;
|
||||||
}
|
try {
|
||||||
|
issue = await gitea.createIssue(ctx, {
|
||||||
/**
|
title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`,
|
||||||
* 建問題模式:把警告+建議(非嚴重)findings 逐條以獨立留言發到追蹤 issue。
|
body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
|
||||||
*
|
labels: labelIds,
|
||||||
* 與一般模式(PR)把警告+建議彙整成單一表格({@link templates.othersComment})不同:
|
});
|
||||||
* issue 內每條問題各發一則留言({@link templates.issueFindingComment},內文標明位置),
|
} catch (err) {
|
||||||
* 讓開發者能針對「單一問題」直接回覆討論,而非只能回覆一整張表格。
|
log('建問題', 'ERR', `建立 issue 失敗:${err.message}。`);
|
||||||
* findings 由呼叫端事先以 {@link sortFindings} 排序(嚴重度→檔案→行號),本函式不再排序。
|
return null;
|
||||||
*
|
|
||||||
* @param {Object} params - 解構參數。
|
|
||||||
* @param {Object} params.ctx - 執行環境 context(`loadContext()` 回傳);供 Gitea API 認證。
|
|
||||||
* @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `createCommentOnIssue`。
|
|
||||||
* @param {number} params.issueNumber - 目標追蹤 issue 的編號。
|
|
||||||
* @param {Array<Object>} params.others - severity 非「嚴重」(警告+建議)的 finding 列表(已排序;呼叫端保證非空)。
|
|
||||||
* @returns {Promise<void>} 無回傳值;結果反映在 issue 留言與日誌。
|
|
||||||
* @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出,由主流程頂層 catch 收斂。
|
|
||||||
* @remarks
|
|
||||||
* 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)步驟 10
|
|
||||||
* 以本函式把警告+建議逐條留言到追蹤 issue,確保 issue 上每條問題都是可個別回覆的留言。
|
|
||||||
*/
|
|
||||||
async function postOthersToIssue({ ctx, gitea, issueNumber, others }) {
|
|
||||||
for (const finding of others) {
|
|
||||||
await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding));
|
|
||||||
}
|
}
|
||||||
log('步驟10', 'INF', `已將 ${others.length} 條警告+建議逐條留言到 issue #${issueNumber}。`);
|
const sorted = [...findings];
|
||||||
|
sortFindingsForIssue(sorted);
|
||||||
|
for (const finding of sorted) {
|
||||||
|
await gitea.createCommentOnIssue(ctx, issue.number, templates.issueFindingComment(finding));
|
||||||
|
}
|
||||||
|
log('建問題', 'INF', `issue #${issue.number} 已建立並逐條留言 ${sorted.length} 條問題。`);
|
||||||
|
return issue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -753,7 +758,7 @@ function readSnippet(cwd, finding) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 步驟 2:將 PR 既有的 bot 留言標記為已解決,本回合剛發的留言除外。
|
* 步驟 8:將 PR 既有的 bot 留言標記為已解決,本回合剛發的留言除外。
|
||||||
*
|
*
|
||||||
* 兩類處理:
|
* 兩類處理:
|
||||||
* - 一般留言(bot 發、含隱藏標記、非本回合、尚未標註)→ 編輯加上「〔已過時〕」前綴。
|
* - 一般留言(bot 發、含隱藏標記、非本回合、尚未標註)→ 編輯加上「〔已過時〕」前綴。
|
||||||
@@ -764,21 +769,18 @@ function readSnippet(cwd, finding) {
|
|||||||
* @param {Object} params - 解構參數。
|
* @param {Object} params - 解構參數。
|
||||||
* @param {Object} params.ctx - 執行環境 context(`loadContext()` 產出,含 repo/PR 編號/token 等 API 呼叫所需資訊)。
|
* @param {Object} params.ctx - 執行環境 context(`loadContext()` 產出,含 repo/PR 編號/token 等 API 呼叫所需資訊)。
|
||||||
* @param {Object} params.gitea - Gitea API 模組(`src/lib/gitea.js`),需提供 `whoAmI`/`listIssueComments`/`editIssueComment`/`listReviews`/`listReviewComments`/`tryResolveReviewComment`;以參數注入便於測試替換。
|
* @param {Object} params.gitea - Gitea API 模組(`src/lib/gitea.js`),需提供 `whoAmI`/`listIssueComments`/`editIssueComment`/`listReviews`/`listReviewComments`/`tryResolveReviewComment`;以參數注入便於測試替換。
|
||||||
* @param {Set<number>} params.currentRunCommentIds - 本回合已發出的一般留言 id 集合;這些留言不標註過時。
|
* @param {Set<number>} params.currentRunCommentIds - 本回合發出的一般留言 id 集合;這些留言不標註過時。
|
||||||
* @returns {Promise<void>} 無回傳值;結果反映在 PR 留言狀態與日誌。
|
* @returns {Promise<void>} 無回傳值;結果反映在 PR 留言狀態與日誌。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:一般模式下,`src/index.js` 於「確定本回合審查已成功產生結果後、發布嚴重/
|
* 使用情境:審查流程「步驟 8」在防守方裁決、保存 findings 之後、
|
||||||
* 其他問題留言之前」呼叫(見 `main()`),刻意延後到工具偵測、diff 整理與攻防裁決都成功之後,
|
* 發布本回合嚴重問題留言(步驟 9)之前呼叫,確保 PR 上只有最新回合的審查結果醒目可見。
|
||||||
* 避免任一前置步驟失敗時舊結果已被清掉、PR 卻沒有新結果。此時本回合的工具/diff/角色留言
|
|
||||||
* 已發出並登錄於 `currentRunCommentIds`,本函式據此排除、不會把這些「新產生的留言」誤標為過時;
|
|
||||||
* 之後才發布的嚴重/其他問題留言更不受影響,確保 PR 上只有最新回合的審查結果醒目可見。
|
|
||||||
*/
|
*/
|
||||||
async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
|
async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
|
||||||
let botLogin = '';
|
let botLogin = '';
|
||||||
try {
|
try {
|
||||||
botLogin = (await gitea.whoAmI(ctx)).login || '';
|
botLogin = (await gitea.whoAmI(ctx)).login || '';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('步驟2', 'WRN', `無法取得 bot 身分(${err.message}),略過留言解決。`);
|
log('步驟8', 'WRN', `無法取得 bot 身分(${err.message}),略過留言解決。`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,9 +797,9 @@ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
|
|||||||
await gitea.editIssueComment(ctx, comment.id, `${templates.OUTDATED_PREFIX}${comment.body}`);
|
await gitea.editIssueComment(ctx, comment.id, `${templates.OUTDATED_PREFIX}${comment.body}`);
|
||||||
outdatedCount += 1;
|
outdatedCount += 1;
|
||||||
}
|
}
|
||||||
log('步驟2', 'INF', `一般留言已標註〔已過時〕:${outdatedCount} 則。`);
|
log('步驟8', 'INF', `一般留言已標註〔已過時〕:${outdatedCount} 則。`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('步驟2', 'WRN', `標註一般留言失敗:${err.message}。`);
|
log('步驟8', 'WRN', `標註一般留言失敗:${err.message}。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// review 程式碼留言:盡力 resolve;API 不支援(第一次就失敗)即停止嘗試。
|
// review 程式碼留言:盡力 resolve;API 不支援(第一次就失敗)即停止嘗試。
|
||||||
@@ -823,46 +825,12 @@ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (resolveSupported) {
|
if (resolveSupported) {
|
||||||
log('步驟2', 'INF', `review 程式碼留言已解決:${resolvedCount} 則。`);
|
log('步驟8', 'INF', `review 程式碼留言已解決:${resolvedCount} 則。`);
|
||||||
} else {
|
} else {
|
||||||
log('步驟2', 'WRN', 'Gitea 版本不支援 resolve API,review 程式碼留言維持原狀(已解決 ' + resolvedCount + ' 則)。');
|
log('步驟8', 'WRN', 'Gitea 版本不支援 resolve API,review 程式碼留言維持原狀(已解決 ' + resolvedCount + ' 則)。');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('步驟2', 'WRN', `解決 review 留言失敗:${err.message}。`);
|
log('步驟8', 'WRN', `解決 review 留言失敗:${err.message}。`);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 建問題模式:只將 PR 上舊的「追蹤問題連結」留言標註為過時,不觸碰 issue 內審查內容。
|
|
||||||
*
|
|
||||||
* @param {Object} params - 解構參數。
|
|
||||||
* @param {Object} params.ctx - 執行環境 context(`loadContext()` 產出)。
|
|
||||||
* @param {Object} params.gitea - Gitea API 模組,需提供 `whoAmI`、`listIssueComments`、`editIssueComment`。
|
|
||||||
* @returns {Promise<void>} 無回傳值;失敗時只記 WRN,不阻斷主流程。
|
|
||||||
*/
|
|
||||||
async function resolveOldIssueLinkComments({ ctx, gitea }) {
|
|
||||||
let botLogin = '';
|
|
||||||
try {
|
|
||||||
botLogin = (await gitea.whoAmI(ctx)).login || '';
|
|
||||||
} catch (err) {
|
|
||||||
log('建問題', 'WRN', `無法取得 bot 身分(${err.message}),略過舊追蹤連結標註。`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const comments = await gitea.listIssueComments(ctx);
|
|
||||||
let outdatedCount = 0;
|
|
||||||
for (const comment of comments) {
|
|
||||||
const isBot = comment.user && comment.user.login === botLogin;
|
|
||||||
const body = typeof comment.body === 'string' ? comment.body : '';
|
|
||||||
const isIssueLink = body.includes(templates.MARK) && body.includes('## 🔍 AI Code Review|已建立追蹤問題');
|
|
||||||
if (!isBot || !isIssueLink || body.startsWith(templates.OUTDATED_PREFIX)) continue;
|
|
||||||
await gitea.editIssueComment(ctx, comment.id, `${templates.OUTDATED_PREFIX}${body}`);
|
|
||||||
outdatedCount += 1;
|
|
||||||
}
|
|
||||||
log('建問題', 'INF', `舊追蹤 issue 連結已標註〔已過時〕:${outdatedCount} 則。`);
|
|
||||||
} catch (err) {
|
|
||||||
log('建問題', 'WRN', `標註舊追蹤 issue 連結失敗:${err.message}。`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -913,11 +881,9 @@ module.exports = {
|
|||||||
runDefenders,
|
runDefenders,
|
||||||
sortFindings,
|
sortFindings,
|
||||||
appendExclusions,
|
appendExclusions,
|
||||||
resultFilesToCommit,
|
sortFindingsForIssue,
|
||||||
selectLabels,
|
selectLabels,
|
||||||
postSevereToIssue,
|
createIssueWithFindings,
|
||||||
postOthersToIssue,
|
|
||||||
resolveOldComments,
|
resolveOldComments,
|
||||||
resolveOldIssueLinkComments,
|
|
||||||
postSevereComments,
|
postSevereComments,
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ const path = require('path');
|
|||||||
* @throws {Error} 當 `rolesDir` 不存在、無法讀取,或個別檔案讀取失敗時,
|
* @throws {Error} 當 `rolesDir` 不存在、無法讀取,或個別檔案讀取失敗時,
|
||||||
* 由 `fs.readdirSync` / `fs.readFileSync` 直接拋出(未在函式內捕捉)。
|
* 由 `fs.readdirSync` / `fs.readFileSync` 直接拋出(未在函式內捕捉)。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:`src/index.js` 於審查流程步驟 5 呼叫
|
* 使用情境:`src/index.js` 於審查流程步驟 4 呼叫
|
||||||
* `loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'))` 載入全部角色,
|
* `loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'))` 載入全部角色,
|
||||||
* 再以 {@link attackersOf} / {@link defendersOf} 依 frontmatter 的 `side` 欄位
|
* 再以 {@link attackersOf} / {@link defendersOf} 依 frontmatter 的 `side` 欄位
|
||||||
* 分出攻擊方(Mage/Assassin/Rogue/Bard/Leo/Maya)與防守方(Paladin),
|
* 分出攻擊方(Mage/Assassin/Rogue/Bard/Leo/Maya)與防守方(Paladin),
|
||||||
|
|||||||
+17
-42
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
// 固定留言模板:本 action 發到 PR 的留言一律由此產生(繁體中文、UTF-8、表格優先)。
|
// 固定留言模板:本 action 發到 PR 的留言一律由此產生(繁體中文、UTF-8、表格優先)。
|
||||||
|
|
||||||
// 隱藏標記:辨識哪些留言是本 action 發的(步驟 2 標註過時時使用)。
|
// 隱藏標記:辨識哪些留言是本 action 發的(步驟 8 標註過時時使用)。
|
||||||
const MARK = '<!-- ai-code-review -->';
|
const MARK = '<!-- ai-code-review -->';
|
||||||
|
|
||||||
// 舊留言標註前綴(步驟 2 的降級做法:無 resolve API 時編輯加註)。
|
// 舊留言標註前綴(步驟 8 的降級做法:無 resolve API 時編輯加註)。
|
||||||
const OUTDATED_PREFIX = '> 〔已過時〕本留言屬於較舊的審查回合。\n\n';
|
const OUTDATED_PREFIX = '> 〔已過時〕本留言屬於較舊的審查回合。\n\n';
|
||||||
|
|
||||||
// 嚴重等級對應的 emoji 與排序權重。
|
// 嚴重等級對應的 emoji 與排序權重。
|
||||||
@@ -33,7 +33,7 @@ const FOCUS_LABEL = {
|
|||||||
* @param {*} text - 任意待處理內容;非字串會先以 `String()` 轉型,null/undefined 視為空字串。
|
* @param {*} text - 任意待處理內容;非字串會先以 `String()` 轉型,null/undefined 視為空字串。
|
||||||
* @returns {string} 已逸出、單行化的儲存格內容;若結果為空則回傳 `'—'`。
|
* @returns {string} 已逸出、單行化的儲存格內容;若結果為空則回傳 `'—'`。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程中所有表格型留言的共用防呆——例如步驟 4 的
|
* 使用情境:審查流程中所有表格型留言的共用防呆——例如步驟 3 的
|
||||||
* `diffComment()` 產生變更摘要表格時,檔名與用途欄位都經本函式處理,
|
* `diffComment()` 產生變更摘要表格時,檔名與用途欄位都經本函式處理,
|
||||||
* 避免檔名或 AI 產生的描述含 `|` 或換行而撐破 Markdown 表格。
|
* 避免檔名或 AI 產生的描述含 `|` 或換行而撐破 Markdown 表格。
|
||||||
* 本函式未匯出,僅供模組內部使用。
|
* 本函式未匯出,僅供模組內部使用。
|
||||||
@@ -55,7 +55,7 @@ function cell(text) {
|
|||||||
* @param {string} focus - 審查面向代碼(例如 `'logic'`、`'security'`);可為 undefined。
|
* @param {string} focus - 審查面向代碼(例如 `'logic'`、`'security'`);可為 undefined。
|
||||||
* @returns {string} 顯示字串:命中時如 `'邏輯(logic)'`;未命中時原樣回傳 `focus`;falsy 時回傳 `'—'`。
|
* @returns {string} 顯示字串:命中時如 `'邏輯(logic)'`;未命中時原樣回傳 `focus`;falsy 時回傳 `'—'`。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程步驟 5/7 的角色登場留言——`rolesComment()`
|
* 使用情境:審查流程步驟 4/6 的角色登場留言——`rolesComment()`
|
||||||
* 產生「角色|面向|個性」表格時,以本函式把每位審查員
|
* 產生「角色|面向|個性」表格時,以本函式把每位審查員
|
||||||
* (攻擊方/防守方)的 focus 代碼轉成中英並列的面向欄位內容。
|
* (攻擊方/防守方)的 focus 代碼轉成中英並列的面向欄位內容。
|
||||||
* 本函式未匯出,僅供模組內部使用。
|
* 本函式未匯出,僅供模組內部使用。
|
||||||
@@ -66,7 +66,7 @@ function focusLabel(focus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 產生審查流程步驟 3 的「審查工具」PR 留言內容。
|
* 產生審查流程步驟 2 的「審查工具」PR 留言內容。
|
||||||
*
|
*
|
||||||
* 留言以隱藏標記 `MARK` 開頭,包含工具資訊表格(工具/版本/模型/
|
* 留言以隱藏標記 `MARK` 開頭,包含工具資訊表格(工具/版本/模型/
|
||||||
* 審查 commit/Run Job 連結)與一張 mermaid 流程圖,說明整條審查管線
|
* 審查 commit/Run Job 連結)與一張 mermaid 流程圖,說明整條審查管線
|
||||||
@@ -81,9 +81,9 @@ function focusLabel(focus) {
|
|||||||
* @param {string} params.runLink - CI run 的網址,直接內插為 Markdown 連結目標。
|
* @param {string} params.runLink - CI run 的網址,直接內插為 Markdown 連結目標。
|
||||||
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記,結尾帶換行)。
|
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記,結尾帶換行)。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程步驟 3——每回合審查開始時,先把工具身分與
|
* 使用情境:審查流程步驟 2——每回合審查開始時,先把工具身分與
|
||||||
* 管線流程圖留言到 PR,讓開發者知道這回合由哪個版本/模型執行;
|
* 管線流程圖留言到 PR,讓開發者知道這回合由哪個版本/模型執行;
|
||||||
* 留言開頭的 MARK 讓步驟 2 能辨識並將舊回合留言標註為過時。
|
* 留言開頭的 MARK 讓步驟 8 能辨識並將舊回合留言標註為過時。
|
||||||
*/
|
*/
|
||||||
function toolComment({ toolName, version, model, sha, runNumber, runLink }) {
|
function toolComment({ toolName, version, model, sha, runNumber, runLink }) {
|
||||||
return `${MARK}
|
return `${MARK}
|
||||||
@@ -108,7 +108,7 @@ flowchart LR
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 產生審查流程步驟 4 的「變更摘要(送審 git diff)」PR 留言內容。
|
* 產生審查流程步驟 3 的「變更摘要(送審 git diff)」PR 留言內容。
|
||||||
*
|
*
|
||||||
* 以四欄表格(檔案/用途/git diff 長度/最後更新時間)列出本回合
|
* 以四欄表格(檔案/用途/git diff 長度/最後更新時間)列出本回合
|
||||||
* 送審的每個檔案;diff 過長被截斷送審的檔案會加註「(過長截斷送審)」,
|
* 送審的每個檔案;diff 過長被截斷送審的檔案會加註「(過長截斷送審)」,
|
||||||
@@ -125,7 +125,7 @@ flowchart LR
|
|||||||
* @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示排除註記。
|
* @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示排除註記。
|
||||||
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
|
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程步驟 4——整理完 git diff 後,把「哪些檔案、多長、
|
* 使用情境:審查流程步驟 3——整理完 git diff 後,把「哪些檔案、多長、
|
||||||
* 是否截斷、哪些被 .reviewignore 排除」留言到 PR,讓開發者確認送審範圍
|
* 是否截斷、哪些被 .reviewignore 排除」留言到 PR,讓開發者確認送審範圍
|
||||||
* 與 AI 實際看到的內容一致。
|
* 與 AI 實際看到的內容一致。
|
||||||
*/
|
*/
|
||||||
@@ -150,10 +150,10 @@ function diffComment(rows, ignoredCount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 產生審查流程步驟 5/7 共用的「角色登場」PR 留言內容。
|
* 產生審查流程步驟 4/6 共用的「角色登場」PR 留言內容。
|
||||||
*
|
*
|
||||||
* 以三欄表格(角色/面向/個性)列出本回合登場的審查員;
|
* 以三欄表格(角色/面向/個性)列出本回合登場的審查員;
|
||||||
* 攻擊方(步驟 5)與防守方(步驟 7)共用本模板,僅標題不同。
|
* 攻擊方(步驟 4)與防守方(步驟 6)共用本模板,僅標題不同。
|
||||||
* 面向欄位經 focusLabel() 轉成「中文(原文)」並列格式。
|
* 面向欄位經 focusLabel() 轉成「中文(原文)」並列格式。
|
||||||
*
|
*
|
||||||
* @param {Object} params - 留言內容(解構參數)。
|
* @param {Object} params - 留言內容(解構參數)。
|
||||||
@@ -166,7 +166,7 @@ function diffComment(rows, ignoredCount) {
|
|||||||
* @param {string} params.roles[].meta.personality - 角色個性描述;經 cell() 防呆。
|
* @param {string} params.roles[].meta.personality - 角色個性描述;經 cell() 防呆。
|
||||||
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
|
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程步驟 5(攻擊方登場)與步驟 7(防守方登場)——
|
* 使用情境:審查流程步驟 4(攻擊方登場)與步驟 6(防守方登場)——
|
||||||
* 在各階段開始審查前,把該回合參與的審查員角色、負責面向與個性
|
* 在各階段開始審查前,把該回合參與的審查員角色、負責面向與個性
|
||||||
* 留言到 PR,讓開發者理解後續 findings 是由哪些視角產出的。
|
* 留言到 PR,讓開發者理解後續 findings 是由哪些視角產出的。
|
||||||
*/
|
*/
|
||||||
@@ -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)的 `createIssueAndFlushBufferedComments` 建立 issue 時,
|
* 使用情境:建問題模式下 `createIssueWithFindings`(src/lib/review.js)建立 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` 與 `review.postOthersToIssue`
|
* 使用情境:建問題模式下 `createIssueWithFindings`(src/lib/review.js)建立 issue 後,
|
||||||
* 把每條 finding 以本函式產生留言內容、經 `gitea.createCommentOnIssue`
|
* 把保留的 findings 依「檔案路徑→嚴重等級→起始行」排序,逐條以本函式產生留言內容、
|
||||||
* 發布到追蹤 issue 上,作為問題明細的追蹤紀錄。
|
* 經 `gitea.createCommentOnIssue` 發布到新 issue 上,作為問題明細的追蹤紀錄。
|
||||||
*/
|
*/
|
||||||
function issueFindingComment(finding) {
|
function issueFindingComment(finding) {
|
||||||
const emoji = SEVERITY_EMOJI[finding.severity] || '🔵';
|
const emoji = SEVERITY_EMOJI[finding.severity] || '🔵';
|
||||||
@@ -371,7 +371,7 @@ function issueFindingComment(finding) {
|
|||||||
* @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示「(N 個檔案被排除)」註記。
|
* @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示「(N 個檔案被排除)」註記。
|
||||||
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
|
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程步驟 4 的替代路徑——整理 git diff 時發現
|
* 使用情境:審查流程步驟 3 的替代路徑——整理 git diff 時發現
|
||||||
* 過濾後送審清單為空(例如整包變更都被 .reviewignore 排除),
|
* 過濾後送審清單為空(例如整包變更都被 .reviewignore 排除),
|
||||||
* 直接以本留言告知開發者本回合視為審查通過,不再進入
|
* 直接以本留言告知開發者本回合視為審查通過,不再進入
|
||||||
* 攻擊方/防守方審查階段。
|
* 攻擊方/防守方審查階段。
|
||||||
@@ -383,30 +383,6 @@ function nothingToReviewComment(ignoredCount) {
|
|||||||
本次 PR 套用 \`.reviewignore\` 後**沒有可審查的變更**${ignoredCount > 0 ? `(${ignoredCount} 個檔案被排除)` : ''},視為審查通過。`;
|
本次 PR 套用 \`.reviewignore\` 後**沒有可審查的變更**${ignoredCount > 0 ? `(${ignoredCount} 個檔案被排除)` : ''},視為審查通過。`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 產生建問題模式(input: create-issue)下,回貼到「PR」的追蹤問題連結留言。
|
|
||||||
*
|
|
||||||
* 建問題模式把審查內容全部發到 issue、不留在 PR;本留言是 PR 上唯一的一則審查留言,
|
|
||||||
* 提供 issue 連結與問題數量統計,讓 PR 讀者一眼看到「本次審查結果在哪個 issue」。
|
|
||||||
*
|
|
||||||
* @param {Object} params - 解構參數。
|
|
||||||
* @param {number} params.issueNumber - 追蹤問題的 issue 編號;內插為 Markdown 連結文字。
|
|
||||||
* @param {string} params.issueUrl - 追蹤問題的 issue 網址;作為 Markdown 連結目標。
|
|
||||||
* @param {number} params.severeCount - 嚴重問題條數,顯示在統計。
|
|
||||||
* @param {number} params.otherCount - 警告+建議問題條數,顯示在統計。
|
|
||||||
* @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。
|
|
||||||
* @remarks
|
|
||||||
* 使用情境:建問題模式下 `main()`(src/index.js)在 issue 建立並寫入全部審查內容後,
|
|
||||||
* 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以
|
|
||||||
* {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。
|
|
||||||
*/
|
|
||||||
function prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {
|
|
||||||
return `${MARK}
|
|
||||||
## 🔍 AI Code Review|已建立追蹤問題
|
|
||||||
|
|
||||||
本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
MARK,
|
MARK,
|
||||||
OUTDATED_PREFIX,
|
OUTDATED_PREFIX,
|
||||||
@@ -421,5 +397,4 @@ module.exports = {
|
|||||||
issueBody,
|
issueBody,
|
||||||
issueFindingComment,
|
issueFindingComment,
|
||||||
nothingToReviewComment,
|
nothingToReviewComment,
|
||||||
prIssueLinkComment,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
'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',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
'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'),
|
|
||||||
/不是安全的分支名稱/,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
'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, /stderr:Authorization: \*\*\*/);
|
|
||||||
assert.match(detail, /stdout:token=\*\*\*/);
|
|
||||||
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'],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('resolveOldIssueLinkComments 只標註舊追蹤 issue 連結', async () => {
|
|
||||||
const edited = [];
|
|
||||||
const fakeGitea = {
|
|
||||||
async whoAmI() {
|
|
||||||
return { login: 'bot' };
|
|
||||||
},
|
|
||||||
async listIssueComments() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
user: { login: 'bot' },
|
|
||||||
body: '<!-- ai-code-review -->\n## 🔍 AI Code Review|已建立追蹤問題\nold',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
user: { login: 'bot' },
|
|
||||||
body: '<!-- ai-code-review -->\n## 📋 變更摘要(送審 git diff)\nkeep',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
user: { login: 'someone' },
|
|
||||||
body: '<!-- ai-code-review -->\n## 🔍 AI Code Review|已建立追蹤問題\nkeep',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
async editIssueComment(ctx, commentId, body) {
|
|
||||||
edited.push({ ctx, commentId, body });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await review.resolveOldIssueLinkComments({ ctx: { token: 'hidden' }, gitea: fakeGitea });
|
|
||||||
|
|
||||||
assert.equal(edited.length, 1);
|
|
||||||
assert.equal(edited[0].commentId, 1);
|
|
||||||
assert.match(edited[0].body, /^> 〔已過時〕/);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user