diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 8f27702..5b5192d 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -58,5 +58,17 @@ "role": "Leo", "original_finding": "categorizeTags 內部頻繁轉換 Set,若被頻繁呼叫可能造成效能浪費,建議改為接收已轉好的 Set。", "reason": "不適用。categorizeTags 每次清理流程僅被呼叫一次,單次建立 Set 成本可忽略;讓純函式自行建立 Set 可保持介面單純,將轉換責任外推反而增加耦合。" + }, + { + "location": "app/gitea-client.js:25", + "role": "Bard", + "original_finding": "GiteaClient 建構子的 header 初始化結構死板,建議抽離為內部 private 函式 _getHeaders() 以增加未來彈性。", + "reason": "YAGNI / 不適用。目前僅單一固定標頭,建構子直接設定清楚直觀;為尚不存在的需求預先抽象屬過度設計,待真有多標頭需求時再重構。" + }, + { + "location": "app/releases.js:23", + "role": "Rogue", + "original_finding": "為排序而 map 成物件陣列再 map 回原物件,造成多次 O(n) 陣列分配與 GC 壓力,建議減少中間狀態或原地排序。", + "reason": "不採納且與前次建議衝突。此 map/sort/map(Schwartzian transform)正是前一輪 Rogue(releases.js:17)要求「排序前預先計算時間戳」的實作,用以避免比較器重複呼叫 Date.parse;release 數量有限,中間陣列開銷可忽略,可讀性更佳。" } ] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 9a554df..af17419 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,42 +1,4 @@ [ - { - "level": "critical", - "role": "Assassin", - "location": "app/releases.js:48", - "problem": "攻擊者可以透過控制 GITEA_REPOSITORY 環境變數,在其名稱中包含路徑穿越序列(如 '../'),進而操控 API 的刪除目標,造成越權刪除其他儲存庫的成品。", - "suggestion": "在將 id 拼接進 URL 前,務必使用 encodeURIComponent(id) 對 id 進行編碼,確保其僅被視為路徑的一部分,而非路徑控制字元。", - "is_new": true - }, - { - "level": "critical", - "role": "Assassin", - "location": "app/tags.js:70", - "problem": "攻擊者可以透過控制 GITEA_REPOSITORY 環境變數,在其名稱中包含路徑穿越序列,進而操控 API 的刪除目標;同時,tag 名稱若包含惡意字元也可能造成路徑穿越。", - "suggestion": "同樣地,在將 tag.name 拼接進 URL 前,必須使用 encodeURIComponent(tag.name) 對其進行編碼。", - "is_new": true - }, - { - "level": "critical", - "role": "Maya", - "problem": "main 函式執行失敗時會觸發 process.exit(1),確保 CI/CD 流程能正確偵測錯誤。目前的測試僅驗證了 Promise 被 reject,但並未驗證程式是否真的正確以非零狀態碼結束。", - "suggestion": "請在 app/test/main.test.js 中,模擬 main 拋出錯誤的情境,並透過 mock process.exit 來驗證當 main 執行失敗時,程式碼確實執行了 process.exit(1)。", - "location": "app/index.js:31", - "is_new": false - }, - { - "level": "critical", - "role": "Maya", - "location": "app/test/config.test.js:63", - "problem": "測試只驗證了正常與極端的 token 輸入,但完全沒有測試 token 為 null 或未定義(匿名模式)下的處理邏輯,也沒確認匿名請求時,設定物件是否正確地將 token 設為 null。", - "suggestion": "請增加測試案例,明確斷言當 GITEA_TOKEN 不存在於環境變數時,loadConfig() 回傳的 token 屬性為 null。" - }, - { - "level": "critical", - "role": "Maya", - "location": "app/test/gitea-client.test.js:33", - "problem": "測試中雖然有 fetchAllPages 的功能測試,但對於 MAX_PAGES 的邊界情況,測試只用了 globalThis.fetch 永遠回傳非空陣列,這是一個快樂路徑的極端變體。如果 API 剛好在第 MAX_PAGES 頁回傳空陣列,測試並未驗證客戶端是否能正確處理並停止。", - "suggestion": "請增加一個測試案例,模擬當 API 恰好在第 MAX_PAGES 次請求時回傳空陣列的情境,確認客戶端能成功結束,而非拋出例外。" - }, { "level": "warning", "role": "Mage", @@ -44,8 +6,7 @@ "problem": "清理流程對網路請求依賴強,若 API 呼叫失敗,整個 main 流程中斷,無法確保後續清理的一致性與部分成功重試。", "suggestion": "引入更細緻的錯誤處理(如錯誤閾值機制)或部分清理成功後的重試策略。", "status": "deferred", - "defer_reason": "與 app/releases.js:46 同屬錯誤處理/重試策略的設計取捨。目前單筆刪除失敗會記錄並繼續、讀取失敗則中止屬合理保守行為;是否引入閾值/重試保留待人工評估。", - "is_new": false + "defer_reason": "錯誤處理/重試策略的設計取捨。目前單筆刪除失敗會記錄並繼續、讀取失敗則中止屬合理保守行為;是否引入閾值/重試保留待人工評估。" }, { "level": "warning", @@ -54,8 +15,7 @@ "problem": "cleanupReleases 同時處理資料獲取、邏輯判斷與副作用執行,違反單一職責原則。", "suggestion": "拆分邏輯與執行層,將刪除副作用抽象化為獨立函式。", "status": "deferred", - "defer_reason": "純邏輯(selectReleasesToDelete)已抽離且函式短小、已具失敗路徑與編碼測試;進一步拆出刪除迴圈為設計偏好,效益有限,保留待人工評估。", - "is_new": false + "defer_reason": "純邏輯(selectReleasesToDelete)已抽離且函式短小、已具失敗路徑/編碼/stderr 測試;進一步拆出刪除迴圈為設計偏好,效益有限,保留待人工評估。" }, { "level": "warning", @@ -64,8 +24,7 @@ "problem": "清理成品與刪除 tag 使用序列化迴圈,API 請求逐一排隊,整體執行時間拉長。", "suggestion": "改用 Promise.all 搭配 map 將刪除請求並行化。", "status": "deferred", - "defer_reason": "刻意保留序列化:避免對 Gitea API 造成併發壓力與觸發速率限制,並維持可預期的記錄輸出順序;無上限並行化非等價變更,保留待人工評估(可日後改為有上限並行)。", - "is_new": false + "defer_reason": "刻意保留序列化:避免對 Gitea API 造成併發壓力與觸發速率限制,並維持可預期的記錄輸出順序;無上限並行化非等價變更,保留待人工評估(可日後改為有上限並行)。" }, { "level": "warning", @@ -74,8 +33,7 @@ "problem": "錯誤路徑以 res.text() 讀取整個回應主體,未限制大小,惡意伺服器可回傳極大內容導致記憶體耗盡(DoS)。", "suggestion": "限制讀取的回應大小,例如檢查 Content-Length 或以串流方式設定讀取上限。", "status": "deferred", - "defer_reason": "風險低:目標為已通過 URL 驗證的受信任 Gitea 實例,需伺服器被入侵或中間人攻擊才成立,且每請求已有 30 秒逾時部分約束。正確修法需以串流逐段讀取並設位元組上限(Content-Length 在 chunked 回應下不可靠),屬較大改動,保留待人工評估。", - "is_new": false + "defer_reason": "風險低:目標為已通過 URL 驗證的受信任 Gitea 實例,需伺服器被入侵或中間人攻擊才成立,且每請求已有 30 秒逾時部分約束。正確修法需串流逐段讀取並設位元組上限(Content-Length 在 chunked 下不可靠),屬較大改動,保留待人工評估。" }, { "level": "warning", @@ -84,87 +42,15 @@ "problem": "成功路徑以 res.json() 解析整個回應,未限制大小,惡意伺服器可回傳極大 JSON 導致記憶體耗盡(DoS)。", "suggestion": "對 API 回應設定明確大小上限,超過時拒絕解析並拋出異常。", "status": "deferred", - "defer_reason": "與 app/gitea-client.js:66 同類:受信任目標、已有逾時與 MAX_PAGES 約束,風險低;正確修法需串流讀取並設上限,屬較大改動,保留待人工評估。", - "is_new": false - }, - { - "level": "warning", - "role": "Maya", - "problem": "logger.js 中的輸出函式(如 separator, section, info, success, warn, fail)負責 Action 的核心視覺輸出格式,但目前缺乏測試驗證其實際輸出內容是否正確對齊並符合格式要求。", - "suggestion": "請在 app/test/logger.test.js 中補齊對這些函式的測試,驗證其是否正確寫入預期的格式內容(含分隔線與正確的前綴)到 stdout。", - "location": "app/logger.js:9", - "is_new": false - }, - { - "level": "warning", - "role": "Maya", - "problem": "sanitizeBody 函式負責 API 回應的字串清理與格式化,但目前缺乏直接的單元測試,無法確保正規表示式能正確處理所有控制字元以及長度限制。", - "suggestion": "請在 app/test/gitea-client.test.js 中新增 sanitizeBody 的單元測試,務必包含正常字串、包含控制字元的字串、空字串/null 值、以及超過 200 字元的極端案例。", - "location": "app/gitea-client.js:13", - "is_new": false - }, - { - "level": "warning", - "role": "Mage", - "location": "app/releases.js:59", - "problem": "使用字串模板直接拼接 API URL:`const url = `${config.releaseApiUrl}/${id}`;`。若 API 回傳的 id 包含 `/` 或特殊字元,可能導致拼接出錯誤的 API 路徑,甚至在某些處理器上導致路徑穿越,雖是 Gitea API 但應防禦性地進行 URL 編碼。", - "suggestion": "建議使用 encodeURIComponent(id) 對 id 進行編碼後再拼接。", - "is_new": true - }, - { - "level": "warning", - "role": "Mage", - "location": "app/tags.js:65", - "problem": "與 releases.js 相同問題,在拼接 tag URL 時:`const url = `${config.tagApiUrl}/${tag.name}`;` 未對 tag 名稱進行 URL 編碼。Tag 名稱若包含 `/` 等特殊字元,會破壞 URL 結構。", - "suggestion": "建議使用 encodeURIComponent(tag.name) 對 tag 名稱進行編碼。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/test/releases.test.js:77", - "problem": "在 `cleanupReleases` 的整合測試中,雖然有測試 `deleteResource` 回傳非 204 時的行為,但測試只檢查了 `client.deleted` 的呼叫順序,並沒有驗證 `fail` 日誌(對 stderr 的寫入)是否正確被觸發。", - "suggestion": "建議攔截 `process.stderr.write`,確認當 `deleteResource` 回傳 500 時,系統確實有記錄到錯誤訊息。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "app/test/tags.test.js:73", - "problem": "在 `cleanupOrphanTags` 的測試中,缺乏對於「當 `deleteResource` 失敗」時的行為驗證。目前只測試了成功的情境。", - "suggestion": "增加模擬 `deleteResource` 回傳非 204 狀態碼的情境,確認該項刪除失敗不會中斷整個標籤清理流程,並檢查是否有對應的錯誤日誌。", - "is_new": true + "defer_reason": "與 app/gitea-client.js:66 同類:受信任目標、已有逾時與 MAX_PAGES 約束,風險低;正確修法需串流讀取並設上限,屬較大改動,保留待人工評估。" }, { "level": "info", "role": "Bard", "location": "app/releases.js:28", - "problem": "在 `cleanupReleases` 函式中,參數 `config` 是 `ReturnType`,這種型別定義方式雖然準確,但過於冗長且與實作細節耦合過深,影響程式碼的可讀性與簡潔度。", - "suggestion": "建議在 `app/config.js` 定義並匯出型別註解(JSDoc @typedef),然後在此處直接使用該別名,提升整體程式碼的可讀性。", - "is_new": true - }, - { - "level": "info", - "role": "Bard", - "location": "app/gitea-client.js:25", - "problem": "在 `GiteaClient` 建構子中,`this.headers` 初始化時僅簡單檢查 `token` 是否存在,且假設所有請求都適用這組標頭。雖然目前專案單純,但若未來擴充需針對不同 API 採取不同標頭時,此處結構會稍顯死板。", - "suggestion": "考慮將產生 header 的邏輯抽離成一個內部 private 函式(如 `_getHeaders()`),即便現在很簡單,也能增加未來的彈性。", - "is_new": true - }, - { - "level": "info", - "role": "Maya", - "location": "app/test/validate.test.js:20", - "problem": "雖然有 `requireInteger` 的測試,但沒有測試當 `KEEP_COUNT` 為 `0` 時的邊界情況。雖然 0 在邏輯上可能是允許的,但這對於清理邏輯來說是個關鍵的邊界。", - "suggestion": "明確測試 `requireInteger('KEEP_COUNT', '0')` 並斷言其不應拋出錯誤,確保系統允許保留 0 個成品的配置。", - "is_new": true - }, - { - "level": "info", - "role": "Rogue", - "location": "app/releases.js:23", - "problem": "為了排序而進行了多次 map 操作,先將陣列 map 為物件陣列,排序後又 map 回原始物件陣列。這在大數據集下會造成多次 O(n) 的陣列分配與垃圾回收壓力,浪費 CPU 與記憶體週期。", - "suggestion": "排序時應嘗試減少陣列中間狀態的產生,例如考慮使用原地排序(若不介意原陣列變更)或簡化 mapping 的邏輯。", - "is_new": true + "problem": "config 參數型別 ReturnType 雖準確但冗長,與實作細節耦合,影響可讀性。", + "suggestion": "在 config.js 定義並匯出 JSDoc @typedef,於各處改用該別名。", + "status": "deferred", + "defer_reason": "純可讀性偏好,現有型別註解準確且可運作;引入 @typedef 會牽動多處 JSDoc 與 README 行號,效益有限,保留待人工評估。" } ]