Author SHA1 Message Date
Jeffery 43ad2b56d2 chore(ai-review 狀態): 回寫本輪 findings 與排除事項
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Successful in 33s
CI / TEST (Antigravity) (pull_request) Successful in 49s
CI / TEST (Codex) (pull_request) Successful in 3m38s
2026-07-21 16:26:30 +08:00
Jeffery 8e18bbacc2 style(action manifest): 統一中文斜線標點 2026-07-21 16:26:30 +08:00
Jeffery 1f012c6cfb refactor(gitref): 將分支名稱驗證改為正式模組 2026-07-21 16:26:30 +08:00
ai-review-bot 76617dd9dc chore: update ai-review findings [ai-review-bot][failure]
node-actions/template: CI / BUILD (pull_request) Successful in 5s
CI / TEST (Codex) (pull_request) Failing after 25s
CI / TEST (Claude) (pull_request) Failing after 27s
CI / TEST (Antigravity) (pull_request) Failing after 35s
2026-07-21 08:00:35 +00:00
6 changed files with 335 additions and 47 deletions
+110
View File
@@ -1890,5 +1890,115 @@
"endLine": 69, "endLine": 69,
"problem": "`redactSecrets` 與 `agentFailureDetail` 是新加入的安全診斷防線,但測試只覆蓋 Authorization/token 的基本遮罩。控制字元單行化、URL 內嵌帳密、長 token/hex、輸入與輸出長度截斷、以及沒有 error code 時的 fallback 訊息都還沒被驗證。", "problem": "`redactSecrets` 與 `agentFailureDetail` 是新加入的安全診斷防線,但測試只覆蓋 Authorization/token 的基本遮罩。控制字元單行化、URL 內嵌帳密、長 token/hex、輸入與輸出長度截斷、以及沒有 error code 時的 fallback 訊息都還沒被驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 redactSecretsagentFailureDetail 對控制字元、URL 帳密、token 格式、截斷與 fallback 診斷等邊界缺少測試。" "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 redactSecretsagentFailureDetail 對控制字元、URL 帳密、token 格式、截斷與 fallback 診斷等邊界缺少測試。"
},
{
"addedAt": "2026/07/21 16:00:33",
"prNumber": 6,
"reviewer": "Bard",
"severity": "警告",
"file": "src/index.js",
"startLine": 166,
"endLine": 223,
"problem": "`main()` 這段新添的建問題模式狀態、暫存佇列、三個閉包輔助函式,全塞在主流程開頭,讓主旋律還沒開始就先進入一大段插曲。`issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies` 彼此共享可變狀態,讀者必須在腦中追蹤閉包副作用,主流程的步驟節奏因此變得沉重。",
"reason": "Paladin:可排除(重複)。此條指涉 main() 內 issue 模式狀態、暫存佇列與閉包職責混雜,已由既有排除事項與歷史 findings 多次涵蓋。"
},
{
"addedAt": "2026/07/21 16:00:33",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 230,
"endLine": 239,
"problem": "註解標成「步驟 2:延後執行」,但實際位置夾在快速檢查與步驟 3 之前,後面又在步驟 8 後再次出現「步驟 2(延後執行)」。同一個步驟號在不同位置反覆變奏,雖然註解有解釋,閱讀節拍仍容易打結。",
"reason": "Paladin:可排除(重複)。歷史 findings 已指出流程步驟編號散落且實際順序變成 1、3~8、2、9~10,與本條「步驟 2 延後但仍用線性編號」相同。"
},
{
"addedAt": "2026/07/21 16:00:33",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 166,
"endLine": 235,
"problem": "`main()` 現在同時負責流程編排、PR 留言、issue 模式暫存、issue 建立、fallback 狀態切換與留言 flush。這段靠 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 多個閉包變數互相配合,半年後要改「留言要發去哪裡」或「建 issue 失敗怎麼降級」時,很容易漏掉某個狀態轉換,尤其後面收尾 commit、resolve 舊留言、嚴重/其他問題發布都還會讀這些狀態。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。main() 同時承擔留言路由、issue 狀態、fallback 與 flush 的發布狀態管理,已由既有排除事項與歷史 findings 涵蓋。"
},
{
"addedAt": "2026/07/21 16:00:33",
"prNumber": 6,
"reviewer": "Leo",
"severity": "建議",
"file": "src/index.js",
"startLine": 222,
"endLine": 230,
"problem": "註解與 log 名稱把「標記舊留言過時」稱為「步驟 2」,但實際執行點被延後到流程後段。這種非時間順序的步驟編號會讓維護者追 log 或對照 README 流程圖時產生認知落差:看到「步驟 2」不再代表第二個發生的動作,而是某個被延後的歷史步驟。",
"reason": "Paladin:可排除(重複)。與 F003 及歷史 findings 指涉同一個延後執行的「步驟 2」仍以線性步驟編號呈現所造成的認知落差。"
},
{
"addedAt": "2026/07/21 16:00:33",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "readme.md",
"startLine": 76,
"endLine": 131,
"problem": "README 內大量手動維護到 `src/branch/develop/...#Lxx` 的精確行號連結。這次變更已經因程式碼位移而更新一整片連結,未來每次插入函式或註解都會造成文件 churn;更糟的是漏改時文件會指到錯誤行,讀者以為文件可信,實際上卻被帶到過期位置。",
"reason": "Paladin:可排除(重複)。README 大量手動維護 develop 分支與 #Lxx 行號連結,歷史 findings 已多次記錄相同維護成本與連結漂移風險。"
},
{
"addedAt": "2026/07/21 16:00:33",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 117,
"endLine": 120,
"problem": "當 PR 審查出嚴重問題,但結果 commit/push 失敗時,`commitFindings` 只回傳 `false`。新版主流程又改成「本輪審查不因嚴重問題直接 exit 1,而是依賴下一輪讀到 `[failure]` commit 才失敗」。最小情境:`severe.length > 0`、token 權限不足或 push 競態導致 `commitAndPushFindings` 丟錯;此時沒有 `[failure]` commit、也不會有下一輪快速回報,但本輪仍可能以 0 結束,PR 檢查會在存在嚴重問題時通過。",
"reason": "Paladin:可排除(重複)。此條與 F001 指涉同一風險:嚴重 finding 存在時 failure 結果 commit/push 失敗,可能使本輪檢查仍以成功結束。"
},
{
"addedAt": "2026/07/21 16:00:33",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 163,
"endLine": 437,
"problem": "建問題模式被大幅改寫:情境留言先暫存、只有 kept finding 才建 issue、建立失敗會 fallback 回 PR、嚴重/非嚴重 finding 會改發到 issue、最後還要回貼追蹤 issue 連結與設定相依關係。但目前新增測試只覆蓋了少數 helper,沒有驗證 `main()` 在這些分支下的實際副作用。這些行為沒經過試煉,等於還不知道留言會不會發錯地方、fallback 後會不會漏清舊留言、或無 finding 時是否真的靜默通過。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、只在有 finding 時建 issue、fallback、嚴重/非嚴重分流、PR 回貼與靜默通過等核心分支缺少流程測試。"
},
{
"addedAt": "2026/07/21 16:00:33",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 298,
"endLine": 355,
"problem": "`commitAndPushFindings` 的推送行為改成一律走 `pushWithCredential`,並宣稱 token 不進 argv、會清掉 checkout 的 extraheader、只接受同一 Gitea origin 的 `.git` URL。這是安全與 CI 觸發都很關鍵的失敗路徑,但目前 `test/gitrepo.test.js` 只測了分支名稱檢查,沒有驗證推送時的 argv/env,也沒有驗證遠端 URL 不符時會停止。這段若改壞,測試不會提醒我們 token 可能出現在命令列或推送根本沒用 PAT 身分。",
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄 commitAndPushFindingspushWithCredential 的認證推送路徑、argv/env、遠端目標檢查與失敗訊息遮蔽缺少測試。"
},
{
"addedAt": "2026/07/21 16:00:33",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 130,
"endLine": 188,
"problem": "`resolveMergeBase` 新增了多階段 fetch 補歷史流程:先 fetch base、首次 merge-base、再 deepen base、deepen PR HEAD、必要時 unshallow,且每個策略成功後要立即重試並短路返回。現有測試只驗證不安全 `baseRef` 會在 fetch 前被拒絕,沒有測到淺層 checkout、首次失敗後補抓成功、策略失敗繼續下一個、全部失敗時診斷訊息等邊界。這正是容易 off-by-one 或順序錯的流程,現在還沒有測試保護。",
"reason": "Paladin:可排除(重複)。resolveMergeBase 多階段 fetch、淺層與非淺層路徑、策略停止條件、全部失敗診斷與 cause 缺少測試,已由歷史 findings 涵蓋。"
},
{
"addedAt": "2026/07/21 16:26:12",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 105,
"endLine": 116,
"problem": "這裡把審查結果 commit/push 失敗吞掉並回傳 `false`,而本次變更又把主流程改成「本輪審查不因嚴重問題直接 exit 1,靠下一輪讀到 `[failure]` commit 才失敗」。攻擊者只要讓結果 commit 推不上去,例如在 PR head 競態推送、讓 token 沒有 push 權限、或讓來源分支拒絕 bot push,就能讓嚴重安全 finding 已產生但沒有 failure commit、也沒有下一輪失敗檢查,等同把必要檢查繞過。",
"reason": "人工裁示排除:主流程已在 result === failure 且 commitFindings 回傳 false 時直接回傳 1;本 finding 針對舊位置的描述已由現有收尾防線涵蓋。"
} }
] ]
@@ -0,0 +1,184 @@
{
"generatedAt": "2026/07/21 16:00:33",
"commitSha": "216bc39255ec176a0f66f2b257beea5929ef4eb8",
"prNumber": 6,
"tool": {
"name": "codex",
"version": "codex-cli 0.144.6",
"model": "gpt-5.5"
},
"findings": [],
"excluded": [
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "警告",
"file": "src/index.js",
"startLine": 166,
"endLine": 223,
"problem": "`main()` 這段新添的建問題模式狀態、暫存佇列、三個閉包輔助函式,全塞在主流程開頭,讓主旋律還沒開始就先進入一大段插曲。`issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies` 彼此共享可變狀態,讀者必須在腦中追蹤閉包副作用,主流程的步驟節奏因此變得沉重。",
"suggestion": "把留言去向抽成小型 helper(例如 `createCommentSink`),讓 `main()` 只看見 `commentSink.post()`、`commentSink.flushToIssue()`、`commentSink.fallbackToPr()` 這類語意清楚的介面。主流程保留編排,狀態管理移到專責函式,樂句會乾淨許多。",
"suggestedCode": "",
"id": "F002",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。此條指涉 main() 內 issue 模式狀態、暫存佇列與閉包職責混雜,已由既有排除事項與歷史 findings 多次涵蓋。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 230,
"endLine": 239,
"problem": "註解標成「步驟 2:延後執行」,但實際位置夾在快速檢查與步驟 3 之前,後面又在步驟 8 後再次出現「步驟 2(延後執行)」。同一個步驟號在不同位置反覆變奏,雖然註解有解釋,閱讀節拍仍容易打結。",
"suggestion": "將流程步驟編號與執行順序拆開命名,例如稱為「舊留言清理(延後)」或「收斂前清理舊留言」,避免用 `步驟 2` 這種線性編號描述一個實際延後執行的階段。",
"suggestedCode": "",
"id": "F003",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已指出流程步驟編號散落且實際順序變成 1、3~8、2、9~10,與本條「步驟 2 延後但仍用線性編號」相同。"
}
}
},
{
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 166,
"endLine": 235,
"problem": "`main()` 現在同時負責流程編排、PR 留言、issue 模式暫存、issue 建立、fallback 狀態切換與留言 flush。這段靠 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 多個閉包變數互相配合,半年後要改「留言要發去哪裡」或「建 issue 失敗怎麼降級」時,很容易漏掉某個狀態轉換,尤其後面收尾 commit、resolve 舊留言、嚴重/其他問題發布都還會讀這些狀態。",
"suggestion": "把發布目的地抽成一個小型 publisher 模組或類別,讓 `main()` 只呼叫 `publisher.comment()`、`publisher.ensureIssue()`、`publisher.fallbackToPr()`、`publisher.resolveOldComments()` 這類語意方法。狀態留在 publisher 內部,並補單元測試覆蓋「一般模式、issue 模式有 finding、issue 模式無 finding、建立 issue 失敗降級」四條路徑。",
"suggestedCode": "",
"id": "F006",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(命中已知排除事項且重複)。main() 同時承擔留言路由、issue 狀態、fallback 與 flush 的發布狀態管理,已由既有排除事項與歷史 findings 涵蓋。"
}
}
},
{
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "建議",
"file": "src/index.js",
"startLine": 222,
"endLine": 230,
"problem": "註解與 log 名稱把「標記舊留言過時」稱為「步驟 2」,但實際執行點被延後到流程後段。這種非時間順序的步驟編號會讓維護者追 log 或對照 README 流程圖時產生認知落差:看到「步驟 2」不再代表第二個發生的動作,而是某個被延後的歷史步驟。",
"suggestion": "改用穩定的階段名稱取代數字,例如 `清理舊留言`、`工具偵測`、`diff 摘要`,或重新編號成實際執行順序。若一定要保留舊編號,建議只放在 README 遷移說明,不要散落在程式碼註解與 log label 中。",
"suggestedCode": "",
"id": "F007",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。與 F003 及歷史 findings 指涉同一個延後執行的「步驟 2」仍以線性步驟編號呈現所造成的認知落差。"
}
}
},
{
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "readme.md",
"startLine": 76,
"endLine": 131,
"problem": "README 內大量手動維護到 `src/branch/develop/...#Lxx` 的精確行號連結。這次變更已經因程式碼位移而更新一整片連結,未來每次插入函式或註解都會造成文件 churn;更糟的是漏改時文件會指到錯誤行,讀者以為文件可信,實際上卻被帶到過期位置。",
"suggestion": "避免在手寫 README 維護精確行號。可改連到檔案或章節錨點,或把 API 列表改由文件產生器輸出。若需要固定定位,優先使用語意穩定的錨點或函式章節,而不是會隨程式碼行數漂移的 `#Lxx`。",
"suggestedCode": "",
"id": "F008",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。README 大量手動維護 develop 分支與 #Lxx 行號連結,歷史 findings 已多次記錄相同維護成本與連結漂移風險。"
}
}
},
{
"reviewer": "Mage",
"focus": "logic",
"badge": "🔮",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 117,
"endLine": 120,
"problem": "當 PR 審查出嚴重問題,但結果 commit/push 失敗時,`commitFindings` 只回傳 `false`。新版主流程又改成「本輪審查不因嚴重問題直接 exit 1,而是依賴下一輪讀到 `[failure]` commit 才失敗」。最小情境:`severe.length > 0`、token 權限不足或 push 競態導致 `commitAndPushFindings` 丟錯;此時沒有 `[failure]` commit、也不會有下一輪快速回報,但本輪仍可能以 0 結束,PR 檢查會在存在嚴重問題時通過。",
"suggestion": "嚴重問題存在時,若 failure 結果 commit 沒有成功產生,必須讓本輪直接回傳 1。可保留「成功 push failure commit 時本輪回傳 0、下一輪失敗」的設計,但 push 失敗不可靜默通過。",
"suggestedCode": "const result = severe.length === 0 ? 'success' : 'failure';\nconst committed = commitFindings({ cwd, ctx, files: filesToCommit, result });\nif (result === 'failure' && !committed) {\n log('收尾', 'ERR', '有嚴重問題但無法推送 failure 結果 commit,本輪直接失敗。');\n return 1;\n}\nreturn 0;",
"id": "F010",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。此條與 F001 指涉同一風險:嚴重 finding 存在時 failure 結果 commit/push 失敗,可能使本輪檢查仍以成功結束。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 163,
"endLine": 437,
"problem": "建問題模式被大幅改寫:情境留言先暫存、只有 kept finding 才建 issue、建立失敗會 fallback 回 PR、嚴重/非嚴重 finding 會改發到 issue、最後還要回貼追蹤 issue 連結與設定相依關係。但目前新增測試只覆蓋了少數 helper,沒有驗證 `main()` 在這些分支下的實際副作用。這些行為沒經過試煉,等於還不知道留言會不會發錯地方、fallback 後會不會漏清舊留言、或無 finding 時是否真的靜默通過。",
"suggestion": "請補 `src/index.js` 的流程層級測試,mock `gitea`、`review`、`agents`、`gitrepo` 等依賴,至少覆蓋:\n\n| 情境 | 應驗證 |\n| --- | --- |\n| `createIssue=true` 且 `kept.length===0` | 不建 issue、不發 PR 留言、暫存情境留言不外洩 |\n| `createIssue=true` 且有 finding | 先建 issue,再依序 flush 工具/diff/角色留言,finding 發到 issuePR 只留下追蹤 issue 連結 |\n| 建 issue 或寫入 issue 失敗 | fallback 後情境留言改發 PR,後續使用一般模式留言與舊留言清理 |\n| 嚴重 finding | 結果 commit 標記為 failure,但本輪 `main()` 回傳 0,下一輪才由 bot commit 回報 1 |",
"suggestedCode": "",
"id": "F011",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、只在有 finding 時建 issue、fallback、嚴重/非嚴重分流、PR 回貼與靜默通過等核心分支缺少流程測試。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 298,
"endLine": 355,
"problem": "`commitAndPushFindings` 的推送行為改成一律走 `pushWithCredential`,並宣稱 token 不進 argv、會清掉 checkout 的 extraheader、只接受同一 Gitea origin 的 `.git` URL。這是安全與 CI 觸發都很關鍵的失敗路徑,但目前 `test/gitrepo.test.js` 只測了分支名稱檢查,沒有驗證推送時的 argv/env,也沒有驗證遠端 URL 不符時會停止。這段若改壞,測試不會提醒我們 token 可能出現在命令列或推送根本沒用 PAT 身分。",
"suggestion": "請補針對 `commitAndPushFindings``pushWithCredential` 的測試。建議用可替換的 `execFileSync` seam 或測試專用注入方式,斷言:\n\n| 情境 | 斷言方向 |\n| --- | --- |\n| 正常推送 | `git push` argv 只含不帶帳密的 `remoteUrl` 與 refspec |\n| 正常推送 | env 含 `GIT_CONFIG_COUNT=2`,先清空 `http.<origin>/.extraheader`,再注入 `Authorization: Basic ...` |\n| `remoteUrl` origin 不符或非 `.git` | 拋出固定錯誤且不執行 push |\n| push 失敗 | 對外錯誤訊息不含 token、remote URL 或 argv |",
"suggestedCode": "",
"id": "F012",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已記錄 commitAndPushFindingspushWithCredential 的認證推送路徑、argv/env、遠端目標檢查與失敗訊息遮蔽缺少測試。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 130,
"endLine": 188,
"problem": "`resolveMergeBase` 新增了多階段 fetch 補歷史流程:先 fetch base、首次 merge-base、再 deepen base、deepen PR HEAD、必要時 unshallow,且每個策略成功後要立即重試並短路返回。現有測試只驗證不安全 `baseRef` 會在 fetch 前被拒絕,沒有測到淺層 checkout、首次失敗後補抓成功、策略失敗繼續下一個、全部失敗時診斷訊息等邊界。這正是容易 off-by-one 或順序錯的流程,現在還沒有測試保護。",
"suggestion": "請補資料驅動的 `resolveMergeBase` 測試,mock git 執行結果來驗證:首次 merge-base 成功時不跑 deependeepen base 後成功會立刻回傳;deepen PR HEAD 使用目前 `HEAD` SHA 而不是遠端符號 `HEAD`;非 shallow repo 不呼叫 `--unshallow`;全部策略失敗時錯誤包含各策略診斷且保留 cause。",
"suggestedCode": "",
"id": "F013",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。resolveMergeBase 多階段 fetch、淺層與非淺層路徑、策略停止條件、全部失敗診斷與 cause 缺少測試,已由歷史 findings 涵蓋。"
}
}
}
]
}
+2 -2
View File
@@ -16,11 +16,11 @@ author: 'Jeffery'
# 輸入參數區塊:呼叫端 workflow 以 `with:` 傳入, # 輸入參數區塊:呼叫端 workflow 以 `with:` 傳入,
# runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取。 # runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取。
inputs: inputs:
# Gitea API token:用於對 PRissue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。 # Gitea API token:用於對 PRissue 留言審查結果,以及 push 審查結果檔(findingsexclusions)回 repo。
token: token:
# 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。 # 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。
# 建議傳入能觸發 CI 的 PAT;自動 token 推送結果 commit 時可能不會再觸發 workflow。 # 建議傳入能觸發 CI 的 PAT;自動 token 推送結果 commit 時可能不會再觸發 workflow。
description: 'Gitea API tokenPR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)' description: 'Gitea API tokenPRissue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)'
# 必填:缺少 token 無法呼叫 Gitea APIaction 無法運作。 # 必填:缺少 token 無法呼叫 Gitea APIaction 無法運作。
required: true required: true
# 指定 AI 工具使用的模型名稱。 # 指定 AI 工具使用的模型名稱。
+32
View File
@@ -0,0 +1,32 @@
'use strict';
const { execFileSync } = require('child_process');
/**
* 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。
*
* @param {string} refName - 使用者或事件 payload 提供的分支名稱。
* @param {string} fieldName - 錯誤訊息中的欄位名稱。
* @returns {string} 原樣回傳通過驗證的分支名稱。
* @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。
*/
function assertSafeBranchRef(refName, fieldName) {
const value = String(refName || '').trim();
if (!value) throw new Error(`${fieldName} 不可為空。`);
if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) {
throw new Error(`${fieldName} 不是安全的分支名稱:${value}`);
}
try {
execFileSync('git', ['check-ref-format', '--branch', value], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
} catch {
throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`);
}
return value;
}
module.exports = {
assertSafeBranchRef,
};
+4 -43
View File
@@ -1,37 +1,10 @@
'use strict'; 'use strict';
const { execFileSync } = require('child_process'); const { execFileSync } = require('child_process');
const { assertSafeBranchRef } = require('./gitref');
// 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 輸出。
* *
@@ -308,18 +281,9 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
/** /**
* 以帶認證的方式推送到指定遠端,認證資訊只經環境變數傳入、不進命令列 argv。 * 以帶認證的方式推送到指定遠端,認證資訊只經環境變數傳入、不進命令列 argv。
* *
* 認證方式:等同 `https://ai-review-bot:<secret>@host/...` 的 HTTP Basicgit 會把 * 以 `GIT_CONFIG_*` 注入本次 HTTP Basic extraheader,避免憑證出現在 argv
* URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的 * 同時先清空 checkout 持久化的自動 token extraheader,確保本次 push 使用呼叫端 token。
* `GIT_CONFIG_*` 環境變數注入 `http.<serverUrl>/.extraheader`,使 base64 憑證**不出現在 argv** * 推送失敗時改拋固定訊息,避免原始例外帶出遠端 URL 或認證資訊。
* (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。
*
* 觸發 CI 關鍵:`actions/checkout` 會把「自動 Actions token」持久化在同一個
* `http.<serverUrl>/.extraheader` scope;若沿用它推送,Gitea 會視為「自動 token 觸發」而
* **不再觸發 workflow**(防遞迴)。故本函式對這次 push 於該 scope**先以空值重置**(清掉自動
* token——git 對 extraHeader 給空值即清空既有清單),**再注入 PAT 的 Authorization**,讓推送以
* PAT 身分進行、觸發 PR 的 synchronize;作用範圍僅限本次 push 的環境變數,不影響 action 其他
* 仰賴 checkout 持久化憑證的 fetch(如 {@link resolveMergeBase})。
* 推送失敗時**不重拋原始例外**(其 message 會含命令列與遠端 URL),改拋固定訊息。
* *
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
* @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。 * @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。
@@ -367,7 +331,4 @@ module.exports = {
fileDiff, fileDiff,
fileLastUpdatedIso, fileLastUpdatedIso,
commitAndPushFindings, commitAndPushFindings,
__test: {
assertSafeBranchRef,
},
}; };
+3 -2
View File
@@ -4,14 +4,15 @@ const assert = require('node:assert/strict');
const test = require('node:test'); const test = require('node:test');
const gitrepo = require('../src/lib/gitrepo'); const gitrepo = require('../src/lib/gitrepo');
const gitref = require('../src/lib/gitref');
test('assertSafeBranchRef 接受一般分支名稱', () => { test('assertSafeBranchRef 接受一般分支名稱', () => {
assert.equal(gitrepo.__test.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123'); assert.equal(gitref.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123');
}); });
test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => { test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => {
assert.throws( assert.throws(
() => gitrepo.__test.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'), () => gitref.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'),
/不是安全的分支名稱/, /不是安全的分支名稱/,
); );
}); });