diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 2cc3ae6..8f27702 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -22,5 +22,41 @@ "role": "Bard", "original_finding": "分隔線字串為魔術字串,硬編碼在模組頂層,不易維護與調整。建議將分隔線管理集中化,並考慮提供動態產生方法。", "reason": "不適用。分隔線已是模組頂層的具名常數(LINE/SUBLINE),本即集中管理且為通用良好實務;固定寬度分隔線改為動態產生方法只會徒增複雜度,無實質效益,故不採納。" + }, + { + "location": "app/gitea-client.js:63", + "role": "Mage", + "original_finding": "while 迴圈中使用 AbortSignal.timeout,若 Node.js 版本低於 16 會崩潰。建議改用 AbortController 搭配 setTimeout。", + "reason": "不適用。本專案以 node:20-alpine 為基底且 package.json engines 限定 >=18,AbortSignal.timeout 自 Node 17.3 起即支援,執行環境保證可用,無向下相容需求。" + }, + { + "location": "app/gitea-client.js:101", + "role": "Rogue", + "original_finding": "處理分頁資料用 for...of 逐一 all.push(item) 效率不佳,建議用 Array.prototype.push.apply(all, items)。", + "reason": "不採納且會造成回歸。先前依 Bard(gitea-client.js:59)建議刻意改用 for...of 以規避展開/apply 在大量項目時的呼叫堆疊上限風險;push.apply 等同展開,會重新引入該風險。逐頁項目數量有限,迴圈開銷可忽略。" + }, + { + "location": "app/validate.js:14", + "role": "Mage", + "original_finding": "isEmptyOrNull 判斷式包含 value === 'null',導致字串 'null' 被判定為空,破壞語義一致性。建議移除對 'null' 字串的特殊判定。", + "reason": "不採納。此為刻意保留原 bash 版本 is_empty_or_null 的語義:Gitea/jq 等來源可能將未設定值輸出為字串 'null',視其為空可避免把字面 'null' 當成有效設定。移除會改變既有行為。" + }, + { + "location": "app/tags.js:46", + "role": "Rogue", + "original_finding": "cleanupOrphanTags 在迴圈中重複掃描所有 release 名稱陣列,複雜度 O(tags*releases),建議改用 Set。", + "reason": "誤報。cleanupOrphanTags 已將 releaseTagNames 交給 categorizeTags,後者於進入迴圈前即建立 Set 進行 O(1) 查找;並無 O(tags*releases) 的重複掃描。" + }, + { + "location": "app/config.js:33", + "role": "Leo", + "original_finding": "直接將 env 預設為 process.env,全域相依性可能導致難以追蹤的副作用,建議抽離為 Provider 或 ConfigFactory。", + "reason": "不適用。loadConfig 已將 env 設為可注入參數(預設 process.env),測試即透過傳入假 env 驗證,已具良好可測性與隔離;對此規模的 Action 引入 Provider/Factory 屬過度設計。" + }, + { + "location": "app/tags.js:21", + "role": "Leo", + "original_finding": "categorizeTags 內部頻繁轉換 Set,若被頻繁呼叫可能造成效能浪費,建議改為接收已轉好的 Set。", + "reason": "不適用。categorizeTags 每次清理流程僅被呼叫一次,單次建立 Set 成本可忽略;讓純函式自行建立 Set 可保持介面單純,將轉換責任外推反而增加耦合。" } ] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 6306dff..86a6258 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,40 +1,30 @@ [ { - "level": "critical", + "level": "warning", "role": "Mage", - "problem": "清理流程對網路請求依賴強,若 API 呼叫失敗,整個 main 流程中斷,導致容器無法確保後續清理的一致性與部分成功重試。", - "suggestion": "引入更細緻的錯誤處理(如錯誤閾值機制)或部分清理成功後的重試策略,確保清理任務具備健壯性。", "location": "app/releases.js:38", - "is_new": false - }, - { - "level": "critical", - "role": "Mage", - "location": "app/gitea-client.js:63", - "problem": "在 fetchAllPages 的 while 迴圈中使用了 AbortSignal.timeout。若運行環境的 Node.js 版本低於 16,此程式碼會直接崩潰。", - "suggestion": "確認運行環境的 Node.js 版本,若未來需向下相容,建議使用 AbortController 搭配 setTimeout 手動實作逾時控制。" - }, - { - "level": "critical", - "role": "Maya", - "location": "app/index.js:31", - "problem": "main 函式執行失敗時會觸發 process.exit(1),確保 CI/CD 流程能正確偵測錯誤。目前的測試僅驗證了 Promise 被 reject,但並未驗證程式是否真的正確以非零狀態碼結束。", - "suggestion": "請在 app/test/main.test.js 中,模擬 main 拋出錯誤的情境,並透過 mock process.exit 來驗證當 main 執行失敗時,程式碼確實執行了 process.exit(1)。", - "is_new": true + "problem": "清理流程對網路請求依賴強,若 API 呼叫失敗,整個 main 流程中斷,無法確保後續清理的一致性與部分成功重試。", + "suggestion": "引入更細緻的錯誤處理(如錯誤閾值機制)或部分清理成功後的重試策略。", + "status": "deferred", + "defer_reason": "與 app/releases.js:46 同屬錯誤處理/重試策略的設計取捨。目前單筆刪除失敗會記錄並繼續、讀取失敗則中止屬合理保守行為;是否引入閾值/重試保留待人工評估。" }, { "level": "warning", "role": "Mage", "location": "app/releases.js:46", - "problem": "刪除邏輯未針對網路不穩定或特定 HTTP 狀態碼(502, 503, 504)實作重試機制,且遇到失敗時未停止後續請求。若 release 清單非常龐大,會佔用大量記憶體。", - "suggestion": "針對特定 HTTP 狀態碼實作指數退避重試,並引入錯誤閾值。考慮在 fetchAllPages 中加入串流處理(Stream)或實作分批讀取機制,避免將所有資料一次性載入記憶體。" + "problem": "刪除邏輯未針對特定 HTTP 狀態碼(502, 503, 504)實作重試機制,且遇到失敗時未停止後續請求。", + "suggestion": "針對特定 HTTP 狀態碼實作指數退避重試並引入錯誤閾值;資料量龐大時考慮分批/串流讀取。", + "status": "deferred", + "defer_reason": "重試/退避/錯誤閾值涉及次數、間隔與冪等性等設計決策;清理 Action 以排程執行,單次失敗可於下次補刪。分頁已有 MAX_PAGES 上限,實務上 release/tag 數量有限,串流化屬最佳化取捨,保留待人工評估。" }, { "level": "warning", "role": "Leo", "location": "app/releases.js:37", - "problem": "cleanupReleases 同時處理資料獲取、邏輯判斷與副作用執行,違反單一職責原則,且未對 release 物件結構進行進一步驗證,可能導致資料正確性風險。", - "suggestion": "拆分邏輯與執行層,將刪除副作用抽象化為獨立函式。建議增加對於 release 物件結構的進一步驗證,並強化檢查邏輯。" + "problem": "cleanupReleases 同時處理資料獲取、邏輯判斷與副作用執行,違反單一職責原則。", + "suggestion": "拆分邏輯與執行層,將刪除副作用抽象化為獨立函式。", + "status": "deferred", + "defer_reason": "純邏輯(selectReleasesToDelete)已抽離且函式短小、已具失敗路徑與編碼測試;進一步拆出刪除迴圈為設計偏好,效益有限,保留待人工評估。" }, { "level": "warning", @@ -43,68 +33,25 @@ "problem": "清理成品與刪除 tag 使用序列化迴圈,API 請求逐一排隊,整體執行時間拉長。", "suggestion": "改用 Promise.all 搭配 map 將刪除請求並行化。", "status": "deferred", - "defer_reason": "刻意保留序列化:避免對 Gitea API 造成併發壓力與觸發速率限制,並維持可預期的記錄輸出順序;無上限並行化非等價變更,保留待人工評估(可日後改為有上限並行)。", - "is_new": false - }, - { - "level": "warning", - "role": "Rogue", - "location": "app/gitea-client.js:101", - "problem": "在處理分頁資料時,使用 for...of 逐一執行 all.push(item),導致大量記憶體配置與無謂的物件拷貝,效率不佳。", - "suggestion": "建議直接將 items 整批解構並存入 all,或者使用 Array.prototype.push.apply(all, items) 來減少迴圈開銷。" + "defer_reason": "刻意保留序列化:避免對 Gitea API 造成併發壓力與觸發速率限制,並維持可預期的記錄輸出順序;無上限並行化非等價變更,保留待人工評估(可日後改為有上限並行)。" }, { "level": "warning", "role": "Assassin", "location": "app/gitea-client.js:66", - "problem": "攻擊者可透過惡意伺服器回傳極大的錯誤回應內容,利用此處未限制大小的 res.text() 直接讀取整個回應主體,導致記憶體耗盡 (DoS)。", - "suggestion": "應限制讀取的回應大小,例如在請求前檢查 Content-Length 或使用串流處理並設定讀取限制。", - "is_new": true + "problem": "錯誤路徑以 res.text() 讀取整個回應主體,未限制大小,惡意伺服器可回傳極大內容導致記憶體耗盡(DoS)。", + "suggestion": "限制讀取的回應大小,例如檢查 Content-Length 或以串流方式設定讀取上限。", + "status": "deferred", + "defer_reason": "風險低:目標為已通過 URL 驗證的受信任 Gitea 實例,需伺服器被入侵或中間人攻擊才成立,且每請求已有 30 秒逾時部分約束。正確修法需以串流逐段讀取並設位元組上限(Content-Length 在 chunked 回應下不可靠),屬較大改動,保留待人工評估。" }, { "level": "warning", "role": "Assassin", "location": "app/gitea-client.js:77", - "problem": "攻擊者可透過惡意伺服器回傳極大的 JSON 資料,利用此處未限制大小的 res.json() 直接解析整個回應,導致記憶體耗盡 (DoS)。", - "suggestion": "應對 API 回應設定明確的大小上限,超過時拒絕解析並拋出異常。", - "is_new": true - }, - { - "level": "warning", - "role": "Mage", - "location": "app/validate.js:14", - "problem": "isEmptyOrNull 判斷式包含 value === 'null',導致正確字串內容 'null' 被判定為空,破壞語義一致性。", - "suggestion": "建議移除對 'null' 字串的特殊判定,改由呼叫端明確處理邏輯。" - }, - { - "level": "warning", - "role": "Maya", - "location": "app/gitea-client.js:13", - "problem": "sanitizeBody 函式負責 API 回應的字串清理與格式化,但目前缺乏直接的單元測試,無法確保正規表示式能正確處理所有控制字元以及長度限制。", - "suggestion": "請在 app/test/gitea-client.test.js 中新增 sanitizeBody 的單元測試,務必包含正常字串、包含控制字元的字串、空字串/null 值、以及超過 200 字元的極端案例。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/logger.js:9", - "problem": "logger.js 中的輸出函式(如 separator, section, info, success, warn, fail)負責 Action 的核心視覺輸出格式,但目前缺乏測試驗證其實際輸出內容是否正確對齊並符合格式要求。", - "suggestion": "請在 app/test/logger.test.js 中補齊對這些函式的測試,驗證其是否正確寫入預期的格式內容(含分隔線與正確的前綴)到 stdout。", - "is_new": true - }, - { - "level": "warning", - "role": "Rogue", - "location": "app/releases.js:17", - "problem": "selectReleasesToDelete 針對每一筆 release 重複執行 Date.parse,浪費 CPU 週期。", - "suggestion": "應在排序前先執行一次 map 轉換,預先計算時間戳記。" - }, - { - "level": "warning", - "role": "Rogue", - "location": "app/tags.js:46", - "problem": "cleanupOrphanTags 在迴圈中重複掃描所有 release 名稱陣列,時間複雜度為 O(tags * releases),效能低落。", - "suggestion": "應在進入 tag 迴圈前,先將所有 release 的 tag 名稱轉成一個 Set 物件,將查找複雜度降為 O(1)。" + "problem": "成功路徑以 res.json() 解析整個回應,未限制大小,惡意伺服器可回傳極大 JSON 導致記憶體耗盡(DoS)。", + "suggestion": "對 API 回應設定明確大小上限,超過時拒絕解析並拋出異常。", + "status": "deferred", + "defer_reason": "與 app/gitea-client.js:66 同類:受信任目標、已有逾時與 MAX_PAGES 約束,風險低;正確修法需串流讀取並設上限,屬較大改動,保留待人工評估。" }, { "level": "info", @@ -113,21 +60,6 @@ "problem": "fetchAllPages 採線性逐頁請求,資料龐大時效能不佳。", "suggestion": "若 API 支援,先取得總頁數後並行請求。", "status": "deferred", - "defer_reason": "Gitea 分頁未可靠提供總頁數,需解析 Link 標頭且各端點支援度不一;線性逐頁搭配逾時與 MAX_PAGES 上限已足夠穩健,並行化屬最佳化取捨,保留待人工評估。", - "is_new": false - }, - { - "level": "info", - "role": "Leo", - "location": "app/config.js:33", - "problem": "直接將 env 預設為 process.env,全域相依性可能導致難以追蹤的副作用。", - "suggestion": "建議在複雜系統中,將環境變數讀取抽離為獨立的 Provider 或 ConfigFactory。" - }, - { - "level": "info", - "role": "Leo", - "location": "app/tags.js:21", - "problem": "categorizeTags 內部頻繁轉換 Set,若此函式被頻繁呼叫,可能造成效能浪費。", - "suggestion": "建議調整設計,讓 categorizeTags 接收已經轉好的 Set,將轉換開銷移至上層。" + "defer_reason": "Gitea 分頁未可靠提供總頁數,需解析 Link 標頭且各端點支援度不一;線性逐頁搭配逾時與 MAX_PAGES 上限已足夠穩健,並行化屬最佳化取捨,保留待人工評估。" } ]