- fetchAllPages 加入 MAX_PAGES(1000)安全斷點,避免 API 異常時無限迴圈
- 刪除 release/tag 時對 id 與 tag 名稱做 encodeURIComponent,
防止特殊字元造成路徑穿越(正常數值/版本字串編碼後不變)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增端對端整合測試(列出→刪除舊 release→重列→列 tag→刪孤立 tag 的呼叫順序)、
loadConfig 去尾斜線測試,並將 id 編碼測試改為驗證非整數 id 被略過。測試共 62 項全數通過。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
嚴重等級:🟡 警告 審查員:Mage 問題:categorizeTags 函式中直接呼叫 keep.has(tag.name),若 API 回傳的 tag 物件缺少 name 欄位或 name 為非字串,可能導致非預期行為。 建議:增加明確的型別檢查,確保 tag.name 為 string 後再進行比對。
**嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:categorizeTags 函式中直接呼叫 keep.has(tag.name),若 API 回傳的 tag 物件缺少 name 欄位或 name 為非字串,可能導致非預期行為。
**建議**:增加明確的型別檢查,確保 tag.name 為 string 後再進行比對。
移除本輪已修復(pathToFileURL、網路錯誤/空陣列測試)與已具防護的 finding
(id 已驗證正整數);保留 6 條錯誤處理/併發/TOCTOU 設計取捨。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cleanupReleases/cleanupOrphanTags 改用 delete-utils:暫時性錯誤(429/5xx、
網路例外)重試、永久性錯誤(401/403/404)即止、單筆失敗只記錄不中斷並彙報失敗數,
並以有上限併發加速。解決 AI review 的重試/錯誤分流/併發/部分失敗續行系列建議。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
變更摘要
將 release-cleanup Action 由單一
entrypoint.sh(bash)改寫為模組化的 Node.js 程式,並經多輪 AI code review 強化健壯性、安全性與測試覆蓋。對外行為維持不變:清理超出保留數量的舊 release,並刪除未被任何 release 指定的孤立 tag。影響範圍
alpine+bash/curl/jq改為node:20-alpine;HTTP 改用 Node 內建fetch。entrypoint.sh保留,僅負責exec node /app/index.js。app/,測試於app/test/(64 項,node --test全數通過)。重點模組
app/logger.jsapp/validate.jsapp/config.jsapp/gitea-client.jsapp/releases.jsapp/tags.jsapp/index.js已強化(經 AI review 多輪)
輸入驗證(URL/repo 格式、SSRF 降風險)、所有請求 30 秒逾時、分頁 MAX_PAGES 上限、回應 content-type 與 JSON 解析防呆、刪除 URL 編碼與 id 整數驗證、錯誤訊息控制字元清理、決定性排序、端對端整合測試。
已知保留事項(設計取捨,見 .gitea/ai-review/findings.json)
DELETE 重試/錯誤閾值、逐筆刪除吞例外續行、刪除併發化、deleteResource 依狀態碼分流、release/tag 兩次讀取的 TOCTOU(已於程式碼註解文件化)。皆為需求面決策,已逐條記錄保留原因。
風險
容器基底改為 Node.js,行為已盡量等價;建議於實際 repo 驗證一次清理流程。
🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 14 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +43,4 @@*/export async function cleanupReleases(client, config) {section('取得成品資訊')info(`GET ${config.releaseApiUrl}`)嚴重等級:🔴 嚴重
審查員:Maya
問題:清理舊版本成品時,若刪除過程拋出例外或 API 回傳異常(除 204 外),未作完善處理,無法保證一致性且未針對『部分失敗』設計重試或完整性檢查機制。(合併:包含 app/releases.js:46 例外中止問題)
建議:將逐筆刪除包在 try/catch,加入錯誤計數;若失敗率過高或發生特定非預期錯誤(如 403),應明確拋出例外讓流程終止。
@@ -0,0 +22,4 @@return { tag, action: 'skip' }}if (keep.has(tag.name)) {return { tag, action: 'keep' }嚴重等級:🟡 警告
審查員:Mage
問題:categorizeTags 函式中直接呼叫 keep.has(tag.name),若 API 回傳的 tag 物件缺少 name 欄位或 name 為非字串,可能導致非預期行為。
建議:增加明確的型別檢查,確保 tag.name 為 string 後再進行比對。
@@ -0,0 +49,4 @@const currentReleases = await client.fetchAllPages(config.releaseApiUrl)const releaseTagNames = currentReleases.map((release) => release.tag_name)info(`GET ${config.tagApiUrl}`)嚴重等級:🟡 警告
審查員:Mage
問題:cleanupOrphanTags 分兩次 API 呼叫取得 releases 與 tags,過濾過程未保證原子性;期間 Gitea 狀態變更可能誤刪非孤立 tag。(合併:包含 app/tags.js:47, 45 之類似 TOCTOU 風險指控)
建議:考量原子性需求,或在刪除前增加確認機制;並明確文件化此風險。建議增加最終防護機制,或在測試中模擬競爭條件。
🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 19 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +112,4 @@async deleteResource(url) {const res = await fetch(url, {method: 'DELETE',headers: this.headers,嚴重等級:🟡 警告
審查員:Maya
問題:deleteResource 僅回傳狀態碼,對 401/403/404 等錯誤未做區分處理。
建議:在 deleteResource 內針對常見錯誤碼拋出更有意義的例外,讓呼叫方採取跳過/重試/中止策略。
@@ -0,0 +27,4 @@// 僅在直接以 `node index.js` 執行時啟動主流程;被測試 import 時不自動執行,方便撰寫整合測試。if (import.meta.url === `file://${process.argv[1]}`) {main().catch((error) => {failError(error)嚴重等級:🔵 建議
審查員:Bard
問題:手動拼接 import.meta.url 字串來判斷執行入口,寫法原始且脆弱。
建議:引用 node:url 中的 pathToFileURL,使用 import.meta.url === pathToFileURL(process.argv[1]).href 進行比較。
@@ -0,0 +18,4 @@return Number.isNaN(time) ? 0 : time}// 先一次性計算每筆的時間戳,避免在排序比較中重複呼叫 Date.parse。// 時間相同時以 id(數值,大者為新)作為次要鍵,確保排序結果具決定性。嚴重等級:🟡 警告
審查員:Mage
問題:在 selectReleasesToDelete 函式中,若 releases 陣列為空,雖不拋錯但邏輯上應考慮空值邊界。且雖然處理了 Date.parse 的 NaN,但對空物件或缺失 created_at 屬性的項目的處理依賴屬性存取鏈,可能在特定結構下產生非預期行為。
建議:建議增加對釋出項目結構的預防性檢查,確保 created_at 存在且有效。
@@ -0,0 +35,4 @@** 流程:取得所有 release → 若總數不超過 `keepCount` 則直接結束(無需清理)→* 否則以 [[selectReleasesToDelete]] 取出待刪除清單,逐筆刪除(略過沒有 `id` 的項目),* 依回應狀態碼 204 判定成功與否並輸出結果。嚴重等級:🟡 警告
審查員:Mage
問題:清理流程對網路請求依賴強,若 API 呼叫失敗,整個 main 流程中斷,無法確保後續清理的一致性與部分成功重試。
建議:引入更細緻的錯誤處理(如錯誤閾值機制)或部分清理成功後的重試策略。
@@ -0,0 +43,4 @@*/export async function cleanupReleases(client, config) {section('取得成品資訊')info(`GET ${config.releaseApiUrl}`)嚴重等級:🔴 嚴重
審查員:Maya
問題:清理舊版本成品時,若刪除過程拋出例外或 API 回傳異常(除 204 外),未作完善處理,導致無法保證一致性且未針對「部分失敗」設計重試或完整性檢查機制。
建議:將逐筆刪除包在 try/catch,加入錯誤計數;若失敗率過高或發生特定非預期錯誤(如 403),應明確拋出例外讓流程終止。
@@ -0,0 +44,4 @@export async function cleanupReleases(client, config) {section('取得成品資訊')info(`GET ${config.releaseApiUrl}`)嚴重等級:🟡 警告
審查員:Maya
問題:cleanupReleases 迴圈逐筆 await deleteResource,release 眾多時依序刪除耗時,且 API 負載高時易逾時。
建議:若 API 允許,採有上限的併發刪除(如限流 Promise.all),或增加進度日誌與重試。
@@ -0,0 +74,4 @@// id 已驗證為正整數;仍對其編碼作為縱深防禦。const url = `${config.releaseApiUrl}/${encodeURIComponent(id)}`info(`DELETE ${tag} (${name})`)嚴重等級:🔴 嚴重
審查員:Assassin
問題:儘管使用了 encodeURIComponent(id),若後續處理中 id 被錯誤地解碼或拼接,仍存在目錄穿越風險(API 路徑穿越)。
建議:除了 encodeURIComponent,需確保 cleanupReleases 迴圈中對 id 為正整數的驗證邏輯不能被繞過,且移除 id 上的任何類型轉換風險。
@@ -0,0 +49,4 @@// 若兩次呼叫之間有新 release 關聯到某 tag,該 tag 仍可能基於過時快照被誤判為孤立。// 此為排程清理任務可接受的殘餘競態(下次執行會自我修正),完整原子性留待人工評估。const currentReleases = await client.fetchAllPages(config.releaseApiUrl)const releaseTagNames = currentReleases.map((release) => release.tag_name)嚴重等級:🟡 警告
審查員:Mage
問題:cleanupOrphanTags 分兩次 API 呼叫取得 releases 與 tags,過濾過程未保證原子性,期間 Gitea 狀態變更可能誤刪非孤立 tag。
建議:考量原子性需求,或在刪除前增加確認機制,並明確文件化此風險。
@@ -0,0 +51,4 @@const currentReleases = await client.fetchAllPages(config.releaseApiUrl)const releaseTagNames = currentReleases.map((release) => release.tag_name)info(`GET ${config.tagApiUrl}`)嚴重等級:🟡 警告
審查員:Maya
問題:刪除 tag 的迴圈缺少對 deleteResource 拋例外的防禦,網路錯誤會中止後續 tag 清理。
建議:在 for 迴圈內加 try/catch,記錄錯誤並 continue;比照 releases 補測試。
@@ -0,0 +147,4 @@globalThis.fetch = async () => {page += 1// 前 999 頁有資料,第 1000 頁(MAX_PAGES)回空陣列,應正常結束return jsonResponse(page < 1000 ? [{ id: page }] : [])嚴重等級:🟡 警告
審查員:Maya
問題:fetchAllPages 目前缺乏測試當 fetch 自身發生網路錯誤時的情境。
建議:補上一個測試案例,模擬 fetch 拋出非 AbortError 的一般網路錯誤,以驗證客戶端能妥善捕捉並拋出具備 URL 上下文的錯誤。
@@ -0,0 +27,4 @@)})test('selectReleasesToDelete 在 created_at 相同時以 id 決定順序(具決定性)', () => {嚴重等級:🔵 建議
審查員:Maya
問題:selectReleasesToDelete 尚未明確測試傳入空陣列 [] 時的行為。
建議:增加測試案例:assert.deepEqual(selectReleasesToDelete([], 1), []),驗證穩健性。
🤖 AI Code Review 團隊
🤖 AI Code Review 團隊
Pull request closed