From e0ab6f2693fd5d74a6116947489fb6f7c2279ffa Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Fri, 26 Jun 2026 03:34:26 +0000 Subject: [PATCH] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/findings.json | 130 ++++++++++++++++++++++++++++----- 1 file changed, 113 insertions(+), 17 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index af17419..7c766ec 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,4 +1,28 @@ [ + { + "level": "critical", + "role": "Maya", + "problem": "測試只驗證了正常與極端的 token 輸入,但完全沒有測試 token 為 null 或未定義(匿名模式)下的處理邏輯,也沒確認匿名請求時,設定物件是否正確地將 token 設為 null。", + "suggestion": "請增加測試案例,明確斷言當 GITEA_TOKEN 不存在於環境變數時,loadConfig() 回傳的 token 屬性為 null。", + "location": "app/test/config.test.js:63", + "is_new": false + }, + { + "level": "critical", + "role": "Maya", + "problem": "測試中雖然有 fetchAllPages 的功能測試,但對於 MAX_PAGES 的邊界情況,測試只用了 globalThis.fetch 永遠回傳非空陣列,這是一個快樂路徑的極端變體。如果 API 剛好在第 MAX_PAGES 頁回傳空陣列,測試並未驗證客戶端是否能正確處理並停止。", + "suggestion": "請增加一個測試案例,模擬當 API 恰好在第 MAX_PAGES 次請求時回傳空陣列的情境,確認客戶端能成功結束,而非拋出例外。", + "location": "app/test/gitea-client.test.js:33", + "is_new": false + }, + { + "level": "critical", + "role": "Assassin", + "location": "app/config.js:35", + "problem": "僅驗證是否為 URL 格式,未檢查傳入的 `GITEA_SERVER_URL` 是否指向內部網路敏感資源(如 localhost, 169.254.169.254 等),這在容器化環境中可能導致 SSRF(伺服器端請求偽造)。", + "suggestion": "在 `validate.js` 的 `requireUrl` 中加入黑名單機制,禁止解析為內部 IP 位址或 loopback 位址。", + "is_new": true + }, { "level": "warning", "role": "Mage", @@ -6,7 +30,8 @@ "problem": "清理流程對網路請求依賴強,若 API 呼叫失敗,整個 main 流程中斷,無法確保後續清理的一致性與部分成功重試。", "suggestion": "引入更細緻的錯誤處理(如錯誤閾值機制)或部分清理成功後的重試策略。", "status": "deferred", - "defer_reason": "錯誤處理/重試策略的設計取捨。目前單筆刪除失敗會記錄並繼續、讀取失敗則中止屬合理保守行為;是否引入閾值/重試保留待人工評估。" + "defer_reason": "錯誤處理/重試策略的設計取捨。目前單筆刪除失敗會記錄並繼續、讀取失敗則中止屬合理保守行為;是否引入閾值/重試保留待人工評估。", + "is_new": false }, { "level": "warning", @@ -15,16 +40,8 @@ "problem": "cleanupReleases 同時處理資料獲取、邏輯判斷與副作用執行,違反單一職責原則。", "suggestion": "拆分邏輯與執行層,將刪除副作用抽象化為獨立函式。", "status": "deferred", - "defer_reason": "純邏輯(selectReleasesToDelete)已抽離且函式短小、已具失敗路徑/編碼/stderr 測試;進一步拆出刪除迴圈為設計偏好,效益有限,保留待人工評估。" - }, - { - "level": "warning", - "role": "Rogue", - "location": "app/releases.js:43", - "problem": "清理成品與刪除 tag 使用序列化迴圈,API 請求逐一排隊,整體執行時間拉長。", - "suggestion": "改用 Promise.all 搭配 map 將刪除請求並行化。", - "status": "deferred", - "defer_reason": "刻意保留序列化:避免對 Gitea API 造成併發壓力與觸發速率限制,並維持可預期的記錄輸出順序;無上限並行化非等價變更,保留待人工評估(可日後改為有上限並行)。" + "defer_reason": "純邏輯(selectReleasesToDelete)已抽離且函式短小、已具失敗路徑/編碼/stderr 測試;進一步拆出刪除迴圈為設計偏好,效益有限,保留待人工評估。", + "is_new": false }, { "level": "warning", @@ -33,7 +50,8 @@ "problem": "錯誤路徑以 res.text() 讀取整個回應主體,未限制大小,惡意伺服器可回傳極大內容導致記憶體耗盡(DoS)。", "suggestion": "限制讀取的回應大小,例如檢查 Content-Length 或以串流方式設定讀取上限。", "status": "deferred", - "defer_reason": "風險低:目標為已通過 URL 驗證的受信任 Gitea 實例,需伺服器被入侵或中間人攻擊才成立,且每請求已有 30 秒逾時部分約束。正確修法需串流逐段讀取並設位元組上限(Content-Length 在 chunked 下不可靠),屬較大改動,保留待人工評估。" + "defer_reason": "風險低:目標為已通過 URL 驗證的受信任 Gitea 實例,需伺服器被入侵或中間人攻擊才成立,且每請求已有 30 秒逾時部分約束。正確修法需串流逐段讀取並設位元組上限(Content-Length 在 chunked 下不可靠),屬較大改動,保留待人工評估。", + "is_new": false }, { "level": "warning", @@ -42,15 +60,93 @@ "problem": "成功路徑以 res.json() 解析整個回應,未限制大小,惡意伺服器可回傳極大 JSON 導致記憶體耗盡(DoS)。", "suggestion": "對 API 回應設定明確大小上限,超過時拒絕解析並拋出異常。", "status": "deferred", - "defer_reason": "與 app/gitea-client.js:66 同類:受信任目標、已有逾時與 MAX_PAGES 約束,風險低;正確修法需串流讀取並設上限,屬較大改動,保留待人工評估。" + "defer_reason": "與 app/gitea-client.js:66 同類:受信任目標、已有逾時與 MAX_PAGES 約束,風險低;正確修法需串流讀取並設上限,屬較大改動,保留待人工評估。", + "is_new": false + }, + { + "level": "warning", + "role": "Maya", + "problem": "在 `cleanupReleases` 的整合測試中,雖然有測試 `deleteResource` 回傳非 204 時的行為,但測試只檢查了 `client.deleted` 的呼叫順序,並沒有驗證 `fail` 日誌(對 stderr 的寫入)是否正確被觸發。", + "suggestion": "建議攔截 `process.stderr.write`,確認當 `deleteResource` 回傳 500 時,系統確實有記錄到錯誤訊息。", + "location": "app/test/releases.test.js:77", + "is_new": false + }, + { + "level": "warning", + "role": "Maya", + "problem": "在 `cleanupOrphanTags` 的測試中,缺乏對於「當 `deleteResource` 失敗」時的行為驗證。目前只測試了成功的情境。", + "suggestion": "增加模擬 `deleteResource` 回傳非 204 狀態碼的情境,確認該項刪除失敗不會中斷整個標籤清理流程,並檢查是否有對應的錯誤日誌。", + "location": "app/test/tags.test.js:73", + "is_new": false + }, + { + "level": "warning", + "role": "Assassin", + "location": "app/config.js:34", + "problem": "將環境變數值直接輸出至日誌,若 `GITEA_SERVER_URL` 等變數內容被注入惡意字元或過長,可能造成 Log Injection 或日誌系統資源耗盡。", + "suggestion": "在輸出前進行 sanitization,移除控制字元並限制長度,類似於 `gitea-client.js` 中的 `sanitizeBody`。", + "is_new": true + }, + { + "level": "warning", + "role": "Assassin", + "location": "app/gitea-client.js:106", + "problem": "將 `items` 直接放入 `all` 陣列。若 API 返回異常巨大的 JSON 陣列,可能導致容器記憶體耗盡(DoS)。(註: 亦包含 Bard 關於 for...of/push 的效能建議)", + "suggestion": "考慮在 `fetchAllPages` 中增加最大總項目數量的限制,並在超過時拋出錯誤;同時建議使用 `AsyncGenerator` 進行串流式處理以降低記憶體佔用。" + }, + { + "level": "warning", + "role": "Bard", + "location": "app/releases.js:46", + "problem": "區段標題『取得成品資訊』與後續的 `section('刪除舊版本成品')` 使用了不同的區段層級。第一個區段內包含了 `info` 輸出,而第二個區段直接開始處理邏輯,視覺節奏上稍微不一致。", + "suggestion": "建議在所有主要的操作階段前統一呼叫 `section()`,或在細部操作前使用更明確的層級標示,保持日誌格式的旋律一致性。", + "is_new": true + }, + { + "level": "warning", + "role": "Leo", + "location": "app/releases.js:20", + "problem": "在 `selectReleasesToDelete` 函式中,對於 `Date.parse` 無法解析的日期直接視為 `0`,這在資料清理邏輯中是一個隱晦的行為,未來維護者可能不清楚為什麼無效日期會優先被刪除。", + "suggestion": "應明確記錄無效日期的處理方式(例如記錄 warning),或在 `Date.parse` 失敗時,應考慮給予一個明確的邏輯(例如拋出錯誤或放到特定排序位置),以減少不可預期的副作用。", + "is_new": true + }, + { + "level": "warning", + "role": "Mage", + "location": "app/releases.js:23", + "problem": "在 `selectReleasesToDelete` 的排序邏輯中,若多個成品具有完全相同的 `created_at` 時間戳,目前的排序行為依賴於 JavaScript 引擎對 `sort()` 的實作(在某些情況下可能不穩定),導致保留與刪除的成品選擇具有不確定性。", + "suggestion": "建議在排序邏輯中加入次要的排序鍵值(如 `id` 或 `tag_name`)作為比較依據(例如:若時間相同,則比較 ID 大小),以確保排序結果在時間相同時仍具有決定性。", + "is_new": true }, { "level": "info", "role": "Bard", "location": "app/releases.js:28", - "problem": "config 參數型別 ReturnType 雖準確但冗長,與實作細節耦合,影響可讀性。", - "suggestion": "在 config.js 定義並匯出 JSDoc @typedef,於各處改用該別名。", - "status": "deferred", - "defer_reason": "純可讀性偏好,現有型別註解準確且可運作;引入 @typedef 會牽動多處 JSDoc 與 README 行號,效益有限,保留待人工評估。" + "problem": "config 參數型別定義方式雖然準確,但過於冗長且與實作細節耦合過深,影響程式碼的可讀性與簡潔度。", + "suggestion": "建議在 `app/config.js` 定義並匯出型別註解(JSDoc @typedef),然後在各處直接使用該別名。" + }, + { + "level": "info", + "role": "Assassin", + "location": "app/logger.js:53", + "problem": "儘管使用了 `stderr` 輸出錯誤,但在 `failError` 中直接輸出 `error.stack` 可能會洩漏專案目錄結構、內部函式名稱等敏感路徑資訊。", + "suggestion": "在生產環境下考慮隱藏堆疊追蹤,或僅在特定 debug 模式下輸出堆疊。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/gitea-client.js:10", + "problem": "目前 `MAX_PAGES` 是寫死在程式碼中的常數,未來如果 API 規格變更或是特殊儲存庫的 Release 數量激增,維護者需要進程式碼修改,且這在不同的環境下可能需要不同的上限。", + "suggestion": "建議將 `MAX_PAGES` 改為透過環境變數傳入,並設定一個合理的預設值,增加部署時的彈性。", + "is_new": true + }, + { + "level": "info", + "role": "Leo", + "location": "app/logger.js:46", + "problem": "雖然目前只有 `fail` 函式會寫入 `stderr`,但如果有更多的 error level 需要處理,分散的邏輯會增加維護成本。", + "suggestion": "考慮在 `logger.js` 中建立一個通用的 `log` 函式,處理 `level`、`prefix` 與 `stream` 的對應,讓其他方法(如 `info`, `warn`, `fail`)只負責呼叫該通用函式,降低重複程式碼。", + "is_new": true } ]