feat: AI Pull Request action — opencode 自動產生 PR 並通過 AI review #2

Open
jiantw83 wants to merge 24 commits from ai-review-resolve/develop-20260626-114117 into develop
2 changed files with 32 additions and 75 deletions
Showing only changes of commit fb47575860 - Show all commits
+32
View File
@@ -0,0 +1,32 @@
[
{
"location": "app/lib/gitea.js:28",
"role": "Assassin",
"original_finding": "Gitea API 請求在 headers 中直接放入了 `this.token`,若來源於不可信輸入且未經驗證,將導致 token 洩漏風險。",
"reason": "token 來自受信任的 `gitea.token`(CI 自動注入)而非使用者輸入;`_request` 從未將 headers 或 request 物件輸出到日誌,無實際洩漏路徑。錯誤訊息可能夾帶 token 的真正風險已於 index.js 頂層 catch 以 maskSecrets 遮蔽處理。"
},
{
"location": "app/lib/gitea.js:52",
"role": "Mage",
"original_finding": "findOpenPull 僅撈取前 50 個 PR,數量眾多可能導致重複建立;且 _request GET 未對回傳 json 結構嚴格驗證。",
"reason": "重複建立由 Gitea 在 POST 時回傳 422/409 阻擋,findOpenPull 僅在收到 422/409 後用於查回既有 PR 編號(fallback),分頁與否不影響是否重複建立。JSON 結構已透過 `if (!ok || !Array.isArray(json)) return null` 與 `_request` 的 try/catch 防禦驗證。"
},
{
"location": "app/lib/git.js:52",
"role": "Assassin",
"original_finding": "使用不可信的 remoteUrl 進行 fetch 操作存在 git 協定漏洞風險,建議驗證 remoteUrl 是否為預期 Gitea 網域。",
"reason": "remoteUrl 由 `${serverUrl}/${owner}/${repo}.git` 組成,serverUrl 來自受信任的 `gitea.server_url`CI 環境變數),並非任意使用者輸入;且已使用 `--no-tags` 限制 refspec。容器基底為 node:20-bookworm-slimgit 版本為近期版本。"
},
{
"location": "app/lib/git.js:63",
"role": "Rogue",
"original_finding": "getCommitMessages 使用 `git log --max-count=50`,數量可能不足或過多。",
"reason": "50 筆為 PR 摘要的合理預設上限,非缺陷;改用 `--since` 屬使用場景偏好調整,無明確需求佐證,不在本次修復範圍。"
},
{
"location": "app/lib/util.js:14",
"role": "Rogue",
"original_finding": "run 函式 maxBuffer 設為 64MBgit diff 內容極大時易引發 OOM,建議改用 stream。",
"reason": "run() 採同步 spawnSync 為刻意設計(所有呼叫端皆同步取用 result.stdout);maxBuffer 為上限保護而非預先配置,僅在輸出達該量時才佔用;傳給 opencode 的 diff 已於 index.js 以 maxDiffChars 截斷。改為 stream 屬大規模架構重構,牽涉設計取捨。"
}
]
-75
View File
@@ -1,19 +1,4 @@
[
{
"level": "critical",
"role": "Assassin",
"location": "app/lib/gitea.js:28",
"problem": "Gitea API 請求在 headers 中直接放入了 `this.token`。雖然這在正常情況下是必要的,但如果 `this.token` 來源於不可信的輸入且未經嚴格驗證,這將導致 token 洩漏風險(透過請求日誌或中間人攻擊)。",
"suggestion": "在 `GiteaClient` 的所有請求方法中增加對 token 的處理,並確保在任何可能將請求細節(包含 headers)輸出到日誌的邏輯中,必須將 token 遮蔽。",
"is_new": true
},
{
"level": "critical",
"role": "Mage",
"location": "app/lib/gitea.js:52",
"problem": "1. `findOpenPull` 方法僅撈取前 50 個 PR,若數量眾多可能導致重複建立。2. `_request` 進行 `GET` 操作時,未對 API 回傳的 `json` 內容結構進行嚴格合法性驗證,可能導致執行時錯誤。",
"suggestion": "1. 實作分頁(pagination)機制確保完整性。2. 在確保 HTTP 狀態碼為 200 後,強化對 `json` 的防禦性檢測(如檢查是否為 undefined 或預期陣列)。"
},
{
"level": "critical",
"role": "Mage",
@@ -28,59 +13,6 @@
"problem": "在偵測到衝突並建立解衝突分支後,程式雖然嘗試透過 `git.createResolveBranch` 建立並 commit 衝突檔案,但後續缺乏邏輯處理衝突檔案,亦無測試驗證「自動解衝突分支是否真的被建立」以及「提交的內容是否正確」。",
"suggestion": "應補上整合測試,模擬合併衝突,驗證 `detectConflict` 能偵測衝突,且 `createResolveBranch` 產生的分支確實包含預期的衝突檔案與 commit。"
},
{
"level": "warning",
"role": "Assassin",
"location": "app/lib/git.js:52",
"problem": "Git 的 `--no-tags` 參數在某些舊版 git 可能無法完全防禦標籤帶來的惡意遠端物件下載。雖然 `fetchBranches` 限制了 refspec,但使用不可信的 `remoteUrl` 進行 fetch 操作時,仍存在與 git 協定漏洞相關的風險。",
"suggestion": "建議確保容器內的 git 版本為最新,並考慮在 fetch 前驗證 `remoteUrl` 是否為預期的 Gitea 網域,而非任意使用者輸入的網址。",
"is_new": true
},
{
"level": "warning",
"role": "Assassin",
"location": "app/index.js:180",
"problem": "在 `main` 函數的 catch 區塊中,直接將 `err.stack` 輸出到標準錯誤流(log.error)。如果錯誤物件中包含了敏感資訊(如 token、API 參數),這些機密將被寫入到 CI/CD 的執行日誌中,極易洩漏。",
"suggestion": "在輸出 `err.stack` 前,必須使用類似 `maskSecrets` 的函式,過濾掉所有可能的機密資訊。",
"is_new": true
},
{
"level": "warning",
"role": "Leo",
"location": "app/lib/opencode.js:34",
"problem": "1. `_writeConfig` 使用 mkdtempSync 建立暫存目錄後未清理,造成空間堆積。2. `summarize` 函式重複建立零散設定檔,增加 I/O 與清理負擔。",
"suggestion": "在程式執行完畢後的 finally 區塊中,統一實作檔案系統清理邏輯(如 fs.rmSync)以刪除暫存目錄與檔案。同時考慮設定檔重用性,減少頻繁的檔案操作。"
},
{
"level": "warning",
"role": "Mage",
"location": "app/index.js:84",
"problem": "在 `buildResolveBranchName` 中,處理衝突分支名稱時,雖然使用了 `safe` 函數替換特殊字元,但如果分支名稱過長,加上 `suffix`(runId)可能導致分支名稱過長而超出 Git 對 branch 名稱長度的極限(雖然通常很大,但這是不必要的風險)。",
"suggestion": "建議對 `resolveBranch` 的總長度進行截斷,確保其不會超過 Git 的建議長度限制。",
"is_new": true
},
{
"level": "warning",
"role": "Mage",
"location": "app/lib/opencode.js:106",
"problem": "在 `summarize` 方法中將 `HOME` 環境變數硬編碼為 `/root`,若 Dockerfile 變更使用者,將導致無法寫入設定檔。",
"suggestion": "建議動態獲取當前環境的使用者家目錄(如使用 `os.homedir()`),增加相容性。"
},
{
"level": "warning",
"role": "Rogue",
"location": "app/lib/git.js:63",
"problem": "在 `getCommitMessages` 中使用 `git log --max-count=50`,若專案歷史悠久,此數量可能不足以產生精確的 AI 摘要,且若取得數量過多則浪費處理資源。",
"suggestion": "評估實際使用場景調整 `limit`,或改用時間區間(例如 `--since`)來抓取相關變更。"
},
{
"level": "warning",
"role": "Rogue",
"location": "app/lib/util.js:14",
"problem": "`run` 函式設定 `maxBuffer: 64 * 1024 * 1024` (64MB)。雖然避免了截斷,但如果 `git diff` 內容極大,這會一次性將大量文字讀入記憶體,極易引發記憶體不足 (OOM) 或過高的 GC 壓力。",
"suggestion": "改用 stream 方式讀取 `git` 指令輸出,而非一次性載入 buffer。",
"is_new": true
},
{
"level": "info",
"role": "Leo",
@@ -103,12 +35,5 @@
"problem": "`extractResult` 函數處理 JSON 解析與清理邏輯,雖然複雜,但目前沒有單元測試驗證其對「LLM 容易輸出的各種非標準 JSON」的處理能力(例如字串內含未跳脫換行)。",
"suggestion": "補上單元測試,提供幾種 LLM 常見的「壞」JSON 格式,驗證 `extractResult` 能否正確解析出 `title` 與 `description`。",
"is_new": true
},
{
"level": "info",
"role": "Rogue",
"location": "app/lib/git.js:95",
"problem": "`detectConflict` 函式透過 `git merge --no-commit --no-ff` 進行完整合併測試,極其耗時且佔用大量磁碟空間。",
"suggestion": "考慮改用 `git merge-tree` (Git 2.29+) 檢查衝突,在不觸碰工作區的情況下快速檢測。"
}
]