refactor(release-cleanup): 將清理邏輯由 bash 改寫為 Node.js #5

Closed
jiantw83 wants to merge 43 commits from refactor/nodejs-rewrite-20260626-103443 into develop
Member

變更摘要

將 release-cleanup Action 的清理邏輯由單一 entrypoint.sh(bash)改寫為模組化的 Node.js 程式,並補上單元測試與文件註解。對外行為維持不變:仍會清理超出保留數量的舊 release,並刪除未被任何 release 指定的孤立 tag。

影響範圍

  • 執行環境:容器基底由 alpine + bash/curl/jq 改為 node:20-alpine;HTTP 呼叫改用 Node 內建 fetch,不再依賴 curl/jq
  • 進入點:entrypoint.sh 保留為進入點,但僅負責 exec node /app/index.js
  • 目錄結構:所有 Node.js 程式集中於 app/,測試集中於 app/test/

重點檔案/模組

模組 職責
app/logger.js 統一主控台輸出(section/info/success/warn/fail)
app/validate.js 參數驗證(isEmptyOrNull/requireValue/requireInteger)
app/config.js 讀取並驗證環境變數,組出設定物件
app/gitea-client.js Gitea API 客戶端(分頁讀取、刪除)
app/releases.js 清理舊版本 release
app/tags.js 清理未指定 release 的孤立 tag
app/index.js 進入點主流程

測試

以 Node 內建 node:test 撰寫 19 項單元測試,涵蓋純函式(isEmptyOrNull/requireValue/requireInteger/selectReleasesToDelete/categorizeTags)與 GiteaClient 的分頁與刪除邏輯。執行 node --test(於 app/)全數通過。

風險/注意事項

  • 容器基底映像更換為 Node.js,建置後映像體積與相依與先前不同;功能行為已盡量保持等價,建議於實際 repo 上驗證一次清理流程。
  • 本 PR 僅含程式碼與註解;尚未產生專案 README(doc-funcs 的 README 重建步驟未包含於此)。
## 變更摘要 將 release-cleanup Action 的清理邏輯由單一 `entrypoint.sh`(bash)改寫為模組化的 Node.js 程式,並補上單元測試與文件註解。**對外行為維持不變**:仍會清理超出保留數量的舊 release,並刪除未被任何 release 指定的孤立 tag。 ## 影響範圍 - **執行環境**:容器基底由 `alpine` + `bash/curl/jq` 改為 `node:20-alpine`;HTTP 呼叫改用 Node 內建 `fetch`,不再依賴 `curl`/`jq`。 - **進入點**:`entrypoint.sh` 保留為進入點,但僅負責 `exec node /app/index.js`。 - **目錄結構**:所有 Node.js 程式集中於 `app/`,測試集中於 `app/test/`。 ## 重點檔案/模組 | 模組 | 職責 | | --- | --- | | `app/logger.js` | 統一主控台輸出(section/info/success/warn/fail) | | `app/validate.js` | 參數驗證(isEmptyOrNull/requireValue/requireInteger) | | `app/config.js` | 讀取並驗證環境變數,組出設定物件 | | `app/gitea-client.js` | Gitea API 客戶端(分頁讀取、刪除) | | `app/releases.js` | 清理舊版本 release | | `app/tags.js` | 清理未指定 release 的孤立 tag | | `app/index.js` | 進入點主流程 | ## 測試 以 Node 內建 `node:test` 撰寫 19 項單元測試,涵蓋純函式(`isEmptyOrNull`/`requireValue`/`requireInteger`/`selectReleasesToDelete`/`categorizeTags`)與 `GiteaClient` 的分頁與刪除邏輯。執行 `node --test`(於 `app/`)全數通過。 ## 風險/注意事項 - 容器基底映像更換為 Node.js,建置後映像體積與相依與先前不同;功能行為已盡量保持等價,建議於實際 repo 上驗證一次清理流程。 - 本 PR 僅含程式碼與註解;尚未產生專案 README(doc-funcs 的 README 重建步驟未包含於此)。
jiantw83 added 3 commits 2026-06-26 02:37:24 +00:00
依功能分組為 logger/validate/config/gitea-client/releases/tags 模組,
entrypoint.sh 改為呼叫 node /app/index.js,Dockerfile 改用 node:20-alpine 基底。
對外行為(清理舊 release 與未指定 release 的 tag)維持不變。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
以 node:test 內建測試器涵蓋純函式(isEmptyOrNull/requireValue/requireInteger、
selectReleasesToDelete、categorizeTags)與 GiteaClient 分頁/刪除邏輯,共 19 項測試。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(release-cleanup): 補上 index.js JSDoc 與 ci/cd workflow 註解
CI / AI Code Review (pull_request) Failing after 40s
4bcc6a5014
為進入點主流程 main() 補上 JSDoc;為 ci.yaml/cd.yaml 加上用途說明與逐行註解。
皆為註解變更,不影響任何執行邏輯。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

🤖 AI Code Review 團隊

👤 角色 🎯 面向 🧠 個性
🗡️ Assassin security 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用
🎼 Bard style 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版
🧰 Leo maintainability 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來
🔮 Mage logic 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」
🧪 Maya testing 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑
Rogue efficiency 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」

🔍 服務:opencode 模型:gemini-2.5-flash

## 🤖 AI Code Review 團隊 | 👤 角色 | 🎯 面向 | 🧠 個性 | |--------|--------|--------| | **🗡️ Assassin** | security | 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用 | | **🎼 Bard** | style | 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版 | | **🧰 Leo** | maintainability | 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來 | | **🔮 Mage** | logic | 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」 | | **🧪 Maya** | testing | 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑 | | **⚡ Rogue** | efficiency | 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」 | > 🔍 服務:opencode 模型:gemini-2.5-flash
gitea-actions bot reviewed 2026-06-26 02:38:03 +00:00
gitea-actions bot left a comment

AI Code Review 統計

類型 🔴 嚴重 🟡 警告 🔵 建議 無法標示
新問題 6 筆 9 筆 3 筆 0 筆
舊問題 0 筆 0 筆 0 筆 0 筆

🤖 AI 助理使用量

本次審查(opencode / gemini-2.5-flash,共 25 次呼叫)

提示 token 回應 token 合計
151,320 5,931 360,549

剩餘可用

剩餘可用:無法計算百分比(自架服務,無帳號額度概念)

## AI Code Review 統計 | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | | --- | --- | --- | --- | --- | | 新問題 | 6 筆 | 9 筆 | 3 筆 | 0 筆 | | 舊問題 | 0 筆 | 0 筆 | 0 筆 | 0 筆 | ## 🤖 AI 助理使用量 **本次審查**(opencode / gemini-2.5-flash,共 25 次呼叫) | 提示 token | 回應 token | 合計 | | --- | --- | --- | | 151,320 | 5,931 | 360,549 | **剩餘可用** 剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -5,1 +10,4 @@
# 複製 Node.js 應用程式(不含測試)
# 先單獨複製 package.json,再複製所有 *.js;有利於 Docker layer 快取。
COPY app/package.json /app/package.json

嚴重等級🔵 建議
審查員:Bard
問題:注释中的冒号后面缺少空格,阅读节奏感稍显拥挤。
建議:请在冒号后面加上一个空格,让注释读起来更舒畅:# 基底映像: Node.js 20 的 Alpine 版本 (體積小)。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:注释中的冒号后面缺少空格,阅读节奏感稍显拥挤。 **建議**:请在冒号后面加上一个空格,让注释读起来更舒畅:`# 基底映像: Node.js 20 的 Alpine 版本 (體積小)。`
Ghost marked this conversation as resolved
@@ -6,0 +13,4 @@
COPY app/package.json /app/package.json
COPY app/*.js /app/
# 複製容器進入點腳本至根目錄。

嚴重等級🔵 建議
審查員:Bard
問題:注释中括号内的说明与前文缺少空格区隔,排版不够优雅。
建議:在括号前面增加一个空格:# 複製 Node.js 應用程式 (不含測試)

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:注释中括号内的说明与前文缺少空格区隔,排版不够优雅。 **建議**:在括号前面增加一个空格:`# 複製 Node.js 應用程式 (不含測試)`
Ghost marked this conversation as resolved
@@ -0,0 +8,4 @@
* 從環境變數載入設定並完成驗證
* @param {NodeJS.ProcessEnv} env 環境變數來源,預設為 process.env
* @returns 包含 API 位址token保留數量等資訊的設定物件
*/

嚴重等級🟡 警告
審查員:Maya
問題:loadConfig 函式雖有呼叫驗證邏輯,但缺乏針對環境變數異常情境(如必填欄位缺失、KEEP_COUNT 非整數)的單元測試,無法確保配置載入流程的穩定性。
建議:補齊 app/test/config.test.js,測試當 process.env 缺少必要參數或 KEEP_COUNT 為無效數字時,loadConfig 是否會正確拋出錯誤。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:loadConfig 函式雖有呼叫驗證邏輯,但缺乏針對環境變數異常情境(如必填欄位缺失、KEEP_COUNT 非整數)的單元測試,無法確保配置載入流程的穩定性。 **建議**:補齊 app/test/config.test.js,測試當 process.env 缺少必要參數或 KEEP_COUNT 為無效數字時,loadConfig 是否會正確拋出錯誤。
Ghost marked this conversation as resolved
app/config.js Outdated
@@ -0,0 +18,4 @@
const token = env.GITEA_TOKEN
info(`GITEA_SERVER_URL=${serverUrl}`)
requireValue('GITEA_SERVER_URL', serverUrl)

嚴重等級🔴 嚴重
審查員:Assassin
問題:GITEA_SERVER_URL 缺乏嚴格的格式驗證與協定強制。攻擊者若控制此環境變數,可將其設定為任意惡意伺服器(例如 http://attacker.com),引發 SSRF 攻擊或在沒有 HTTPS 的情況下,導致傳輸過程中 GITEA_TOKEN 被竊取。
建議:應使用 Node.js 的 URL 類別驗證 GITEA_SERVER_URL 格式是否合法,並強制使用 HTTPS 協定。若可能,建議實作 allowed host 的白名單機制。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:GITEA_SERVER_URL 缺乏嚴格的格式驗證與協定強制。攻擊者若控制此環境變數,可將其設定為任意惡意伺服器(例如 http://attacker.com),引發 SSRF 攻擊或在沒有 HTTPS 的情況下,導致傳輸過程中 GITEA_TOKEN 被竊取。 **建議**:應使用 Node.js 的 URL 類別驗證 GITEA_SERVER_URL 格式是否合法,並強制使用 HTTPS 協定。若可能,建議實作 allowed host 的白名單機制。
Ghost marked this conversation as resolved
app/config.js Outdated
@@ -0,0 +28,4 @@
requireInteger('KEEP_COUNT', keepCountRaw)
if (isEmptyOrNull(token)) {
warn('GITEA_TOKEN is empty; release API calls will be anonymous')

嚴重等級🔵 建議
審查員:Bard
問題:日志信息的键值对缺少空格,排版不够整齐统一。
建議:建议加上空格以提升易读性:info('GITEA_TOKEN = [redacted]')

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:日志信息的键值对缺少空格,排版不够整齐统一。 **建議**:建议加上空格以提升易读性:`info('GITEA_TOKEN = [redacted]')`
Ghost marked this conversation as resolved
app/config.js Outdated
@@ -0,0 +36,4 @@
return {
serverUrl,
repository,
token: isEmptyOrNull(token) ? null : token,

嚴重等級🔴 嚴重
審查員:Assassin
問題:GITEA_REPOSITORY 變數直接被拼接進 API URL 中,卻未進行任何消毒。攻擊者可利用 Path Traversal(例如設定為 ../../other-repo)繞過權限,導致 API 呼叫目標被竄改,進而執行未授權的動作。
建議:必須對 GITEA_REPOSITORY 進行格式驗證,僅允許合法的 Repository 名稱字元(例如英數字、-_/),且必須拒絕包含 .. 或其他路徑穿越字元的輸入。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:GITEA_REPOSITORY 變數直接被拼接進 API URL 中,卻未進行任何消毒。攻擊者可利用 Path Traversal(例如設定為 ../../other-repo)繞過權限,導致 API 呼叫目標被竄改,進而執行未授權的動作。 **建議**:必須對 GITEA_REPOSITORY 進行格式驗證,僅允許合法的 Repository 名稱字元(例如英數字、`-`、`_`、`/`),且必須拒絕包含 `..` 或其他路徑穿越字元的輸入。
Ghost marked this conversation as resolved
@@ -0,0 +22,4 @@
let page = 1
while (true) {
const url = `${baseUrl}?page=${page}`

嚴重等級🟡 警告
審查員:Leo
問題:HTTP 請求 (fetch) 未設定逾時時間,若 API 伺服器回應緩慢或網路阻塞,可能導致此 Actions 執行過程長時間卡死,造成除錯與排程上的困難。
建議:建議使用 AbortController 為所有 fetch 請求設定合理的逾時限制(例如 30 秒),並正確處理 AbortError

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:HTTP 請求 (fetch) 未設定逾時時間,若 API 伺服器回應緩慢或網路阻塞,可能導致此 Actions 執行過程長時間卡死,造成除錯與排程上的困難。 **建議**:建議使用 `AbortController` 為所有 `fetch` 請求設定合理的逾時限制(例如 30 秒),並正確處理 `AbortError`。
Ghost marked this conversation as resolved
@@ -0,0 +25,4 @@
const url = `${baseUrl}?page=${page}`
const res = await fetch(url, { headers: this.headers })
if (!res.ok) {

嚴重等級🟡 警告
審查員:Mage
問題:在 fetchAllPages 中未檢查 API 回傳的內容是否符合預期格式(除了 Array 檢查)。若 API 回傳非 JSON 格式的內容(例如 HTML 錯誤頁面),res.json() 會拋出 SyntaxError,且未被目前邏輯中的 try-catch 明確攔截處理,會導致程式在 catch 區塊中直接結束並輸出錯誤訊息,缺乏更細緻的除錯資訊。
建議:在解析 JSON 前,應先判斷 res.headers.get('content-type') 是否包含 application/json,若非 JSON,應將 res.text() 的內容一併在錯誤訊息中輸出,方便排查。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `fetchAllPages` 中未檢查 API 回傳的內容是否符合預期格式(除了 Array 檢查)。若 API 回傳非 JSON 格式的內容(例如 HTML 錯誤頁面),`res.json()` 會拋出 SyntaxError,且未被目前邏輯中的 `try-catch` 明確攔截處理,會導致程式在 catch 區塊中直接結束並輸出錯誤訊息,缺乏更細緻的除錯資訊。 **建議**:在解析 JSON 前,應先判斷 `res.headers.get('content-type')` 是否包含 `application/json`,若非 JSON,應將 `res.text()` 的內容一併在錯誤訊息中輸出,方便排查。
Ghost marked this conversation as resolved
@@ -0,0 +28,4 @@
if (!res.ok) {
throw new Error(`GET ${url} failed: HTTP ${res.status}`)
}

嚴重等級🔴 嚴重
審查員:Mage
問題:在 fetchAllPageswhile 迴圈中直接使用 await fetch 而沒有設定逾時(timeout)。若 Gitea API 伺服器因負載過重導致回應緩慢或掛起,該容器進程將會永久卡死,無法釋放資源或失敗重試。
建議:建議使用 AbortController 設定合理的逾時時間(例如:{ signal: AbortSignal.timeout(30000) }),確保網路呼叫在預期時間內未完成時能主動拋出例外。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:在 `fetchAllPages` 的 `while` 迴圈中直接使用 `await fetch` 而沒有設定逾時(timeout)。若 Gitea API 伺服器因負載過重導致回應緩慢或掛起,該容器進程將會永久卡死,無法釋放資源或失敗重試。 **建議**:建議使用 `AbortController` 設定合理的逾時時間(例如:`{ signal: AbortSignal.timeout(30000) }`),確保網路呼叫在預期時間內未完成時能主動拋出例外。
Ghost marked this conversation as resolved
app/index.js Outdated
@@ -0,0 +14,4 @@
*
* @returns {Promise<void>} 流程完成時 resolve;設定驗證或讀取 API 失敗時 reject
*/
async function main() {

嚴重等級🟡 警告
審查員:Maya
問題:main 函式作為整個流程的入口,缺乏端對端(E2E)或整合測試,無法確保各模組整合後在失敗情境下(如 process.exit(1) 的觸發)是否能正確運作。
建議:針對 main 函式建立整合測試,至少驗證在拋出例外時,logger.fail 是否有被呼叫且程序是否以錯誤代碼結束。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:main 函式作為整個流程的入口,缺乏端對端(E2E)或整合測試,無法確保各模組整合後在失敗情境下(如 process.exit(1) 的觸發)是否能正確運作。 **建議**:針對 main 函式建立整合測試,至少驗證在拋出例外時,logger.fail 是否有被呼叫且程序是否以錯誤代碼結束。
Ghost marked this conversation as resolved
@@ -0,0 +23,4 @@
separator()
}

嚴重等級🔴 嚴重
審查員:Mage
問題:當 main().catch(...) 捕捉到錯誤並執行 process.exit(1) 時,僅輸出 error.message。若錯誤是由 fetch 網路層拋出(例如 DNS 解析失敗或 connection refused),Node.js 的 Error 物件可能不包含足夠的上下文訊息,導致使用者難以區分是 API 回傳錯誤還是程式碼執行期錯誤。
建議:建議在 catch 區塊中,若錯誤是 Error 物件,輸出 error.stack 或至少記錄錯誤類型,並區分不同層級的例外(例如:驗證錯誤 vs. API 請求錯誤)。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:當 `main().catch(...)` 捕捉到錯誤並執行 `process.exit(1)` 時,僅輸出 `error.message`。若錯誤是由 `fetch` 網路層拋出(例如 DNS 解析失敗或 connection refused),Node.js 的 `Error` 物件可能不包含足夠的上下文訊息,導致使用者難以區分是 API 回傳錯誤還是程式碼執行期錯誤。 **建議**:建議在 `catch` 區塊中,若錯誤是 `Error` 物件,輸出 `error.stack` 或至少記錄錯誤類型,並區分不同層級的例外(例如:驗證錯誤 vs. API 請求錯誤)。
Ghost marked this conversation as resolved
@@ -0,0 +11,4 @@
* @param {number} keepCount 要保留的筆數
* @returns {any[]} 需要刪除的成品(較舊者)
*/
export function selectReleasesToDelete(releases, keepCount) {

嚴重等級🟡 警告
審查員:Leo
問題:使用 new Date(release.created_at) 進行排序時,若 created_at 格式非預期或為空,會導致 NaN 並造成排序異常,可能無法正確刪除舊版本。
建議:建議在排序邏輯中增加對 created_at 的有效性檢查,若無效則給予預設值(例如:new Date(0)),確保排序結果可預期。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:使用 `new Date(release.created_at)` 進行排序時,若 `created_at` 格式非預期或為空,會導致 `NaN` 並造成排序異常,可能無法正確刪除舊版本。 **建議**:建議在排序邏輯中增加對 `created_at` 的有效性檢查,若無效則給予預設值(例如:`new Date(0)`),確保排序結果可預期。
Ghost marked this conversation as resolved
app/releases.js Outdated
@@ -0,0 +18,4 @@
return sorted.slice(keepCount)
}
/**

嚴重等級🔴 嚴重
審查員:Maya
問題:cleanupReleases 是執行刪除舊成品的核心函式,但目前缺乏針對 API 呼叫失敗(如 GET 失敗、DELETE 失敗)的測試,無法驗證錯誤處理邏輯是否如預期運作。
建議:使用測試框架搭配 mock 伺服器回應,補齊 cleanupReleases 的測試案例,特別是驗證當 deleteResource 回傳非 204 狀態碼時,系統是否正確記錄錯誤並繼續或中斷。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:cleanupReleases 是執行刪除舊成品的核心函式,但目前缺乏針對 API 呼叫失敗(如 GET 失敗、DELETE 失敗)的測試,無法驗證錯誤處理邏輯是否如預期運作。 **建議**:使用測試框架搭配 mock 伺服器回應,補齊 cleanupReleases 的測試案例,特別是驗證當 deleteResource 回傳非 204 狀態碼時,系統是否正確記錄錯誤並繼續或中斷。
Ghost marked this conversation as resolved
@@ -0,0 +40,4 @@
const toDelete = selectReleasesToDelete(releases, config.keepCount)
for (const release of toDelete) {
const { id, tag_name: tag, name } = release

嚴重等級🟡 警告
審查員:Rogue
問題:在刪除舊成品時使用了序列化的 for...of 迴圈搭配 await,導致刪除請求一個個排隊等待 API 回應,浪費了寶貴的 I/O 等待時間。
建議:改用 Promise.all 搭配 map 將刪除請求並行化,讓所有請求同時發送,瞬間縮短總執行時間。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:在刪除舊成品時使用了序列化的 `for...of` 迴圈搭配 `await`,導致刪除請求一個個排隊等待 API 回應,浪費了寶貴的 I/O 等待時間。 **建議**:改用 `Promise.all` 搭配 `map` 將刪除請求並行化,讓所有請求同時發送,瞬間縮短總執行時間。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
const { id, tag_name: tag, name } = release
if (isEmptyOrNull(id)) {
warn(`略過沒有 id 的成品: ${tag} (${name})`)

嚴重等級🟡 警告
審查員:Mage
問題:在 cleanupReleases 迴圈中執行 DELETE 請求時,未針對網路不穩定或暫時性服務錯誤(如 502, 503, 504)實作重試機制。若刪除過程中發生瞬間網路中斷,該 release 將不會被刪除,且當前流程會因為失敗呼叫 fail 並繼續執行,可能導致後續刪除邏輯的不一致。
建議:對於特定的 HTTP 狀態碼(502, 503, 504),建議引入簡單的指數退避重試機制(Exponential Backoff),而不是直接宣告刪除失敗。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `cleanupReleases` 迴圈中執行 DELETE 請求時,未針對網路不穩定或暫時性服務錯誤(如 502, 503, 504)實作重試機制。若刪除過程中發生瞬間網路中斷,該 release 將不會被刪除,且當前流程會因為失敗呼叫 `fail` 並繼續執行,可能導致後續刪除邏輯的不一致。 **建議**:對於特定的 HTTP 狀態碼(502, 503, 504),建議引入簡單的指數退避重試機制(Exponential Backoff),而不是直接宣告刪除失敗。
Ghost marked this conversation as resolved
app/tags.js Outdated
@@ -0,0 +32,4 @@
* @param {ReturnType<import('./config.js').loadConfig>} config
*/
export async function cleanupOrphanTags(client, config) {
section('刪除未指定 release 的 tag')

嚴重等級🔴 嚴重
審查員:Maya
問題:cleanupOrphanTags 涉及多次 API 交互,目前缺乏測試驗證邏輯,無法確保在 API 請求失敗或標籤分類錯誤時系統的行為一致性。
建議:補齊 cleanupOrphanTags 的測試案例,需模擬 fetchAllPages 回傳內容,並驗證對於不同 action(keep/delete/skip)的處理流程是否正確。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:cleanupOrphanTags 涉及多次 API 交互,目前缺乏測試驗證邏輯,無法確保在 API 請求失敗或標籤分類錯誤時系統的行為一致性。 **建議**:補齊 cleanupOrphanTags 的測試案例,需模擬 fetchAllPages 回傳內容,並驗證對於不同 action(keep/delete/skip)的處理流程是否正確。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
info(`TAG_COUNT=${tags.length}`)
for (const { tag, action } of categorizeTags(tags, releaseTagNames)) {
if (action === 'skip') {

嚴重等級🟡 警告
審查員:Rogue
問題:同樣在刪除孤立 tag 時使用了序列化的迴圈,導致同樣的阻塞問題,造成不必要的總執行時間拉長。
建議:同樣改用 Promise.all 將刪除請求並行化,加快清理速度。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:同樣在刪除孤立 tag 時使用了序列化的迴圈,導致同樣的阻塞問題,造成不必要的總執行時間拉長。 **建議**:同樣改用 `Promise.all` 將刪除請求並行化,加快清理速度。
Ghost marked this conversation as resolved
@@ -0,0 +53,4 @@
continue
}
const url = `${config.tagApiUrl}/${tag.name}`

嚴重等級🟡 警告
審查員:Mage
問題:在 cleanupOrphanTags 中,雖然先重新 fetch 了 release 清單,但 cleanupReleasescleanupOrphanTags 是非同步執行,且中間無確保一致性的機制。若在 cleanupReleases 刪除完成後到 cleanupOrphanTags 執行期間,Gitea 上有新的 release 被建立,則 releaseTagNames 的快照將會過時,導致正在使用的 tag 被錯誤刪除。
建議:考慮在兩個 cleanup 步驟之間,確保 API 狀態的一致性,或者在刪除 tag 前再次檢查該 tag 是否真的未被任何現存 release 使用。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `cleanupOrphanTags` 中,雖然先重新 fetch 了 release 清單,但 `cleanupReleases` 和 `cleanupOrphanTags` 是非同步執行,且中間無確保一致性的機制。若在 `cleanupReleases` 刪除完成後到 `cleanupOrphanTags` 執行期間,Gitea 上有新的 release 被建立,則 `releaseTagNames` 的快照將會過時,導致正在使用的 tag 被錯誤刪除。 **建議**:考慮在兩個 cleanup 步驟之間,確保 API 狀態的一致性,或者在刪除 tag 前再次檢查該 tag 是否真的未被任何現存 release 使用。
Ghost marked this conversation as resolved
gitea-actions bot added 1 commit 2026-06-26 02:38:06 +00:00
jiantw83 added 6 commits 2026-06-26 02:55:55 +00:00
為 loadConfig、GiteaClient(含 fetchAllPages/deleteResource)、
selectReleasesToDelete/cleanupReleases、categorizeTags/cleanupOrphanTags
補上完整 JSDoc(參數、回傳、例外、行為說明)。僅註解變更。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增專案列表、公開功能列表(連結至 develop 上的原始碼行)與使用範例;
連結指向 develop 分支以確保合併後仍有效。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
解決 AI review 的安全性與健壯性問題:
- 驗證 GITEA_SERVER_URL 為合法 http/https URL(降低 SSRF 風險)
- 驗證 GITEA_REPOSITORY 為 owner/repo 形式並拒絕路徑穿越
- 為所有 fetch 請求加上 30 秒逾時(AbortSignal.timeout),避免卡死
- fetchAllPages 解析前檢查 content-type,非 JSON 時拋出明確錯誤
- selectReleasesToDelete 對無效 created_at 防呆,避免 NaN 排序
- index.js 匯出 main 並加上直接執行守衛,錯誤輸出含類型與堆疊

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增 cleanupReleases、cleanupOrphanTags 的失敗路徑與分類測試、
loadConfig 環境變數驗證測試、main 整合測試,以及 requireUrl/
requireRepository 與 content-type 檢查測試。測試共 41 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README 新增 requireUrl/requireRepository、修正原始碼連結行號與更新時間;
Dockerfile 調整註解冒號/括號前的空白(無邏輯變更)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(ai-review): 更新 findings.json 與 exclusions.json
CI / AI Code Review (pull_request) Failing after 36s
245f66de8f
移除已修復的 13 條 finding;保留 4 條設計取捨類(重試/TOCTOU/並行化)
待人工評估;將 token log 排版建議登記為誤報於 exclusions.json。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

🤖 AI Code Review 團隊

👤 角色 🎯 面向 🧠 個性
🗡️ Assassin security 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用
🎼 Bard style 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版
🧰 Leo maintainability 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來
🔮 Mage logic 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」
🧪 Maya testing 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑
Rogue efficiency 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」

🔍 服務:opencode 模型:gemini-2.5-flash

## 🤖 AI Code Review 團隊 | 👤 角色 | 🎯 面向 | 🧠 個性 | |--------|--------|--------| | **🗡️ Assassin** | security | 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用 | | **🎼 Bard** | style | 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版 | | **🧰 Leo** | maintainability | 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來 | | **🔮 Mage** | logic | 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」 | | **🧪 Maya** | testing | 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑 | | **⚡ Rogue** | efficiency | 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」 | > 🔍 服務:opencode 模型:gemini-2.5-flash
gitea-actions bot reviewed 2026-06-26 02:56:30 +00:00
gitea-actions bot left a comment

AI Code Review 統計

類型 🔴 嚴重 🟡 警告 🔵 建議 無法標示
新問題 3 筆 9 筆 3 筆 0 筆
舊問題 2 筆 4 筆 0 筆 0 筆

🤖 AI 助理使用量

本次審查(opencode / gemini-2.5-flash,共 31 次呼叫)

提示 token 回應 token 合計
217,041 7,227 501,320

剩餘可用

剩餘可用:無法計算百分比(自架服務,無帳號額度概念)

## AI Code Review 統計 | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | | --- | --- | --- | --- | --- | | 新問題 | 3 筆 | 9 筆 | 3 筆 | 0 筆 | | 舊問題 | 2 筆 | 4 筆 | 0 筆 | 0 筆 | ## 🤖 AI 助理使用量 **本次審查**(opencode / gemini-2.5-flash,共 31 次呼叫) | 提示 token | 回應 token | 合計 | | --- | --- | --- | | 217,041 | 7,227 | 501,320 | **剩餘可用** 剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
app/config.js Outdated
@@ -0,0 +33,4 @@
info(`GITEA_SERVER_URL=${serverUrl}`)
requireValue('GITEA_SERVER_URL', serverUrl)
requireUrl('GITEA_SERVER_URL', serverUrl)

嚴重等級🟡 警告
審查員:Maya
問題:參數 GITEA_TOKEN 若提供,在 loadConfig 中只會檢查其是否為空,且為了資安會遮蔽輸出。這很棒,但缺乏對 GITEA_TOKEN 格式或長度的邊界測試。
建議:在 app/test/config.test.js 中增加測試案例,針對 GITEA_TOKEN 為極短字串(如空字串、單一字元)或極長字串進行測試,確認系統行為符合預期。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:參數 `GITEA_TOKEN` 若提供,在 `loadConfig` 中只會檢查其是否為空,且為了資安會遮蔽輸出。這很棒,但缺乏對 `GITEA_TOKEN` 格式或長度的邊界測試。 **建議**:在 `app/test/config.test.js` 中增加測試案例,針對 `GITEA_TOKEN` 為極短字串(如空字串、單一字元)或極長字串進行測試,確認系統行為符合預期。
Ghost marked this conversation as resolved
@@ -0,0 +30,4 @@
* @param {string} baseUrl 不含 query string API 位址(本方法會自行附加 `?page=N`)
* @returns {Promise<any[]>} 所有頁面合併後的項目陣列;無資料時為空陣列
* @throws {Error} 任一頁回應 HTTP 2xx(`!res.ok`)時拋出,訊息含 URL 與狀態碼
*/

嚴重等級🟡 警告
審查員:Maya
問題fetchAllPages 使用了 AbortSignal.timeout(REQUEST_TIMEOUT_MS),但在測試 app/test/gitea-client.js 時,並未測試過「網路逾時」情境。
建議:在 app/test/gitea-client.js 中增加一個測試案例,模擬 fetch 函數直接拋出 AbortError,驗證 fetchAllPages 能否正確處理該錯誤,而不是讓容器無預警崩潰。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:`fetchAllPages` 使用了 `AbortSignal.timeout(REQUEST_TIMEOUT_MS)`,但在測試 `app/test/gitea-client.js` 時,並未測試過「網路逾時」情境。 **建議**:在 `app/test/gitea-client.js` 中增加一個測試案例,模擬 fetch 函數直接拋出 `AbortError`,驗證 `fetchAllPages` 能否正確處理該錯誤,而不是讓容器無預警崩潰。

嚴重等級🔵 建議
審查員:Rogue
問題fetchAllPages 採取線性逐頁請求,資料量龐大時會導致總請求時間過長。
建議:若 API 支援並行讀取,建議先取得總頁數並行發出請求,而非序列式逐頁讀取。

**嚴重等級**:🔵 建議 **審查員**:Rogue **問題**:`fetchAllPages` 採取線性逐頁請求,資料量龐大時會導致總請求時間過長。 **建議**:若 API 支援並行讀取,建議先取得總頁數並行發出請求,而非序列式逐頁讀取。
Ghost marked this conversation as resolved
@@ -0,0 +45,4 @@
if (!res.ok) {
const body = await res.text().catch(() => '')
throw new Error(
`GET ${url} failed: HTTP ${res.status}${body ? ` - ${body.slice(0, 200)}` : ''}`,

嚴重等級🟡 警告
審查員:Assassin
問題:將 HTTP 錯誤回應內容不加淨化地直接寫入錯誤訊息。攻擊者可能構造特殊的錯誤回應,透過 log 注入或後續錯誤處理機制進行惡意利用。
建議:錯誤訊息應限制內容長度(已做到),建議進一步剝離 HTML 標籤,或僅記錄必要的狀態碼與錯誤類型,避免直接紀錄原始回應內容。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:將 HTTP 錯誤回應內容不加淨化地直接寫入錯誤訊息。攻擊者可能構造特殊的錯誤回應,透過 log 注入或後續錯誤處理機制進行惡意利用。 **建議**:錯誤訊息應限制內容長度(已做到),建議進一步剝離 HTML 標籤,或僅記錄必要的狀態碼與錯誤類型,避免直接紀錄原始回應內容。
Ghost marked this conversation as resolved
@@ -0,0 +51,4 @@
// 確認回應確實是 JSON,避免 API 回傳 HTML 錯誤頁時 res.json() 拋出難以理解的 SyntaxError。
const contentType = res.headers.get('content-type') || ''
if (!contentType.includes('application/json')) {

嚴重等級🟡 警告
審查員:Mage
問題:fetchAllPages 的迴圈終止條件僅檢查 !Array.isArray(items) || items.length === 0。若 API 因為某種狀況(如認證過期或 API 內部錯誤)回傳了非陣列格式的 JSON 錯誤訊息,會直接視為「最後一頁」而提前終止迴圈,導致回傳不完整的清單且未報錯。
建議:應在確認 res.ok 為 true 的前提下,嚴格要求 items 為陣列。若 res.ok 為 true 但 items 不為陣列,應拋出錯誤而非將其視為終止訊號。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:fetchAllPages 的迴圈終止條件僅檢查 `!Array.isArray(items) || items.length === 0`。若 API 因為某種狀況(如認證過期或 API 內部錯誤)回傳了非陣列格式的 JSON 錯誤訊息,會直接視為「最後一頁」而提前終止迴圈,導致回傳不完整的清單且未報錯。 **建議**:應在確認 res.ok 為 true 的前提下,嚴格要求 items 為陣列。若 res.ok 為 true 但 items 不為陣列,應拋出錯誤而非將其視為終止訊號。
Ghost marked this conversation as resolved
@@ -0,0 +52,4 @@
// 確認回應確實是 JSON,避免 API 回傳 HTML 錯誤頁時 res.json() 拋出難以理解的 SyntaxError。
const contentType = res.headers.get('content-type') || ''
if (!contentType.includes('application/json')) {
const body = await res.text().catch(() => '')

嚴重等級🔴 嚴重
審查員:Mage
問題:await res.json() 若遇到 API 回傳格式不符的內容(即使 content-type 為 application/json),會直接拋出 SyntaxError,且因為此處未以 try-catch 包裹,會導致程式崩潰並遺失錯誤發生的 URL 上下文。
建議:將 await res.json() 包裹在 try-catch 區塊中,解析失敗時捕捉錯誤並拋出包含當前請求 URL 的明確錯誤訊息。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:await res.json() 若遇到 API 回傳格式不符的內容(即使 content-type 為 application/json),會直接拋出 SyntaxError,且因為此處未以 try-catch 包裹,會導致程式崩潰並遺失錯誤發生的 URL 上下文。 **建議**:將 await res.json() 包裹在 try-catch 區塊中,解析失敗時捕捉錯誤並拋出包含當前請求 URL 的明確錯誤訊息。
Ghost marked this conversation as resolved
@@ -0,0 +56,4 @@
throw new Error(
`GET ${url} returned non-JSON content-type "${contentType}": ${body.slice(0, 200)}`,
)
}

嚴重等級🔴 嚴重
審查員:Bard
問題:在 fetchAllPages 中使用 all.push(...items) 展開處理分頁資料,若 API 回傳的項目數量龐大(例如數千筆),極可能觸發「超過呼叫堆疊最大長度」(Maximum call stack size exceeded)導致程式崩潰,這對長期運行的工具而言是一大隱憂。
建議:建議改用簡單的迴圈 for (const item of items) { all.push(item); } 來逐一加入項目,這能完美規避展開運算子在處理大型陣列時的堆疊風險,讓程式運行得更加穩健優雅。

**嚴重等級**:🔴 嚴重 **審查員**:Bard **問題**:在 `fetchAllPages` 中使用 `all.push(...items)` 展開處理分頁資料,若 API 回傳的項目數量龐大(例如數千筆),極可能觸發「超過呼叫堆疊最大長度」(Maximum call stack size exceeded)導致程式崩潰,這對長期運行的工具而言是一大隱憂。 **建議**:建議改用簡單的迴圈 `for (const item of items) { all.push(item); }` 來逐一加入項目,這能完美規避展開運算子在處理大型陣列時的堆疊風險,讓程式運行得更加穩健優雅。
Ghost marked this conversation as resolved
@@ -0,0 +64,4 @@
}
all.push(...items)
page += 1

嚴重等級🟡 警告
審查員:Assassin
問題:將 Content-Type 不符的錯誤內容直接寫入錯誤訊息。同樣面臨 log 注入或惡意回應內容的問題。
建議:同上,建議精簡錯誤資訊,移除可能包含惡意 payload 的回應 body 部分。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:將 Content-Type 不符的錯誤內容直接寫入錯誤訊息。同樣面臨 log 注入或惡意回應內容的問題。 **建議**:同上,建議精簡錯誤資訊,移除可能包含惡意 payload 的回應 body 部分。
Ghost marked this conversation as resolved
@@ -0,0 +31,4 @@
*/
function reportFatal(error) {
if (error instanceof Error) {
fail(`${error.name}: ${error.message}`)

嚴重等級🔵 建議
審查員:Leo
問題reportFatal 直接操作 process.stderr 並在 main().catch 中手動調用,這與 logger.js 中定義的 fail 函式職責重疊,這會在未來增加日誌格式變更的維護成本。
建議:建議直接呼叫 fail 函式,並在 logger.js 中根據錯誤類型(是否為 Error 物件)自動處理堆疊追蹤資訊,將「如何輸出錯誤」的邏輯全部收斂至 logger.js

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:`reportFatal` 直接操作 `process.stderr` 並在 `main().catch` 中手動調用,這與 `logger.js` 中定義的 `fail` 函式職責重疊,這會在未來增加日誌格式變更的維護成本。 **建議**:建議直接呼叫 `fail` 函式,並在 `logger.js` 中根據錯誤類型(是否為 Error 物件)自動處理堆疊追蹤資訊,將「如何輸出錯誤」的邏輯全部收斂至 `logger.js`。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
// 僅在直接以 `node index.js` 執行時啟動主流程;被測試 import 時不自動執行,方便撰寫整合測試。
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
reportFatal(error)

嚴重等級🔴 嚴重
審查員:Maya
問題:專案雖然新增了 app/test/ 資料夾與多個測試檔案,但目前沒看到任何整合測試,且 app/index.jsmain() 邏輯未被驗證過,這意味著最核心的清理流程尚未受到測試保護。
建議:請在 app/test/ 中新增整合測試,模擬 loadConfig 回傳假設定後,確認 cleanupReleasescleanupOrphanTags 有被正確呼叫(例如透過 mock GiteaClient),確保流程串接無誤。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:專案雖然新增了 `app/test/` 資料夾與多個測試檔案,但目前沒看到任何整合測試,且 `app/index.js` 的 `main()` 邏輯未被驗證過,這意味著最核心的清理流程尚未受到測試保護。 **建議**:請在 `app/test/` 中新增整合測試,模擬 `loadConfig` 回傳假設定後,確認 `cleanupReleases` 與 `cleanupOrphanTags` 有被正確呼叫(例如透過 mock GiteaClient),確保流程串接無誤。
Ghost marked this conversation as resolved
app/releases.js Outdated
@@ -0,0 +34,4 @@
*/
export async function cleanupReleases(client, config) {
section('取得成品資訊')
info(`GET ${config.releaseApiUrl}`)

嚴重等級🟡 警告
審查員:Leo
問題:這裡的 cleanupReleases 函式同時負責了「取得資料」、「判斷邏輯」與「執行副作用(刪除)」三種責任,這違反了單一職責原則(SRP),隨著 API 呼叫變複雜,這會導致未來很難單獨測試刪除邏輯。
建議:建議將 cleanupReleases 拆分為「取得成品與過濾需要刪除的成品」的純邏輯層,以及「執行刪除請求」的執行層。目前的 selectReleasesToDelete 已經做了一部分,建議把迴圈刪除的部分也封裝成一個負責執行動作的函式。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:這裡的 `cleanupReleases` 函式同時負責了「取得資料」、「判斷邏輯」與「執行副作用(刪除)」三種責任,這違反了單一職責原則(SRP),隨著 API 呼叫變複雜,這會導致未來很難單獨測試刪除邏輯。 **建議**:建議將 `cleanupReleases` 拆分為「取得成品與過濾需要刪除的成品」的純邏輯層,以及「執行刪除請求」的執行層。目前的 `selectReleasesToDelete` 已經做了一部分,建議把迴圈刪除的部分也封裝成一個負責執行動作的函式。
Ghost marked this conversation as resolved
@@ -0,0 +40,4 @@
info(`RELEASE_COUNT=${releases.length}`)
info(`KEEP_COUNT=${config.keepCount}`)
if (releases.length <= config.keepCount) {

嚴重等級🟡 警告
審查員:Rogue
問題:在刪除舊成品時使用了序列化的 for...of 迴圈搭配 await,導致刪除請求一個個排隊等待 API 回應,浪費了寶貴的 I/O 等待時間。
建議:改用 Promise.all 搭配 map 將刪除請求並行化,讓所有請求同時發送,瞬間縮短總執行時間。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:在刪除舊成品時使用了序列化的 `for...of` 迴圈搭配 `await`,導致刪除請求一個個排隊等待 API 回應,浪費了寶貴的 I/O 等待時間。 **建議**:改用 `Promise.all` 搭配 `map` 將刪除請求並行化,讓所有請求同時發送,瞬間縮短總執行時間。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
section('刪除未指定 release 的 tag')
// 重新取得 release 清單,得到刪除舊版本後仍指定 tag 的成品
const currentReleases = await client.fetchAllPages(config.releaseApiUrl)

嚴重等級🟡 警告
審查員:Rogue
問題:在刪除孤立 tag 時使用了序列化的迴圈,導致同樣的阻塞問題,造成不必要的總執行時間拉長。
建議:改用 Promise.all 將刪除請求並行化,加快清理速度。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:在刪除孤立 tag 時使用了序列化的迴圈,導致同樣的阻塞問題,造成不必要的總執行時間拉長。 **建議**:改用 `Promise.all` 將刪除請求並行化,加快清理速度。

嚴重等級🟡 警告
審查員:Leo
問題:在 cleanupOrphanTags 中,直接在主流程中遍歷並執行刪除,同樣面臨未來如果需要對刪除失敗進行更複雜的處理(例如重試、批次處理)時,邏輯會變得難以維護。
建議:建議將 cleanupOrphanTags 參考 cleanupReleases 的結構,將「分類與過濾」與「執行刪除動作」進一步解耦,並對每個 tag 的刪除動作進行更細緻的錯誤處理。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:在 `cleanupOrphanTags` 中,直接在主流程中遍歷並執行刪除,同樣面臨未來如果需要對刪除失敗進行更複雜的處理(例如重試、批次處理)時,邏輯會變得難以維護。 **建議**:建議將 `cleanupOrphanTags` 參考 `cleanupReleases` 的結構,將「分類與過濾」與「執行刪除動作」進一步解耦,並對每個 tag 的刪除動作進行更細緻的錯誤處理。
Ghost marked this conversation as resolved
@@ -0,0 +44,4 @@
*/
export function requireUrl(name, value) {
let url
try {

嚴重等級🔵 建議
審查員:Leo
問題:雖然驗證邏輯完整,但 requireUrlrequireRepository 的規則直接寫死在函式內。如果未來有其他的 API 端點需要不同的驗證規則,會產生大量重複程式碼。
建議:考慮將驗證規則(Regex 或協定清單)提取為設定物件或共用常數,這能讓維護者一眼看出系統允許的 URL 限制,方便未來擴充。

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:雖然驗證邏輯完整,但 `requireUrl` 和 `requireRepository` 的規則直接寫死在函式內。如果未來有其他的 API 端點需要不同的驗證規則,會產生大量重複程式碼。 **建議**:考慮將驗證規則(Regex 或協定清單)提取為設定物件或共用常數,這能讓維護者一眼看出系統允許的 URL 限制,方便未來擴充。
Ghost marked this conversation as resolved
gitea-actions bot added 1 commit 2026-06-26 02:56:32 +00:00
jiantw83 added 5 commits 2026-06-26 03:04:36 +00:00
- res.json() 以 try-catch 包裹,解析失敗時補上 URL 上下文
- res.ok 為真時嚴格要求陣列,非陣列回應改為拋錯而非靜默結束
- 改用迴圈逐一 push 取代展開運算子,避免大量項目的堆疊風險
- 錯誤訊息中的回應內容經 sanitize(移除控制字元)防止 log 注入

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 新增 logger.failError 集中處理 Error/非 Error 的 stderr 輸出與堆疊,
  index.js 改用之,移除重複的 reportFatal
- validate.js 將允許協定與 repository 格式抽為模組常數,便於維護擴充

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增 fetchAllPages 的 AbortError 逾時與非陣列回應測試、loadConfig 的
GITEA_TOKEN 邊界測試,以及 logger.failError 測試。測試共 47 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增 failError 功能列表/使用範例,更新 validate/gitea-client 連結行號與更新時間。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(ai-review): 更新 findings.json
CI / AI Code Review (pull_request) Failing after 1m10s
8dae4aceaf
移除本輪已修復(JSON/陣列處理、sanitize、failError 收斂、常數抽出)與
已存在測試覆蓋的 finding;保留 7 條設計取捨類(重試/TOCTOU/並行化/SRP)待人工評估。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

🤖 AI Code Review 團隊

👤 角色 🎯 面向 🧠 個性
🗡️ Assassin security 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用
🎼 Bard style 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版
🧰 Leo maintainability 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來
🔮 Mage logic 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」
🧪 Maya testing 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑
Rogue efficiency 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」

🔍 服務:opencode 模型:gemini-2.5-flash

## 🤖 AI Code Review 團隊 | 👤 角色 | 🎯 面向 | 🧠 個性 | |--------|--------|--------| | **🗡️ Assassin** | security | 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用 | | **🎼 Bard** | style | 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版 | | **🧰 Leo** | maintainability | 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來 | | **🔮 Mage** | logic | 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」 | | **🧪 Maya** | testing | 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑 | | **⚡ Rogue** | efficiency | 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」 | > 🔍 服務:opencode 模型:gemini-2.5-flash
gitea-actions bot reviewed 2026-06-26 03:05:45 +00:00
gitea-actions bot left a comment

AI Code Review 統計

類型 🔴 嚴重 🟡 警告 🔵 建議 無法標示
新問題 3 筆 7 筆 3 筆 0 筆
舊問題 0 筆 0 筆 0 筆 0 筆

🤖 AI 助理使用量

本次審查(opencode / gemini-2.5-flash,共 21 次呼叫)

提示 token 回應 token 合計
171,376 5,528 388,902

剩餘可用

剩餘可用:無法計算百分比(自架服務,無帳號額度概念)

## AI Code Review 統計 | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | | --- | --- | --- | --- | --- | | 新問題 | 3 筆 | 7 筆 | 3 筆 | 0 筆 | | 舊問題 | 0 筆 | 0 筆 | 0 筆 | 0 筆 | ## 🤖 AI 助理使用量 **本次審查**(opencode / gemini-2.5-flash,共 21 次呼叫) | 提示 token | 回應 token | 合計 | | --- | --- | --- | | 171,376 | 5,528 | 388,902 | **剩餘可用** 剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +56,4 @@
})
if (!res.ok) {
const body = sanitizeBody(await res.text().catch(() => ''))

嚴重等級🔴 嚴重
審查員:Bard
問題:fetchAllPages 採展開運算子處理分頁資料,面對龐大項目數量可能導致 Maximum call stack size exceeded 錯誤,並伴隨無窮迴圈風險 (缺少 MAX_PAGES 限制)。
建議:改用簡單的 for...of 迴圈逐一 push 項目以規避堆疊風險,並務必加入 MAX_PAGES 常數作為安全斷點,防止 API 異常導致無限迴圈與資源耗盡。

**嚴重等級**:🔴 嚴重 **審查員**:Bard **問題**:fetchAllPages 採展開運算子處理分頁資料,面對龐大項目數量可能導致 Maximum call stack size exceeded 錯誤,並伴隨無窮迴圈風險 (缺少 MAX_PAGES 限制)。 **建議**:改用簡單的 for...of 迴圈逐一 push 項目以規避堆疊風險,並務必加入 MAX_PAGES 常數作為安全斷點,防止 API 異常導致無限迴圈與資源耗盡。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:45 +00:00
@@ -0,0 +56,4 @@
})
if (!res.ok) {
const body = sanitizeBody(await res.text().catch(() => ''))

嚴重等級🔴 嚴重
審查員:Bard
問題:fetchAllPages 採展開運算子處理分頁資料,面對龐大項目數量可能導致 Maximum call stack size exceeded 錯誤,並伴隨無窮迴圈風險 (缺少 MAX_PAGES 限制)。
建議:改用簡單的 for...of 迴圈逐一 push 項目以規避堆疊風險,並務必加入 MAX_PAGES 常數作為安全斷點,防止 API 異常導致無限迴圈與資源耗盡。

**嚴重等級**:🔴 嚴重 **審查員**:Bard **問題**:fetchAllPages 採展開運算子處理分頁資料,面對龐大項目數量可能導致 Maximum call stack size exceeded 錯誤,並伴隨無窮迴圈風險 (缺少 MAX_PAGES 限制)。 **建議**:改用簡單的 for...of 迴圈逐一 push 項目以規避堆疊風險,並務必加入 MAX_PAGES 常數作為安全斷點,防止 API 異常導致無限迴圈與資源耗盡。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:46 +00:00
@@ -0,0 +35,4 @@
export async function cleanupReleases(client, config) {
section('取得成品資訊')
info(`GET ${config.releaseApiUrl}`)

嚴重等級🔴 嚴重
審查員:Mage
問題:清理流程對網路請求依賴強,若 API 呼叫失敗,整個 main 流程中斷,導致容器無法確保後續清理的一致性與部分成功重試。
建議:引入更細緻的錯誤處理(如錯誤閾值機制)或部分清理成功後的重試策略,確保清理任務具備健壯性。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:清理流程對網路請求依賴強,若 API 呼叫失敗,整個 main 流程中斷,導致容器無法確保後續清理的一致性與部分成功重試。 **建議**:引入更細緻的錯誤處理(如錯誤閾值機制)或部分清理成功後的重試策略,確保清理任務具備健壯性。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:46 +00:00
@@ -0,0 +33,4 @@
info(`GITEA_SERVER_URL=${serverUrl}`)
requireValue('GITEA_SERVER_URL', serverUrl)
requireUrl('GITEA_SERVER_URL', serverUrl)

嚴重等級🟡 警告
審查員:Maya
問題:缺乏對 GITEA_TOKEN 長度或格式的邊界測試,以及對 KEEP_COUNT 格式異常的檢查。
建議:在測試檔中增加針對 Token 與 KEEP_COUNT 的邊界測試,並在 loadConfig 內加強格式轉換檢查。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:缺乏對 GITEA_TOKEN 長度或格式的邊界測試,以及對 KEEP_COUNT 格式異常的檢查。 **建議**:在測試檔中增加針對 Token 與 KEEP_COUNT 的邊界測試,並在 loadConfig 內加強格式轉換檢查。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:46 +00:00
@@ -0,0 +30,4 @@
this.headers = {}
if (token) {
this.headers.Authorization = `token ${token}`
}

嚴重等級🟡 警告
審查員:Maya
問題:fetchAllPages 使用了逾時訊號,但測試套件未驗證網路逾時情境,且總耗時未受限制。
建議:在測試中模擬 AbortError,並考慮對整個 fetchAllPages 流程引入總執行時間限制。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:fetchAllPages 使用了逾時訊號,但測試套件未驗證網路逾時情境,且總耗時未受限制。 **建議**:在測試中模擬 AbortError,並考慮對整個 fetchAllPages 流程引入總執行時間限制。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:46 +00:00
@@ -0,0 +2,4 @@
const LINE = '=================================================='
const SUBLINE = '--------------------------------------------------'

嚴重等級🟡 警告
審查員:Bard
問題:分隔線字串為魔術字串,硬編碼在模組頂層,不易維護與調整。
建議:將分隔線管理集中化,並考慮提供動態產生方法。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:分隔線字串為魔術字串,硬編碼在模組頂層,不易維護與調整。 **建議**:將分隔線管理集中化,並考慮提供動態產生方法。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:46 +00:00
@@ -0,0 +34,4 @@
*/
export async function cleanupReleases(client, config) {
section('取得成品資訊')
info(`GET ${config.releaseApiUrl}`)

嚴重等級🟡 警告
審查員:Leo
問題:cleanupReleases 違反單一職責原則,同時處理資料獲取、邏輯判斷與副作用執行,不易測試。
建議:拆分邏輯與執行層,將刪除副作用抽象化為獨立函式,以利單獨測試。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:cleanupReleases 違反單一職責原則,同時處理資料獲取、邏輯判斷與副作用執行,不易測試。 **建議**:拆分邏輯與執行層,將刪除副作用抽象化為獨立函式,以利單獨測試。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:46 +00:00
@@ -0,0 +40,4 @@
info(`RELEASE_COUNT=${releases.length}`)
info(`KEEP_COUNT=${config.keepCount}`)
if (releases.length <= config.keepCount) {

嚴重等級🟡 警告
審查員:Rogue
問題:清理成品與刪除 tag 使用序列化迴圈,導致 API 請求逐一排隊,整體執行時間拉長。
建議:改用 Promise.all 搭配 map 將刪除請求並行化以縮短執行時間。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:清理成品與刪除 tag 使用序列化迴圈,導致 API 請求逐一排隊,整體執行時間拉長。 **建議**:改用 Promise.all 搭配 map 將刪除請求並行化以縮短執行時間。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:46 +00:00
@@ -0,0 +43,4 @@
if (releases.length <= config.keepCount) {
success('沒有需要清理的舊版本成品')
return
}

嚴重等級🟡 警告
審查員:Mage
問題:刪除邏輯未針對網路不穩定或特定 HTTP 狀態碼 (502, 503, 504) 實作重試機制,且在遇到失敗時未停止後續請求,導致大量無意義錯誤。
建議:針對特定 HTTP 狀態碼實作指數退避重試機制,並引入錯誤閾值,當失敗次數過高時立即中斷流程。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:刪除邏輯未針對網路不穩定或特定 HTTP 狀態碼 (502, 503, 504) 實作重試機制,且在遇到失敗時未停止後續請求,導致大量無意義錯誤。 **建議**:針對特定 HTTP 狀態碼實作指數退避重試機制,並引入錯誤閾值,當失敗次數過高時立即中斷流程。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:46 +00:00
@@ -0,0 +53,4 @@
if (isEmptyOrNull(id)) {
warn(`略過沒有 id 的成品: ${tag} (${name})`)
continue

嚴重等級🟡 警告
審查員:Assassin
問題:直接將 API 回傳的 id 與 tag 名稱拼接到 URL 中進行 DELETE 操作,未經驗證,存在路徑穿越或 SSRF 風險。
建議:在使用 id 或 tag 名稱構建 URL 前,必須嚴格驗證其字元組成(如僅允許特定格式或編碼處理)。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:直接將 API 回傳的 id 與 tag 名稱拼接到 URL 中進行 DELETE 操作,未經驗證,存在路徑穿越或 SSRF 風險。 **建議**:在使用 id 或 tag 名稱構建 URL 前,必須嚴格驗證其字元組成(如僅允許特定格式或編碼處理)。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:46 +00:00
@@ -0,0 +30,4 @@
this.headers = {}
if (token) {
this.headers.Authorization = `token ${token}`
}

嚴重等級🔵 建議
審查員:Rogue
問題:fetchAllPages 採線性逐頁請求,資料龐大時效能不佳,且回應處理透過 .text() 再轉 JSON 造成重複記憶體開銷。
建議:若 API 支援,先取得總頁數後並行請求,並直接處理 Response 的 ReadableStream 以提升效能。

**嚴重等級**:🔵 建議 **審查員**:Rogue **問題**:fetchAllPages 採線性逐頁請求,資料龐大時效能不佳,且回應處理透過 .text() 再轉 JSON 造成重複記憶體開銷。 **建議**:若 API 支援,先取得總頁數後並行請求,並直接處理 Response 的 ReadableStream 以提升效能。
Ghost marked this conversation as resolved
gitea-actions bot reviewed 2026-06-26 03:05:46 +00:00
@@ -0,0 +44,4 @@
* 要求值為合法的 http/https URL,藉此避免設定來源指向格式錯誤或非預期協定的伺服器(降低 SSRF 風險)
* @param {string} name 欄位名稱,用於組出錯誤訊息
* @param {string} value 待檢查的 URL 字串
* @throws {Error} value 無法解析為 URL 時丟出 `${name} must be a valid URL`;

嚴重等級🔵 建議
審查員:Leo
問題:驗證規則寫死在函式內,易產生重複程式碼,且缺乏擴充性。
建議:將驗證規則提取為設定物件或共用常數,或考慮使用 Zod 等 Schema 套件進行驗證與型別轉換。

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:驗證規則寫死在函式內,易產生重複程式碼,且缺乏擴充性。 **建議**:將驗證規則提取為設定物件或共用常數,或考慮使用 Zod 等 Schema 套件進行驗證與型別轉換。
Ghost marked this conversation as resolved
gitea-actions bot added 1 commit 2026-06-26 03:05:49 +00:00
jiantw83 added 4 commits 2026-06-26 03:22:05 +00:00
- fetchAllPages 加入 MAX_PAGES(1000)安全斷點,避免 API 異常時無限迴圈
- 刪除 release/tag 時對 id 與 tag 名稱做 encodeURIComponent,
  防止特殊字元造成路徑穿越(正常數值/版本字串編碼後不變)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增 fetchAllPages 超過 MAX_PAGES 中止的測試,以及 cleanupReleases/
cleanupOrphanTags 對含 ../ 的 id/tag 名稱編碼的測試。測試共 50 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(ai-review): 更新 findings.json 與 exclusions.json
CI / AI Code Review (pull_request) Failing after 46s
c11440e139
移除本輪已修復(MAX_PAGES、URL 編碼)與已具測試/已收斂的 finding;
保留 5 條設計取捨類(重試/錯誤閾值/SRP/並行化)待人工評估;
將 logger 分隔線常數建議登記為不適用於 exclusions。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

🤖 AI Code Review 團隊

👤 角色 🎯 面向 🧠 個性
🗡️ Assassin security 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用
🎼 Bard style 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版
🧰 Leo maintainability 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來
🔮 Mage logic 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」
🧪 Maya testing 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑
Rogue efficiency 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」

🔍 服務:opencode 模型:gemini-2.5-flash

## 🤖 AI Code Review 團隊 | 👤 角色 | 🎯 面向 | 🧠 個性 | |--------|--------|--------| | **🗡️ Assassin** | security | 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用 | | **🎼 Bard** | style | 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版 | | **🧰 Leo** | maintainability | 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來 | | **🔮 Mage** | logic | 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」 | | **🧪 Maya** | testing | 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑 | | **⚡ Rogue** | efficiency | 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」 | > 🔍 服務:opencode 模型:gemini-2.5-flash
gitea-actions bot reviewed 2026-06-26 03:22:50 +00:00
gitea-actions bot left a comment

AI Code Review 統計

類型 🔴 嚴重 🟡 警告 🔵 建議 無法標示
新問題 2 筆 10 筆 2 筆 0 筆
舊問題 1 筆 1 筆 1 筆 0 筆

🤖 AI 助理使用量

本次審查(opencode / gemini-2.5-flash,共 25 次呼叫)

提示 token 回應 token 合計
206,841 5,581 419,522

剩餘可用

剩餘可用:無法計算百分比(自架服務,無帳號額度概念)

## AI Code Review 統計 | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | | --- | --- | --- | --- | --- | | 新問題 | 2 筆 | 10 筆 | 2 筆 | 0 筆 | | 舊問題 | 1 筆 | 1 筆 | 1 筆 | 0 筆 | ## 🤖 AI 助理使用量 **本次審查**(opencode / gemini-2.5-flash,共 25 次呼叫) | 提示 token | 回應 token | 合計 | | --- | --- | --- | | 206,841 | 5,581 | 419,522 | **剩餘可用** 剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +30,4 @@
const repository = env.GITEA_REPOSITORY
const keepCountRaw = env.KEEP_COUNT
const token = env.GITEA_TOKEN

嚴重等級🔵 建議
審查員:Leo
問題:直接將 env 預設為 process.env,全域相依性可能導致難以追蹤的副作用。
建議:建議在複雜系統中,將環境變數讀取抽離為獨立的 Provider 或 ConfigFactory。

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:直接將 env 預設為 process.env,全域相依性可能導致難以追蹤的副作用。 **建議**:建議在複雜系統中,將環境變數讀取抽離為獨立的 Provider 或 ConfigFactory。
Ghost marked this conversation as resolved
@@ -0,0 +10,4 @@
/**
* 將回應內容整理成可安全寫入錯誤訊息的片段:移除控制字元(避免換行等造成的 log 注入)並限制長度
* @param {string} text 原始回應文字
* @returns {string} 清理後最長 200 字元的片段

嚴重等級🟡 警告
審查員:Maya
問題:sanitizeBody 函式負責 API 回應的字串清理與格式化,但目前缺乏直接的單元測試,無法確保正規表示式能正確處理所有控制字元以及長度限制。
建議:請在 app/test/gitea-client.test.js 中新增 sanitizeBody 的單元測試,務必包含正常字串、包含控制字元的字串、空字串/null 值、以及超過 200 字元的極端案例。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:sanitizeBody 函式負責 API 回應的字串清理與格式化,但目前缺乏直接的單元測試,無法確保正規表示式能正確處理所有控制字元以及長度限制。 **建議**:請在 app/test/gitea-client.test.js 中新增 sanitizeBody 的單元測試,務必包含正常字串、包含控制字元的字串、空字串/null 值、以及超過 200 字元的極端案例。
Ghost marked this conversation as resolved
@@ -0,0 +60,4 @@
const res = await fetch(url, {
headers: this.headers,
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
})

嚴重等級🔴 嚴重
審查員:Mage
問題:在 fetchAllPages 的 while 迴圈中使用了 AbortSignal.timeout。若運行環境的 Node.js 版本低於 16,此程式碼會直接崩潰。
建議:確認運行環境的 Node.js 版本,若未來需向下相容,建議使用 AbortController 搭配 setTimeout 手動實作逾時控制。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:在 fetchAllPages 的 while 迴圈中使用了 AbortSignal.timeout。若運行環境的 Node.js 版本低於 16,此程式碼會直接崩潰。 **建議**:確認運行環境的 Node.js 版本,若未來需向下相容,建議使用 AbortController 搭配 setTimeout 手動實作逾時控制。
Ghost marked this conversation as resolved
@@ -0,0 +63,4 @@
})
if (!res.ok) {
const body = sanitizeBody(await res.text().catch(() => ''))

嚴重等級🟡 警告
審查員:Assassin
問題:攻擊者可透過惡意伺服器回傳極大的錯誤回應內容,利用此處未限制大小的 res.text() 直接讀取整個回應主體,導致記憶體耗盡 (DoS)。
建議:應限制讀取的回應大小,例如在請求前檢查 Content-Length 或使用串流處理並設定讀取限制。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:攻擊者可透過惡意伺服器回傳極大的錯誤回應內容,利用此處未限制大小的 res.text() 直接讀取整個回應主體,導致記憶體耗盡 (DoS)。 **建議**:應限制讀取的回應大小,例如在請求前檢查 Content-Length 或使用串流處理並設定讀取限制。
Ghost marked this conversation as resolved
@@ -0,0 +74,4 @@
if (!contentType.includes('application/json')) {
const body = sanitizeBody(await res.text().catch(() => ''))
throw new Error(
`GET ${url} returned non-JSON content-type "${contentType}"${body ? `: ${body}` : ''}`,

嚴重等級🟡 警告
審查員:Assassin
問題:攻擊者可透過惡意伺服器回傳極大的 JSON 資料,利用此處未限制大小的 res.json() 直接解析整個回應,導致記憶體耗盡 (DoS)。
建議:應對 API 回應設定明確的大小上限,超過時拒絕解析並拋出異常。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:攻擊者可透過惡意伺服器回傳極大的 JSON 資料,利用此處未限制大小的 res.json() 直接解析整個回應,導致記憶體耗盡 (DoS)。 **建議**:應對 API 回應設定明確的大小上限,超過時拒絕解析並拋出異常。
Ghost marked this conversation as resolved
@@ -0,0 +98,4 @@
for (const item of items) {
all.push(item)
}
page += 1

嚴重等級🟡 警告
審查員:Rogue
問題:在處理分頁資料時,使用 for...of 逐一執行 all.push(item),導致大量記憶體配置與無謂的物件拷貝,效率不佳。
建議:建議直接將 items 整批解構並存入 all,或者使用 Array.prototype.push.apply(all, items) 來減少迴圈開銷。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:在處理分頁資料時,使用 for...of 逐一執行 all.push(item),導致大量記憶體配置與無謂的物件拷貝,效率不佳。 **建議**:建議直接將 items 整批解構並存入 all,或者使用 Array.prototype.push.apply(all, items) 來減少迴圈開銷。
Ghost marked this conversation as resolved
@@ -0,0 +28,4 @@
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
failError(error)
process.exit(1)

嚴重等級🔴 嚴重
審查員:Maya
問題:main 函式執行失敗時會觸發 process.exit(1),確保 CI/CD 流程能正確偵測錯誤。目前的測試僅驗證了 Promise 被 reject,但並未驗證程式是否真的正確以非零狀態碼結束。
建議:請在 app/test/main.test.js 中,模擬 main 拋出錯誤的情境,並透過 mock process.exit 來驗證當 main 執行失敗時,程式碼確實執行了 process.exit(1)。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:main 函式執行失敗時會觸發 process.exit(1),確保 CI/CD 流程能正確偵測錯誤。目前的測試僅驗證了 Promise 被 reject,但並未驗證程式是否真的正確以非零狀態碼結束。 **建議**:請在 app/test/main.test.js 中,模擬 main 拋出錯誤的情境,並透過 mock process.exit 來驗證當 main 執行失敗時,程式碼確實執行了 process.exit(1)。
Ghost marked this conversation as resolved
@@ -0,0 +6,4 @@
/**
* 在前後換行的情況下輸出一條等號分隔線至 stdout,用於視覺上區隔不同階段的輸出
*/
export function separator() {

嚴重等級🟡 警告
審查員:Maya
問題:logger.js 中的輸出函式(如 separator, section, info, success, warn, fail)負責 Action 的核心視覺輸出格式,但目前缺乏測試驗證其實際輸出內容是否正確對齊並符合格式要求。
建議:請在 app/test/logger.test.js 中補齊對這些函式的測試,驗證其是否正確寫入預期的格式內容(含分隔線與正確的前綴)到 stdout。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:logger.js 中的輸出函式(如 separator, section, info, success, warn, fail)負責 Action 的核心視覺輸出格式,但目前缺乏測試驗證其實際輸出內容是否正確對齊並符合格式要求。 **建議**:請在 app/test/logger.test.js 中補齊對這些函式的測試,驗證其是否正確寫入預期的格式內容(含分隔線與正確的前綴)到 stdout。
Ghost marked this conversation as resolved
@@ -0,0 +14,4 @@
export function selectReleasesToDelete(releases, keepCount) {
// 解析 created_at;格式無效或缺漏時視為最舊(0),避免 NaN 造成排序結果不可預期。
const createdTime = (release) => {
const time = Date.parse(release?.created_at)

嚴重等級🟡 警告
審查員:Rogue
問題:selectReleasesToDelete 針對每一筆 release 重複執行 Date.parse,浪費 CPU 週期。
建議:應在排序前先執行一次 map 轉換,預先計算時間戳記。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:selectReleasesToDelete 針對每一筆 release 重複執行 Date.parse,浪費 CPU 週期。 **建議**:應在排序前先執行一次 map 轉換,預先計算時間戳記。
Ghost marked this conversation as resolved
@@ -0,0 +34,4 @@
*/
export async function cleanupReleases(client, config) {
section('取得成品資訊')
info(`GET ${config.releaseApiUrl}`)

嚴重等級🟡 警告
審查員:Leo
問題:cleanupReleases 同時處理資料獲取、邏輯判斷與副作用執行,違反單一職責原則,且未對 release 物件結構進行進一步驗證,可能導致資料正確性風險。
建議:拆分邏輯與執行層,將刪除副作用抽象化為獨立函式。建議增加對於 release 物件結構的進一步驗證,並強化檢查邏輯。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:cleanupReleases 同時處理資料獲取、邏輯判斷與副作用執行,違反單一職責原則,且未對 release 物件結構進行進一步驗證,可能導致資料正確性風險。 **建議**:拆分邏輯與執行層,將刪除副作用抽象化為獨立函式。建議增加對於 release 物件結構的進一步驗證,並強化檢查邏輯。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
if (releases.length <= config.keepCount) {
success('沒有需要清理的舊版本成品')
return
}

嚴重等級🟡 警告
審查員:Mage
問題:刪除邏輯未針對網路不穩定或特定 HTTP 狀態碼(502, 503, 504)實作重試機制,且遇到失敗時未停止後續請求。若 release 清單非常龐大,會佔用大量記憶體。
建議:針對特定 HTTP 狀態碼實作指數退避重試,並引入錯誤閾值。考慮在 fetchAllPages 中加入串流處理(Stream)或實作分批讀取機制,避免將所有資料一次性載入記憶體。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:刪除邏輯未針對網路不穩定或特定 HTTP 狀態碼(502, 503, 504)實作重試機制,且遇到失敗時未停止後續請求。若 release 清單非常龐大,會佔用大量記憶體。 **建議**:針對特定 HTTP 狀態碼實作指數退避重試,並引入錯誤閾值。考慮在 fetchAllPages 中加入串流處理(Stream)或實作分批讀取機制,避免將所有資料一次性載入記憶體。
Ghost marked this conversation as resolved
@@ -0,0 +18,4 @@
const keep = new Set(releaseTagNames)
return tags.map((tag) => {
if (isEmptyOrNull(tag.name)) {

嚴重等級🔵 建議
審查員:Leo
問題:categorizeTags 內部頻繁轉換 Set,若此函式被頻繁呼叫,可能造成效能浪費。
建議:建議調整設計,讓 categorizeTags 接收已經轉好的 Set,將轉換開銷移至上層。

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:categorizeTags 內部頻繁轉換 Set,若此函式被頻繁呼叫,可能造成效能浪費。 **建議**:建議調整設計,讓 categorizeTags 接收已經轉好的 Set<string>,將轉換開銷移至上層。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
section('刪除未指定 release 的 tag')
// 重新取得 release 清單,得到刪除舊版本後仍指定 tag 的成品
const currentReleases = await client.fetchAllPages(config.releaseApiUrl)

嚴重等級🟡 警告
審查員:Rogue
問題:cleanupOrphanTags 在迴圈中重複掃描所有 release 名稱陣列,時間複雜度為 O(tags * releases),效能低落。
建議:應在進入 tag 迴圈前,先將所有 release 的 tag 名稱轉成一個 Set 物件,將查找複雜度降為 O(1)。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:cleanupOrphanTags 在迴圈中重複掃描所有 release 名稱陣列,時間複雜度為 O(tags * releases),效能低落。 **建議**:應在進入 tag 迴圈前,先將所有 release 的 tag 名稱轉成一個 Set 物件,將查找複雜度降為 O(1)。
Ghost marked this conversation as resolved
@@ -0,0 +11,4 @@
* @param {*} value 待判斷的值
* @returns {boolean} 當值為 `undefined``null`空字串或字串 `"null"` 時回傳 true
*/
export function isEmptyOrNull(value) {

嚴重等級🟡 警告
審查員:Mage
問題:isEmptyOrNull 判斷式包含 value === 'null',導致正確字串內容 'null' 被判定為空,破壞語義一致性。
建議:建議移除對 'null' 字串的特殊判定,改由呼叫端明確處理邏輯。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:isEmptyOrNull 判斷式包含 value === 'null',導致正確字串內容 'null' 被判定為空,破壞語義一致性。 **建議**:建議移除對 'null' 字串的特殊判定,改由呼叫端明確處理邏輯。
Ghost marked this conversation as resolved
gitea-actions bot added 1 commit 2026-06-26 03:22:52 +00:00
jiantw83 added 4 commits 2026-06-26 03:29:38 +00:00
排序前先一次性計算各 release 的 created_at 時間戳,避免在排序比較器中
重複呼叫 Date.parse;排序結果與原行為等價。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增子行程驗證 index.js 失敗時以非零狀態結束、fetchAllPages 錯誤訊息
清理控制字元與長度限制、以及 logger 各輸出函式格式與 stdout/stderr 分流測試。
測試共 56 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(ai-review): 更新 findings.json 與 exclusions.json
CI / AI Code Review (pull_request) Failing after 50s
1cc217f04e
移除本輪已修復(Date.parse 預算、結束碼/sanitizeBody/logger 測試)的 finding;
保留 7 條設計取捨類(重試/錯誤閾值/SRP/並行化/回應大小 DoS 防護);
將 6 條不適用或誤報(Node<16、push.apply、'null' 字串、已用 Set、env 可注入、
Set 轉換)登記至 exclusions。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

🤖 AI Code Review 團隊

👤 角色 🎯 面向 🧠 個性
🗡️ Assassin security 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用
🎼 Bard style 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版
🧰 Leo maintainability 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來
🔮 Mage logic 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」
🧪 Maya testing 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑
Rogue efficiency 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」

🔍 服務:opencode 模型:gemini-2.5-flash

## 🤖 AI Code Review 團隊 | 👤 角色 | 🎯 面向 | 🧠 個性 | |--------|--------|--------| | **🗡️ Assassin** | security | 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用 | | **🎼 Bard** | style | 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版 | | **🧰 Leo** | maintainability | 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來 | | **🔮 Mage** | logic | 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」 | | **🧪 Maya** | testing | 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑 | | **⚡ Rogue** | efficiency | 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」 | > 🔍 服務:opencode 模型:gemini-2.5-flash
gitea-actions bot reviewed 2026-06-26 03:30:27 +00:00
gitea-actions bot left a comment

AI Code Review 統計

類型 🔴 嚴重 🟡 警告 🔵 建議 無法標示
新問題 4 筆 4 筆 4 筆 0 筆
舊問題 1 筆 7 筆 0 筆 0 筆

🤖 AI 助理使用量

本次審查(opencode / gemini-2.5-flash,共 29 次呼叫)

提示 token 回應 token 合計
197,872 7,617 556,159

剩餘可用

剩餘可用:無法計算百分比(自架服務,無帳號額度概念)

## AI Code Review 統計 | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | | --- | --- | --- | --- | --- | | 新問題 | 4 筆 | 4 筆 | 4 筆 | 0 筆 | | 舊問題 | 1 筆 | 7 筆 | 0 筆 | 0 筆 | ## 🤖 AI 助理使用量 **本次審查**(opencode / gemini-2.5-flash,共 29 次呼叫) | 提示 token | 回應 token | 合計 | | --- | --- | --- | | 197,872 | 7,617 | 556,159 | **剩餘可用** 剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +22,4 @@
/**
* Gitea REST API 溝通的輕量 HTTP 客戶端,負責帶上認證標頭分頁讀取清單與發出刪除請求
* 取代原 bash 版本以 `curl`/`jq` 進行的 API 操作
*/

嚴重等級🔵 建議
審查員:Bard
問題:在 GiteaClient 建構子中,this.headers 初始化時僅簡單檢查 token 是否存在,且假設所有請求都適用這組標頭。雖然目前專案單純,但若未來擴充需針對不同 API 採取不同標頭時,此處結構會稍顯死板。
建議:考慮將產生 header 的邏輯抽離成一個內部 private 函式(如 _getHeaders()),即便現在很簡單,也能增加未來的彈性。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:在 `GiteaClient` 建構子中,`this.headers` 初始化時僅簡單檢查 `token` 是否存在,且假設所有請求都適用這組標頭。雖然目前專案單純,但若未來擴充需針對不同 API 採取不同標頭時,此處結構會稍顯死板。 **建議**:考慮將產生 header 的邏輯抽離成一個內部 private 函式(如 `_getHeaders()`),即便現在很簡單,也能增加未來的彈性。
Ghost marked this conversation as resolved
@@ -0,0 +20,4 @@
// 先一次性計算每筆的時間戳,避免在排序比較中重複呼叫 Date.parse。
return releases
.map((release) => ({ release, time: createdTime(release) }))
.sort((a, b) => b.time - a.time)

嚴重等級🔵 建議
審查員:Rogue
問題:為了排序而進行了多次 map 操作,先將陣列 map 為物件陣列,排序後又 map 回原始物件陣列。這在大數據集下會造成多次 O(n) 的陣列分配與垃圾回收壓力,浪費 CPU 與記憶體週期。
建議:排序時應嘗試減少陣列中間狀態的產生,例如考慮使用原地排序(若不介意原陣列變更)或簡化 mapping 的邏輯。

**嚴重等級**:🔵 建議 **審查員**:Rogue **問題**:為了排序而進行了多次 map 操作,先將陣列 map 為物件陣列,排序後又 map 回原始物件陣列。這在大數據集下會造成多次 O(n) 的陣列分配與垃圾回收壓力,浪費 CPU 與記憶體週期。 **建議**:排序時應嘗試減少陣列中間狀態的產生,例如考慮使用原地排序(若不介意原陣列變更)或簡化 mapping 的邏輯。
Ghost marked this conversation as resolved
@@ -0,0 +25,4 @@
.slice(keepCount)
}
/**

嚴重等級🔵 建議
審查員:Bard
問題:在 cleanupReleases 函式中,參數 configReturnType<import('./config.js').loadConfig>,這種型別定義方式雖然準確,但過於冗長且與實作細節耦合過深,影響程式碼的可讀性與簡潔度。
建議:建議在 app/config.js 定義並匯出型別註解(JSDoc @typedef),然後在此處直接使用該別名,提升整體程式碼的可讀性。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:在 `cleanupReleases` 函式中,參數 `config` 是 `ReturnType<import('./config.js').loadConfig>`,這種型別定義方式雖然準確,但過於冗長且與實作細節耦合過深,影響程式碼的可讀性與簡潔度。 **建議**:建議在 `app/config.js` 定義並匯出型別註解(JSDoc @typedef),然後在此處直接使用該別名,提升整體程式碼的可讀性。
Ghost marked this conversation as resolved
@@ -0,0 +45,4 @@
info(`KEEP_COUNT=${config.keepCount}`)
if (releases.length <= config.keepCount) {
success('沒有需要清理的舊版本成品')

嚴重等級🔴 嚴重
審查員:Assassin
問題:攻擊者可以透過控制 GITEA_REPOSITORY 環境變數,在其名稱中包含路徑穿越序列(如 '../'),進而操控 API 的刪除目標,造成越權刪除其他儲存庫的成品。
建議:在將 id 拼接進 URL 前,務必使用 encodeURIComponent(id) 對 id 進行編碼,確保其僅被視為路徑的一部分,而非路徑控制字元。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:攻擊者可以透過控制 GITEA_REPOSITORY 環境變數,在其名稱中包含路徑穿越序列(如 '../'),進而操控 API 的刪除目標,造成越權刪除其他儲存庫的成品。 **建議**:在將 id 拼接進 URL 前,務必使用 encodeURIComponent(id) 對 id 進行編碼,確保其僅被視為路徑的一部分,而非路徑控制字元。
Ghost marked this conversation as resolved
@@ -0,0 +56,4 @@
const { id, tag_name: tag, name } = release
if (isEmptyOrNull(id)) {
warn(`略過沒有 id 的成品: ${tag} (${name})`)

嚴重等級🟡 警告
審查員:Mage
問題:使用字串模板直接拼接 API URL:const url = ${config.releaseApiUrl}/${id};。若 API 回傳的 id 包含 / 或特殊字元,可能導致拼接出錯誤的 API 路徑,甚至在某些處理器上導致路徑穿越,雖是 Gitea API 但應防禦性地進行 URL 編碼。
建議:建議使用 encodeURIComponent(id) 對 id 進行編碼後再拼接。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:使用字串模板直接拼接 API URL:`const url = `${config.releaseApiUrl}/${id}`;`。若 API 回傳的 id 包含 `/` 或特殊字元,可能導致拼接出錯誤的 API 路徑,甚至在某些處理器上導致路徑穿越,雖是 Gitea API 但應防禦性地進行 URL 編碼。 **建議**:建議使用 encodeURIComponent(id) 對 id 進行編碼後再拼接。
Ghost marked this conversation as resolved
@@ -0,0 +62,4 @@
}
// 對 tag 名稱做編碼,避免名稱中的特殊字元被拼接進 URL(防路徑穿越);一般 tag 名編碼後不變。
const url = `${config.tagApiUrl}/${encodeURIComponent(tag.name)}`

嚴重等級🟡 警告
審查員:Mage
問題:與 releases.js 相同問題,在拼接 tag URL 時:const url = ${config.tagApiUrl}/${tag.name}; 未對 tag 名稱進行 URL 編碼。Tag 名稱若包含 / 等特殊字元,會破壞 URL 結構。
建議:建議使用 encodeURIComponent(tag.name) 對 tag 名稱進行編碼。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:與 releases.js 相同問題,在拼接 tag URL 時:`const url = `${config.tagApiUrl}/${tag.name}`;` 未對 tag 名稱進行 URL 編碼。Tag 名稱若包含 `/` 等特殊字元,會破壞 URL 結構。 **建議**:建議使用 encodeURIComponent(tag.name) 對 tag 名稱進行編碼。
Ghost marked this conversation as resolved
@@ -0,0 +67,4 @@
const code = await client.deleteResource(url)
if (code === 204) {
success(`成功刪除未指定 release 的 tag: ${tag.name}`)

嚴重等級🔴 嚴重
審查員:Assassin
問題:攻擊者可以透過控制 GITEA_REPOSITORY 環境變數,在其名稱中包含路徑穿越序列,進而操控 API 的刪除目標;同時,tag 名稱若包含惡意字元也可能造成路徑穿越。
建議:同樣地,在將 tag.name 拼接進 URL 前,必須使用 encodeURIComponent(tag.name) 對其進行編碼。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:攻擊者可以透過控制 GITEA_REPOSITORY 環境變數,在其名稱中包含路徑穿越序列,進而操控 API 的刪除目標;同時,tag 名稱若包含惡意字元也可能造成路徑穿越。 **建議**:同樣地,在將 tag.name 拼接進 URL 前,必須使用 encodeURIComponent(tag.name) 對其進行編碼。
Ghost marked this conversation as resolved
@@ -0,0 +60,4 @@
test('loadConfig 在 GITEA_TOKEN 為空字串時視為匿名(token 為 null)', () => {
const cfg = loadConfig({ ...base, GITEA_TOKEN: '' })
assert.equal(cfg.token, null)
})

嚴重等級🔴 嚴重
審查員:Maya
問題:測試只驗證了正常與極端的 token 輸入,但完全沒有測試 token 為 null 或未定義(匿名模式)下的處理邏輯,也沒確認匿名請求時,設定物件是否正確地將 token 設為 null。
建議:請增加測試案例,明確斷言當 GITEA_TOKEN 不存在於環境變數時,loadConfig() 回傳的 token 屬性為 null。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:測試只驗證了正常與極端的 token 輸入,但完全沒有測試 token 為 null 或未定義(匿名模式)下的處理邏輯,也沒確認匿名請求時,設定物件是否正確地將 token 設為 null。 **建議**:請增加測試案例,明確斷言當 GITEA_TOKEN 不存在於環境變數時,loadConfig() 回傳的 token 屬性為 null。
Ghost marked this conversation as resolved
@@ -0,0 +30,4 @@
})
test('fetchAllPages 逐頁讀取直到空陣列', async () => {
const pages = {

嚴重等級🔴 嚴重
審查員:Maya
問題:測試中雖然有 fetchAllPages 的功能測試,但對於 MAX_PAGES 的邊界情況,測試只用了 globalThis.fetch 永遠回傳非空陣列,這是一個快樂路徑的極端變體。如果 API 剛好在第 MAX_PAGES 頁回傳空陣列,測試並未驗證客戶端是否能正確處理並停止。
建議:請增加一個測試案例,模擬當 API 恰好在第 MAX_PAGES 次請求時回傳空陣列的情境,確認客戶端能成功結束,而非拋出例外。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:測試中雖然有 fetchAllPages 的功能測試,但對於 MAX_PAGES 的邊界情況,測試只用了 globalThis.fetch 永遠回傳非空陣列,這是一個快樂路徑的極端變體。如果 API 剛好在第 MAX_PAGES 頁回傳空陣列,測試並未驗證客戶端是否能正確處理並停止。 **建議**:請增加一個測試案例,模擬當 API 恰好在第 MAX_PAGES 次請求時回傳空陣列的情境,確認客戶端能成功結束,而非拋出例外。
Ghost marked this conversation as resolved
@@ -0,0 +74,4 @@
test('cleanupReleases 在未超過保留數時不刪除', async () => {
const client = fakeClient([
{ id: 1, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
])

嚴重等級🟡 警告
審查員:Maya
問題:在 cleanupReleases 的整合測試中,雖然有測試 deleteResource 回傳非 204 時的行為,但測試只檢查了 client.deleted 的呼叫順序,並沒有驗證 fail 日誌(對 stderr 的寫入)是否正確被觸發。
建議:建議攔截 process.stderr.write,確認當 deleteResource 回傳 500 時,系統確實有記錄到錯誤訊息。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:在 `cleanupReleases` 的整合測試中,雖然有測試 `deleteResource` 回傳非 204 時的行為,但測試只檢查了 `client.deleted` 的呼叫順序,並沒有驗證 `fail` 日誌(對 stderr 的寫入)是否正確被觸發。 **建議**:建議攔截 `process.stderr.write`,確認當 `deleteResource` 回傳 500 時,系統確實有記錄到錯誤訊息。
Ghost marked this conversation as resolved
@@ -0,0 +70,4 @@
})
test('cleanupOrphanTags 在所有 tag 都被指定時不刪除', async () => {
const client = fakeClient({

嚴重等級🟡 警告
審查員:Maya
問題:在 cleanupOrphanTags 的測試中,缺乏對於「當 deleteResource 失敗」時的行為驗證。目前只測試了成功的情境。
建議:增加模擬 deleteResource 回傳非 204 狀態碼的情境,確認該項刪除失敗不會中斷整個標籤清理流程,並檢查是否有對應的錯誤日誌。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:在 `cleanupOrphanTags` 的測試中,缺乏對於「當 `deleteResource` 失敗」時的行為驗證。目前只測試了成功的情境。 **建議**:增加模擬 `deleteResource` 回傳非 204 狀態碼的情境,確認該項刪除失敗不會中斷整個標籤清理流程,並檢查是否有對應的錯誤日誌。
Ghost marked this conversation as resolved
@@ -0,0 +17,4 @@
test('isEmptyOrNull 對有效值回傳 false', () => {
assert.equal(isEmptyOrNull('value'), false)
assert.equal(isEmptyOrNull('0'), false)

嚴重等級🔵 建議
審查員:Maya
問題:雖然有 requireInteger 的測試,但沒有測試當 KEEP_COUNT0 時的邊界情況。雖然 0 在邏輯上可能是允許的,但這對於清理邏輯來說是個關鍵的邊界。
建議:明確測試 requireInteger('KEEP_COUNT', '0') 並斷言其不應拋出錯誤,確保系統允許保留 0 個成品的配置。

**嚴重等級**:🔵 建議 **審查員**:Maya **問題**:雖然有 `requireInteger` 的測試,但沒有測試當 `KEEP_COUNT` 為 `0` 時的邊界情況。雖然 0 在邏輯上可能是允許的,但這對於清理邏輯來說是個關鍵的邊界。 **建議**:明確測試 `requireInteger('KEEP_COUNT', '0')` 並斷言其不應拋出錯誤,確保系統允許保留 0 個成品的配置。
Ghost marked this conversation as resolved
gitea-actions bot added 1 commit 2026-06-26 03:30:29 +00:00
jiantw83 added 2 commits 2026-06-26 03:33:56 +00:00
新增 fetchAllPages 於第 MAX_PAGES 頁回空陣列時正常結束的測試,以及
cleanupReleases/cleanupOrphanTags 在 deleteResource 回非 204 時繼續處理
並寫入 stderr 錯誤記錄的測試。測試共 59 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(ai-review): 更新 findings.json 與 exclusions.json
CI / AI Code Review (pull_request) Failing after 32s
b57180153e
移除本輪已具測試覆蓋的 finding(MAX_PAGES 邊界、刪除失敗記錄)與多項
先前已修復卻被重複提報者;保留 6 條設計取捨類待人工評估;新增 2 條不適用
(_getHeaders 過度設計、排序中間陣列與前次優化衝突)至 exclusions。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

🤖 AI Code Review 團隊

👤 角色 🎯 面向 🧠 個性
🗡️ Assassin security 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用
🎼 Bard style 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版
🧰 Leo maintainability 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來
🔮 Mage logic 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」
🧪 Maya testing 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑
Rogue efficiency 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」

🔍 服務:opencode 模型:gemini-2.5-flash

## 🤖 AI Code Review 團隊 | 👤 角色 | 🎯 面向 | 🧠 個性 | |--------|--------|--------| | **🗡️ Assassin** | security | 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用 | | **🎼 Bard** | style | 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版 | | **🧰 Leo** | maintainability | 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來 | | **🔮 Mage** | logic | 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」 | | **🧪 Maya** | testing | 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑 | | **⚡ Rogue** | efficiency | 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」 | > 🔍 服務:opencode 模型:gemini-2.5-flash
gitea-actions bot reviewed 2026-06-26 03:34:26 +00:00
gitea-actions bot left a comment

AI Code Review 統計

類型 🔴 嚴重 🟡 警告 🔵 建議 無法標示
新問題 1 筆 5 筆 4 筆 0 筆
舊問題 2 筆 6 筆 0 筆 0 筆

🤖 AI 助理使用量

本次審查(opencode / gemini-2.5-flash,共 26 次呼叫)

提示 token 回應 token 合計
154,690 5,679 467,446

剩餘可用

剩餘可用:無法計算百分比(自架服務,無帳號額度概念)

## AI Code Review 統計 | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | | --- | --- | --- | --- | --- | | 新問題 | 1 筆 | 5 筆 | 4 筆 | 0 筆 | | 舊問題 | 2 筆 | 6 筆 | 0 筆 | 0 筆 | ## 🤖 AI 助理使用量 **本次審查**(opencode / gemini-2.5-flash,共 26 次呼叫) | 提示 token | 回應 token | 合計 | | --- | --- | --- | | 154,690 | 5,679 | 467,446 | **剩餘可用** 剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +31,4 @@
const keepCountRaw = env.KEEP_COUNT
const token = env.GITEA_TOKEN
info(`GITEA_SERVER_URL=${serverUrl}`)

嚴重等級🟡 警告
審查員:Assassin
問題:將環境變數值直接輸出至日誌,若 GITEA_SERVER_URL 等變數內容被注入惡意字元或過長,可能造成 Log Injection 或日誌系統資源耗盡。
建議:在輸出前進行 sanitization,移除控制字元並限制長度,類似於 gitea-client.js 中的 sanitizeBody

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:將環境變數值直接輸出至日誌,若 `GITEA_SERVER_URL` 等變數內容被注入惡意字元或過長,可能造成 Log Injection 或日誌系統資源耗盡。 **建議**:在輸出前進行 sanitization,移除控制字元並限制長度,類似於 `gitea-client.js` 中的 `sanitizeBody`。
Ghost marked this conversation as resolved
@@ -0,0 +32,4 @@
const token = env.GITEA_TOKEN
info(`GITEA_SERVER_URL=${serverUrl}`)
requireValue('GITEA_SERVER_URL', serverUrl)

嚴重等級🔴 嚴重
審查員:Assassin
問題:僅驗證是否為 URL 格式,未檢查傳入的 GITEA_SERVER_URL 是否指向內部網路敏感資源(如 localhost, 169.254.169.254 等),這在容器化環境中可能導致 SSRF(伺服器端請求偽造)。
建議:在 validate.jsrequireUrl 中加入黑名單機制,禁止解析為內部 IP 位址或 loopback 位址。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:僅驗證是否為 URL 格式,未檢查傳入的 `GITEA_SERVER_URL` 是否指向內部網路敏感資源(如 localhost, 169.254.169.254 等),這在容器化環境中可能導致 SSRF(伺服器端請求偽造)。 **建議**:在 `validate.js` 的 `requireUrl` 中加入黑名單機制,禁止解析為內部 IP 位址或 loopback 位址。
Ghost marked this conversation as resolved
@@ -0,0 +7,4 @@
// 分頁讀取的最大頁數上限,作為安全斷點:即使 API 異常未以空陣列結尾,也不致無限迴圈耗盡資源。
const MAX_PAGES = 1000
/**

嚴重等級🔵 建議
審查員:Leo
問題:目前 MAX_PAGES 是寫死在程式碼中的常數,未來如果 API 規格變更或是特殊儲存庫的 Release 數量激增,維護者需要進程式碼修改,且這在不同的環境下可能需要不同的上限。
建議:建議將 MAX_PAGES 改為透過環境變數傳入,並設定一個合理的預設值,增加部署時的彈性。

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:目前 `MAX_PAGES` 是寫死在程式碼中的常數,未來如果 API 規格變更或是特殊儲存庫的 Release 數量激增,維護者需要進程式碼修改,且這在不同的環境下可能需要不同的上限。 **建議**:建議將 `MAX_PAGES` 改為透過環境變數傳入,並設定一個合理的預設值,增加部署時的彈性。
Ghost marked this conversation as resolved
@@ -0,0 +103,4 @@
return all
}

嚴重等級🟡 警告
審查員:Assassin
問題:將 items 直接放入 all 陣列。若 API 返回異常巨大的 JSON 陣列,可能導致容器記憶體耗盡(DoS)。(註: 亦包含 Bard 關於 for...of/push 的效能建議)
建議:考慮在 fetchAllPages 中增加最大總項目數量的限制,並在超過時拋出錯誤;同時建議使用 AsyncGenerator 進行串流式處理以降低記憶體佔用。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:將 `items` 直接放入 `all` 陣列。若 API 返回異常巨大的 JSON 陣列,可能導致容器記憶體耗盡(DoS)。(註: 亦包含 Bard 關於 for...of/push 的效能建議) **建議**:考慮在 `fetchAllPages` 中增加最大總項目數量的限制,並在超過時拋出錯誤;同時建議使用 `AsyncGenerator` 進行串流式處理以降低記憶體佔用。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
export function warn(message) {
process.stdout.write(`[WARN] ${message}\n`)
}

嚴重等級🔵 建議
審查員:Leo
問題:雖然目前只有 fail 函式會寫入 stderr,但如果有更多的 error level 需要處理,分散的邏輯會增加維護成本。
建議:考慮在 logger.js 中建立一個通用的 log 函式,處理 levelprefixstream 的對應,讓其他方法(如 info, warn, fail)只負責呼叫該通用函式,降低重複程式碼。

**嚴重等級**:🔵 建議 **審查員**:Leo **問題**:雖然目前只有 `fail` 函式會寫入 `stderr`,但如果有更多的 error level 需要處理,分散的邏輯會增加維護成本。 **建議**:考慮在 `logger.js` 中建立一個通用的 `log` 函式,處理 `level`、`prefix` 與 `stream` 的對應,讓其他方法(如 `info`, `warn`, `fail`)只負責呼叫該通用函式,降低重複程式碼。
Ghost marked this conversation as resolved
@@ -0,0 +50,4 @@
*/
export function fail(message) {
process.stderr.write(`[ERR] ${message}\n`)
}

嚴重等級🔵 建議
審查員:Assassin
問題:儘管使用了 stderr 輸出錯誤,但在 failError 中直接輸出 error.stack 可能會洩漏專案目錄結構、內部函式名稱等敏感路徑資訊。
建議:在生產環境下考慮隱藏堆疊追蹤,或僅在特定 debug 模式下輸出堆疊。

**嚴重等級**:🔵 建議 **審查員**:Assassin **問題**:儘管使用了 `stderr` 輸出錯誤,但在 `failError` 中直接輸出 `error.stack` 可能會洩漏專案目錄結構、內部函式名稱等敏感路徑資訊。 **建議**:在生產環境下考慮隱藏堆疊追蹤,或僅在特定 debug 模式下輸出堆疊。
Ghost marked this conversation as resolved
@@ -0,0 +17,4 @@
const time = Date.parse(release?.created_at)
return Number.isNaN(time) ? 0 : time
}
// 先一次性計算每筆的時間戳,避免在排序比較中重複呼叫 Date.parse。

嚴重等級🟡 警告
審查員:Leo
問題:在 selectReleasesToDelete 函式中,對於 Date.parse 無法解析的日期直接視為 0,這在資料清理邏輯中是一個隱晦的行為,未來維護者可能不清楚為什麼無效日期會優先被刪除。
建議:應明確記錄無效日期的處理方式(例如記錄 warning),或在 Date.parse 失敗時,應考慮給予一個明確的邏輯(例如拋出錯誤或放到特定排序位置),以減少不可預期的副作用。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:在 `selectReleasesToDelete` 函式中,對於 `Date.parse` 無法解析的日期直接視為 `0`,這在資料清理邏輯中是一個隱晦的行為,未來維護者可能不清楚為什麼無效日期會優先被刪除。 **建議**:應明確記錄無效日期的處理方式(例如記錄 warning),或在 `Date.parse` 失敗時,應考慮給予一個明確的邏輯(例如拋出錯誤或放到特定排序位置),以減少不可預期的副作用。
Ghost marked this conversation as resolved
@@ -0,0 +20,4 @@
// 先一次性計算每筆的時間戳,避免在排序比較中重複呼叫 Date.parse。
return releases
.map((release) => ({ release, time: createdTime(release) }))
.sort((a, b) => b.time - a.time)

嚴重等級🟡 警告
審查員:Mage
問題:在 selectReleasesToDelete 的排序邏輯中,若多個成品具有完全相同的 created_at 時間戳,目前的排序行為依賴於 JavaScript 引擎對 sort() 的實作(在某些情況下可能不穩定),導致保留與刪除的成品選擇具有不確定性。
建議:建議在排序邏輯中加入次要的排序鍵值(如 idtag_name)作為比較依據(例如:若時間相同,則比較 ID 大小),以確保排序結果在時間相同時仍具有決定性。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `selectReleasesToDelete` 的排序邏輯中,若多個成品具有完全相同的 `created_at` 時間戳,目前的排序行為依賴於 JavaScript 引擎對 `sort()` 的實作(在某些情況下可能不穩定),導致保留與刪除的成品選擇具有不確定性。 **建議**:建議在排序邏輯中加入次要的排序鍵值(如 `id` 或 `tag_name`)作為比較依據(例如:若時間相同,則比較 ID 大小),以確保排序結果在時間相同時仍具有決定性。
Ghost marked this conversation as resolved
@@ -0,0 +25,4 @@
.slice(keepCount)
}
/**

嚴重等級🔵 建議
審查員:Bard
問題:config 參數型別定義方式雖然準確,但過於冗長且與實作細節耦合過深,影響程式碼的可讀性與簡潔度。
建議:建議在 app/config.js 定義並匯出型別註解(JSDoc @typedef),然後在各處直接使用該別名。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:config 參數型別定義方式雖然準確,但過於冗長且與實作細節耦合過深,影響程式碼的可讀性與簡潔度。 **建議**:建議在 `app/config.js` 定義並匯出型別註解(JSDoc @typedef),然後在各處直接使用該別名。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
const releases = await client.fetchAllPages(config.releaseApiUrl)
info(`RELEASE_COUNT=${releases.length}`)
info(`KEEP_COUNT=${config.keepCount}`)

嚴重等級🟡 警告
審查員:Bard
問題:區段標題『取得成品資訊』與後續的 section('刪除舊版本成品') 使用了不同的區段層級。第一個區段內包含了 info 輸出,而第二個區段直接開始處理邏輯,視覺節奏上稍微不一致。
建議:建議在所有主要的操作階段前統一呼叫 section(),或在細部操作前使用更明確的層級標示,保持日誌格式的旋律一致性。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:區段標題『取得成品資訊』與後續的 `section('刪除舊版本成品')` 使用了不同的區段層級。第一個區段內包含了 `info` 輸出,而第二個區段直接開始處理邏輯,視覺節奏上稍微不一致。 **建議**:建議在所有主要的操作階段前統一呼叫 `section()`,或在細部操作前使用更明確的層級標示,保持日誌格式的旋律一致性。
Ghost marked this conversation as resolved
gitea-actions bot added 1 commit 2026-06-26 03:34:27 +00:00
jiantw83 added 4 commits 2026-06-26 03:43:24 +00:00
排序加入次要鍵(id 數值,大者視為新),避免多筆 created_at 相同時保留/
刪除的選擇因引擎排序穩定性而不確定。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增測試:多筆 created_at 相同時,以 id 大小決定保留/刪除順序。測試共 60 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(ai-review): 更新 findings.json 與 exclusions.json
CI / AI Code Review (pull_request) Failing after 43s
b461f72d43
移除已修復(決定性排序)與多項重複提報的 finding;保留 7 條設計取捨;
新增 6 條不適用/誤報至 exclusions(內部 IP 黑名單會破壞自架 Gitea、
section 排版主觀、無效日期已註記、CI 堆疊輸出無妨、MAX_PAGES 為安全上限、
通用 log 抽象效益低)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

🤖 AI Code Review 團隊

👤 角色 🎯 面向 🧠 個性
🗡️ Assassin security 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用
🎼 Bard style 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版
🧰 Leo maintainability 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來
🔮 Mage logic 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」
🧪 Maya testing 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑
Rogue efficiency 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」

🔍 服務:opencode 模型:gemini-2.5-flash

## 🤖 AI Code Review 團隊 | 👤 角色 | 🎯 面向 | 🧠 個性 | |--------|--------|--------| | **🗡️ Assassin** | security | 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用 | | **🎼 Bard** | style | 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版 | | **🧰 Leo** | maintainability | 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來 | | **🔮 Mage** | logic | 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」 | | **🧪 Maya** | testing | 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑 | | **⚡ Rogue** | efficiency | 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」 | > 🔍 服務:opencode 模型:gemini-2.5-flash
gitea-actions bot reviewed 2026-06-26 03:44:06 +00:00
gitea-actions bot left a comment

AI Code Review 統計

類型 🔴 嚴重 🟡 警告 🔵 建議 無法標示
新問題 2 筆 7 筆 1 筆 0 筆
舊問題 0 筆 3 筆 0 筆 0 筆

🤖 AI 助理使用量

本次審查(opencode / gemini-2.5-flash,共 23 次呼叫)

提示 token 回應 token 合計
97,251 8,186 453,492

剩餘可用

剩餘可用:無法計算百分比(自架服務,無帳號額度概念)

## AI Code Review 統計 | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | | --- | --- | --- | --- | --- | | 新問題 | 2 筆 | 7 筆 | 1 筆 | 0 筆 | | 舊問題 | 0 筆 | 3 筆 | 0 筆 | 0 筆 | ## 🤖 AI 助理使用量 **本次審查**(opencode / gemini-2.5-flash,共 23 次呼叫) | 提示 token | 回應 token | 合計 | | --- | --- | --- | | 97,251 | 8,186 | 453,492 | **剩餘可用** 剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +31,4 @@
const keepCountRaw = env.KEEP_COUNT
const token = env.GITEA_TOKEN
info(`GITEA_SERVER_URL=${serverUrl}`)

嚴重等級🔵 建議
審查員:Maya
問題:雖然有 requireUrl 驗證,但在 loadConfig 中,對於 GITEA_SERVER_URL 的解析假設其為完整路徑。若環境變數提供的網址不含 api/v1 或路徑有變化,整合後的 releaseApiUrl 可能無效。
建議:建議在 loadConfig 中增加對 serverUrl 的處理,確保結尾沒有多餘的 /,避免拼接 API 路徑時產生類似 //api 的錯誤路徑。

**嚴重等級**:🔵 建議 **審查員**:Maya **問題**:雖然有 requireUrl 驗證,但在 loadConfig 中,對於 GITEA_SERVER_URL 的解析假設其為完整路徑。若環境變數提供的網址不含 api/v1 或路徑有變化,整合後的 releaseApiUrl 可能無效。 **建議**:建議在 loadConfig 中增加對 serverUrl 的處理,確保結尾沒有多餘的 /,避免拼接 API 路徑時產生類似 //api 的錯誤路徑。
Ghost marked this conversation as resolved
@@ -0,0 +60,4 @@
const res = await fetch(url, {
headers: this.headers,
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
})

嚴重等級🔴 嚴重
審查員:Assassin
問題:攻擊者可控制 baseUrl 參數,若未對 baseUrl 的來源進行嚴格限制,可能導致 Server-Side Request Forgery (SSRF) 攻擊。此處直接將變數拼接到 URL,如果 baseUrl 來自惡意環境變數,攻擊者能發送請求到內網資源或意圖控制的位址。
建議:應在 config.js 中對 GITEA_SERVER_URL 和 GITEA_REPOSITORY 進行嚴格的 URL 結構驗證與白名單過濾,確保拼接出的 URL 處於預期範圍內。目前雖有 requireUrl 和 requireRepository,應進一步強化確保 baseUrl 只能以預期的 GITEA_SERVER_URL 開頭。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:攻擊者可控制 baseUrl 參數,若未對 baseUrl 的來源進行嚴格限制,可能導致 Server-Side Request Forgery (SSRF) 攻擊。此處直接將變數拼接到 URL,如果 baseUrl 來自惡意環境變數,攻擊者能發送請求到內網資源或意圖控制的位址。 **建議**:應在 config.js 中對 GITEA_SERVER_URL 和 GITEA_REPOSITORY 進行嚴格的 URL 結構驗證與白名單過濾,確保拼接出的 URL 處於預期範圍內。目前雖有 requireUrl 和 requireRepository,應進一步強化確保 baseUrl 只能以預期的 GITEA_SERVER_URL 開頭。
Ghost marked this conversation as resolved
@@ -0,0 +103,4 @@
return all
}

嚴重等級🟡 警告
審查員:Assassin
問題:將 items 直接放入 all 陣列。若 API 返回異常巨大的 JSON 陣列,可能導致容器記憶體耗盡(DoS)。(註: 亦包含 Bard 關於 for...of/push 的效能建議)
建議:考慮在 fetchAllPages 中增加最大總項目數量的限制,並在超過時拋出錯誤;同時建議使用 AsyncGenerator 進行串流式處理以降低記憶體佔用。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:將 items 直接放入 all 陣列。若 API 返回異常巨大的 JSON 陣列,可能導致容器記憶體耗盡(DoS)。(註: 亦包含 Bard 關於 for...of/push 的效能建議) **建議**:考慮在 fetchAllPages 中增加最大總項目數量的限制,並在超過時拋出錯誤;同時建議使用 AsyncGenerator 進行串流式處理以降低記憶體佔用。
Ghost marked this conversation as resolved
@@ -0,0 +112,4 @@
async deleteResource(url) {
const res = await fetch(url, {
method: 'DELETE',
headers: this.headers,

嚴重等級🟡 警告
審查員:Maya
問題:deleteResource 僅檢查狀態碼,若刪除失敗(非 204),目前實作僅回傳狀態碼,呼叫方需要處理後續的邏輯。但在測試中,對於非 204 的處理顯得較為鬆散。
建議:建議在 deleteResource 內部就針對常見的錯誤狀態碼(如 401/403/404)進行特定的錯誤處理或拋出更有意義的例外,讓呼叫方能針對不同刪除失敗的原因採取對應策略(如:跳過、重試或完全中止)。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:deleteResource 僅檢查狀態碼,若刪除失敗(非 204),目前實作僅回傳狀態碼,呼叫方需要處理後續的邏輯。但在測試中,對於非 204 的處理顯得較為鬆散。 **建議**:建議在 deleteResource 內部就針對常見的錯誤狀態碼(如 401/403/404)進行特定的錯誤處理或拋出更有意義的例外,讓呼叫方能針對不同刪除失敗的原因採取對應策略(如:跳過、重試或完全中止)。
Ghost marked this conversation as resolved
@@ -0,0 +18,4 @@
const config = loadConfig()
const client = new GiteaClient({ token: config.token })
await cleanupReleases(client, config)

嚴重等級🔴 嚴重
審查員:Maya
問題:Action 的核心邏輯 cleanupReleases 與 cleanupOrphanTags 被直接呼叫,但缺少針對清理流程的整合測試或端對端測試,僅有單元測試無法保證整個「清理 -> 再清理 tag」的完整路徑是否會因環境設定或 API 回應產生非預期的行為。
建議:建議補上一個整合測試 (app/test/integration.test.js),模擬完整的 API 回應序列(如:先列出舊版本 -> 刪除舊版本 -> 重新列出 release -> 列出 tag -> 刪除孤立 tag),驗證所有 API 呼叫順序與參數皆符合預期。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:Action 的核心邏輯 cleanupReleases 與 cleanupOrphanTags 被直接呼叫,但缺少針對清理流程的整合測試或端對端測試,僅有單元測試無法保證整個「清理 -> 再清理 tag」的完整路徑是否會因環境設定或 API 回應產生非預期的行為。 **建議**:建議補上一個整合測試 (app/test/integration.test.js),模擬完整的 API 回應序列(如:先列出舊版本 -> 刪除舊版本 -> 重新列出 release -> 列出 tag -> 刪除孤立 tag),驗證所有 API 呼叫順序與參數皆符合預期。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
*/
export async function cleanupReleases(client, config) {
section('取得成品資訊')
info(`GET ${config.releaseApiUrl}`)

嚴重等級🟡 警告
審查員:Maya
問題:在 cleanupReleases 中,若 client.fetchAllPages 拋出錯誤,流程會直接中斷且 cleanupOrphanTags 將不會被執行。雖然這符合嚴格的錯誤處理,但缺乏「部分失敗」後的清理與回報機制。
建議:考慮在 cleanupReleases 中加入 try...catch,若僅為該步驟失敗,記錄錯誤後仍嘗試執行 cleanupOrphanTags 或明確標示整個 Action 處於部分清理狀態。至少應確保在清理失敗時,日誌能明確指出是哪一步驟導致中斷。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:在 cleanupReleases 中,若 client.fetchAllPages 拋出錯誤,流程會直接中斷且 cleanupOrphanTags 將不會被執行。雖然這符合嚴格的錯誤處理,但缺乏「部分失敗」後的清理與回報機制。 **建議**:考慮在 cleanupReleases 中加入 try...catch,若僅為該步驟失敗,記錄錯誤後仍嘗試執行 cleanupOrphanTags 或明確標示整個 Action 處於部分清理狀態。至少應確保在清理失敗時,日誌能明確指出是哪一步驟導致中斷。
Ghost marked this conversation as resolved
@@ -0,0 +61,4 @@
const { id, tag_name: tag, name } = release
if (isEmptyOrNull(id)) {
warn(`略過沒有 id 的成品: ${tag} (${name})`)

嚴重等級🟡 警告
審查員:Assassin
問題:雖然有 encodeURIComponent,但直接將 id 拼接至 URL 是危險操作。若 id 未經妥善驗證,攻擊者可能試圖透過特殊字元擾亂 API 路徑。
建議:建議確保 id 在進入此函數前,已驗證為預期的整數類型或符合嚴格格式的字串,不要完全依賴 encodeURIComponent 來防範所有注入可能。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:雖然有 encodeURIComponent,但直接將 id 拼接至 URL 是危險操作。若 id 未經妥善驗證,攻擊者可能試圖透過特殊字元擾亂 API 路徑。 **建議**:建議確保 id 在進入此函數前,已驗證為預期的整數類型或符合嚴格格式的字串,不要完全依賴 encodeURIComponent 來防範所有注入可能。
Ghost marked this conversation as resolved
@@ -0,0 +16,4 @@
*/
export function categorizeTags(tags, releaseTagNames) {
const keep = new Set(releaseTagNames)

嚴重等級🟡 警告
審查員:Bard
問題:函式 categorizeTags 內部的判斷邏輯稍微複雜,且直接在 map 內部處理多種條件分支。
建議:建議將邏輯拆分為更小的判斷函式,提高可讀性。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:函式 `categorizeTags` 內部的判斷邏輯稍微複雜,且直接在 map 內部處理多種條件分支。 **建議**:建議將邏輯拆分為更小的判斷函式,提高可讀性。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
section('刪除未指定 release 的 tag')
// 重新取得 release 清單,得到刪除舊版本後仍指定 tag 的成品
const currentReleases = await client.fetchAllPages(config.releaseApiUrl)

嚴重等級🟡 警告
審查員:Mage
問題:在 cleanupOrphanTags 中,呼叫了兩次 API 分別獲取 releases 和 tags。若 API 在這兩次呼叫間發生變更(例如新的 release 剛好把某個舊 tag 關聯起來),categorizeTags 的結果可能會基於不一致的狀態,造成誤刪。
建議:這是一個典型的併發/時序問題。雖然 API 本身無交易機制,但建議在取得 releases 後,若 tags 數量巨大,應考慮是否存在原子性操作的需求,或者至少在日誌中明確標示兩次獲取資料的時間間距,以便除錯。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 cleanupOrphanTags 中,呼叫了兩次 API 分別獲取 releases 和 tags。若 API 在這兩次呼叫間發生變更(例如新的 release 剛好把某個舊 tag 關聯起來),categorizeTags 的結果可能會基於不一致的狀態,造成誤刪。 **建議**:這是一個典型的併發/時序問題。雖然 API 本身無交易機制,但建議在取得 releases 後,若 tags 數量巨大,應考慮是否存在原子性操作的需求,或者至少在日誌中明確標示兩次獲取資料的時間間距,以便除錯。
Ghost marked this conversation as resolved
@@ -0,0 +59,4 @@
if (action === 'keep') {
info(`保留指定 release 的 tag: ${tag.name}`)
continue
}

嚴重等級🟡 警告
審查員:Assassin
問題:直接將 tag 名稱拼接至 URL 是常見的 Injection 破口。儘管使用了 encodeURIComponent,但若 tag.name 包含某些在 Gitea API 邏輯中具特殊意義的字元,仍可能造成非預期的資源存取或路徑穿越。
建議:除了 encodeURIComponent 外,應在 config.js 或 tags.js 中對 tag.name 進行嚴格的白名單格式驗證(例如限制為英數字、點、破折號,並禁止 .. 或 /),這比單純編碼更安全。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:直接將 tag 名稱拼接至 URL 是常見的 Injection 破口。儘管使用了 encodeURIComponent,但若 tag.name 包含某些在 Gitea API 邏輯中具特殊意義的字元,仍可能造成非預期的資源存取或路徑穿越。 **建議**:除了 encodeURIComponent 外,應在 config.js 或 tags.js 中對 tag.name 進行嚴格的白名單格式驗證(例如限制為英數字、點、破折號,並禁止 .. 或 /),這比單純編碼更安全。
Ghost marked this conversation as resolved
gitea-actions bot added 1 commit 2026-06-26 03:44:07 +00:00
jiantw83 added 3 commits 2026-06-26 05:55:57 +00:00
- loadConfig 去除 GITEA_SERVER_URL 結尾的 /,避免拼出 //api/v1 錯誤路徑
- cleanupReleases 要求 id 為正整數,非整數一律略過(縱深防禦,不僅依賴編碼)

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>
chore(ai-review): 更新 findings.json 與 exclusions.json
CI / AI Code Review (pull_request) Failing after 50s
7b742ed85e
移除本輪已修復(server URL 正規化、id 整數驗證、整合測試)與重複提報的 finding;
保留 7 條設計取捨(重試/錯誤處理/DoS/TOCTOU/deleteResource 策略);
新增 3 條不適用至 exclusions(baseUrl 由已驗證 config 組成、tag 白名單非必要、
categorizeTags 拆分無益)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

🤖 AI Code Review 團隊

👤 角色 🎯 面向 🧠 個性
🗡️ Assassin security 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用
🎼 Bard style 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版
🧰 Leo maintainability 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來
🔮 Mage logic 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」
🧪 Maya testing 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑
Rogue efficiency 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」

🔍 服務:opencode 模型:gemini-2.5-flash

## 🤖 AI Code Review 團隊 | 👤 角色 | 🎯 面向 | 🧠 個性 | |--------|--------|--------| | **🗡️ Assassin** | security | 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用 | | **🎼 Bard** | style | 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版 | | **🧰 Leo** | maintainability | 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來 | | **🔮 Mage** | logic | 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」 | | **🧪 Maya** | testing | 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑 | | **⚡ Rogue** | efficiency | 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」 | > 🔍 服務:opencode 模型:gemini-2.5-flash
gitea-actions bot reviewed 2026-06-26 05:56:45 +00:00
gitea-actions bot left a comment

AI Code Review 統計

類型 🔴 嚴重 🟡 警告 🔵 建議 無法標示
新問題 4 筆 2 筆 0 筆 0 筆
舊問題 1 筆 2 筆 0 筆 0 筆

🤖 AI 助理使用量

本次審查(opencode / gemini-2.5-flash,共 17 次呼叫)

提示 token 回應 token 合計
141,712 5,465 358,588

剩餘可用

剩餘可用:無法計算百分比(自架服務,無帳號額度概念)

## AI Code Review 統計 | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | | --- | --- | --- | --- | --- | | 新問題 | 4 筆 | 2 筆 | 0 筆 | 0 筆 | | 舊問題 | 1 筆 | 2 筆 | 0 筆 | 0 筆 | ## 🤖 AI 助理使用量 **本次審查**(opencode / gemini-2.5-flash,共 17 次呼叫) | 提示 token | 回應 token | 合計 | | --- | --- | --- | | 141,712 | 5,465 | 358,588 | **剩餘可用** 剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +30,4 @@
const rawServerUrl = env.GITEA_SERVER_URL
const serverUrl =
typeof rawServerUrl === 'string' ? rawServerUrl.replace(/\/+$/, '') : rawServerUrl
const repository = env.GITEA_REPOSITORY

嚴重等級🔴 嚴重
審查員:Mage
問題:在 loadConfig 函式中,對 GITEA_SERVER_URL 的 replace(//+$/, '') 操作假設了輸入一定是 string。雖然隨後有 requireValue 和 requireUrl 的驗證,但如果 env.GITEA_SERVER_URL 是非字串(例如數字、陣列、物件),這裡的 replace 可能會拋出 TypeError。
建議:應在 replace 之前確保其型別為 string,或使用 optional chaining 以及更嚴格的類型防護。例如: typeof rawServerUrl === 'string' ? rawServerUrl.replace(//+$/, '') : rawServerUrl 已經做了檢查,但後續的 requireValue 檢查順序應確保傳入的是處理後的字串。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:在 loadConfig 函式中,對 GITEA_SERVER_URL 的 replace(//+$/, '') 操作假設了輸入一定是 string。雖然隨後有 requireValue 和 requireUrl 的驗證,但如果 env.GITEA_SERVER_URL 是非字串(例如數字、陣列、物件),這裡的 replace 可能會拋出 TypeError。 **建議**:應在 replace 之前確保其型別為 string,或使用 optional chaining 以及更嚴格的類型防護。例如: `typeof rawServerUrl === 'string' ? rawServerUrl.replace(//+$/, '') : rawServerUrl` 已經做了檢查,但後續的 `requireValue` 檢查順序應確保傳入的是處理後的字串。
Ghost marked this conversation as resolved
@@ -0,0 +60,4 @@
releaseApiUrl: `${serverUrl}/api/v1/repos/${repository}/releases`,
tagApiUrl: `${serverUrl}/api/v1/repos/${repository}/tags`,
}
}

嚴重等級🔴 嚴重
審查員:Maya
問題:在 loadConfig 中,serverUrlrepositorytoken 以及 keepCount 雖有呼叫驗證函式,但這些驗證函式(如 requireValuerequireInteger)在失敗時會直接拋出錯誤,且沒有相對應的單元測試去驗證這些設定錯誤的情境,尤其是當環境變數輸入惡意或無效字串時的系統行為。
建議:補上針對 config.js 的完整錯誤處理測試,特別是模擬 process.env 各項缺失或異常時,確認是否確實會拋出預期的 Error,並確保清理流程在設定錯誤時能安全停止。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:在 `loadConfig` 中,`serverUrl`、`repository`、`token` 以及 `keepCount` 雖有呼叫驗證函式,但這些驗證函式(如 `requireValue`、`requireInteger`)在失敗時會直接拋出錯誤,且沒有相對應的單元測試去驗證這些設定錯誤的情境,尤其是當環境變數輸入惡意或無效字串時的系統行為。 **建議**:補上針對 `config.js` 的完整錯誤處理測試,特別是模擬 `process.env` 各項缺失或異常時,確認是否確實會拋出預期的 Error,並確保清理流程在設定錯誤時能安全停止。
Ghost marked this conversation as resolved
@@ -0,0 +112,4 @@
async deleteResource(url) {
const res = await fetch(url, {
method: 'DELETE',
headers: this.headers,

嚴重等級🟡 警告
審查員:Maya
問題:deleteResource 僅回傳狀態碼,對於 401/403/404 等錯誤未做區分處理,呼叫方策略較鬆散,且對於非 204 的處理顯得較為鬆散。
建議:在 deleteResource 內針對常見錯誤碼拋出更有意義的例外,讓呼叫方可採取跳過/重試/中止等對應策略。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:deleteResource 僅回傳狀態碼,對於 401/403/404 等錯誤未做區分處理,呼叫方策略較鬆散,且對於非 204 的處理顯得較為鬆散。 **建議**:在 deleteResource 內針對常見錯誤碼拋出更有意義的例外,讓呼叫方可採取跳過/重試/中止等對應策略。
Ghost marked this conversation as resolved
@@ -0,0 +42,4 @@
* @returns {Promise<void>}
*/
export async function cleanupReleases(client, config) {
section('取得成品資訊')

嚴重等級🔴 嚴重
審查員:Maya
問題:在 cleanupReleases 函數中,儘管對 id 進行了 isEmptyOrNull 檢查,但對於 releases 陣列中可能存在的 nullundefined 或非預期結構的 release 物件,測試覆蓋僅止於 id 為空的情況,缺少對 release 本身結構異常的測試案例。
建議:增加 cleanupReleases 的測試案例,模擬傳入包含異常結構(例如缺少 tag_name 或其他必要欄位)的 release 物件,確保在處理過程中不會拋出未捕捉的異常。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:在 `cleanupReleases` 函數中,儘管對 `id` 進行了 `isEmptyOrNull` 檢查,但對於 `releases` 陣列中可能存在的 `null`、`undefined` 或非預期結構的 `release` 物件,測試覆蓋僅止於 `id` 為空的情況,缺少對 `release` 本身結構異常的測試案例。 **建議**:增加 `cleanupReleases` 的測試案例,模擬傳入包含異常結構(例如缺少 `tag_name` 或其他必要欄位)的 `release` 物件,確保在處理過程中不會拋出未捕捉的異常。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
*/
export async function cleanupReleases(client, config) {
section('取得成品資訊')
info(`GET ${config.releaseApiUrl}`)

嚴重等級🟡 警告
審查員:Maya
問題:cleanupReleases 中若 fetchAllPages 拋錯,cleanupOrphanTags 不會執行;缺乏部分失敗後的清理與回報機制。且當 fetchAllPages 成功取得列表後,若後續刪除操作發生異常(例如網路中斷、API 回應逾時),會拋出錯誤並導致整個 cleanupReleases 流程終止,無法保證「已讀取到的所有舊 release」都被嘗試清理。
建議:考慮在 cleanupReleases 加入 try/catch,並將逐筆刪除的迴圈包覆在 try-catch 中,即使單步驟或單筆刪除失敗,也應記錄錯誤後繼續嘗試執行後續步驟或刪除下一筆,僅該步驟失敗時記錄並仍嘗試 cleanupOrphanTags,或明確標示部分清理狀態。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:cleanupReleases 中若 fetchAllPages 拋錯,cleanupOrphanTags 不會執行;缺乏部分失敗後的清理與回報機制。且當 fetchAllPages 成功取得列表後,若後續刪除操作發生異常(例如網路中斷、API 回應逾時),會拋出錯誤並導致整個 cleanupReleases 流程終止,無法保證「已讀取到的所有舊 release」都被嘗試清理。 **建議**:考慮在 cleanupReleases 加入 try/catch,並將逐筆刪除的迴圈包覆在 try-catch 中,即使單步驟或單筆刪除失敗,也應記錄錯誤後繼續嘗試執行後續步驟或刪除下一筆,僅該步驟失敗時記錄並仍嘗試 cleanupOrphanTags,或明確標示部分清理狀態。
Ghost marked this conversation as resolved
@@ -0,0 +53,4 @@
for (const { tag, action } of categorizeTags(tags, releaseTagNames)) {
if (action === 'skip') {
warn('略過沒有名稱的 tag')
continue

嚴重等級🔴 嚴重
審查員:Maya
問題:在 cleanupOrphanTags 中,雖有 categorizeTags 的邏輯測試,但對於 cleanupOrphanTags 本身與 GiteaClient 的整合互動,例如在 API 回傳異常 tag 列表時的處理,缺乏失敗路徑(如 API 請求失敗、JSON 解析失敗)的測試。
建議:補上 cleanupOrphanTags 的整合測試,模擬 client.fetchAllPages 拋出例外的情境,確認清理流程是否會優雅地處理或拋出預期的錯誤。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:在 `cleanupOrphanTags` 中,雖有 `categorizeTags` 的邏輯測試,但對於 `cleanupOrphanTags` 本身與 `GiteaClient` 的整合互動,例如在 API 回傳異常 tag 列表時的處理,缺乏失敗路徑(如 API 請求失敗、JSON 解析失敗)的測試。 **建議**:補上 `cleanupOrphanTags` 的整合測試,模擬 `client.fetchAllPages` 拋出例外的情境,確認清理流程是否會優雅地處理或拋出預期的錯誤。
Ghost marked this conversation as resolved
gitea-actions bot added 1 commit 2026-06-26 05:56:46 +00:00
jiantw83 added 3 commits 2026-06-26 05:59:52 +00:00
在刪除迴圈中先檢查項目為非 null 物件,避免對非預期結構解構時拋出未捕捉例外。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增 cleanupReleases 處理 null/缺欄位 release 不拋例外的測試,以及
cleanupOrphanTags 在 fetchAllPages 拋錯時向外拋出的測試。測試共 64 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(ai-review): 更新 findings.json 與 exclusions.json
CI / AI Code Review (pull_request) Failing after 55s
9e598143bf
移除本輪已修復(防禦異常結構)與重複提報的 finding;保留 4 條錯誤處理類設計取捨;
將 config.js replace 型別 finding 登記為不成立(已有 typeof 防護)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

🤖 AI Code Review 團隊

👤 角色 🎯 面向 🧠 個性
🗡️ Assassin security 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用
🎼 Bard style 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版
🧰 Leo maintainability 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來
🔮 Mage logic 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」
🧪 Maya testing 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑
Rogue efficiency 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」

🔍 服務:opencode 模型:gemini-2.5-flash

## 🤖 AI Code Review 團隊 | 👤 角色 | 🎯 面向 | 🧠 個性 | |--------|--------|--------| | **🗡️ Assassin** | security | 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用 | | **🎼 Bard** | style | 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版 | | **🧰 Leo** | maintainability | 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來 | | **🔮 Mage** | logic | 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」 | | **🧪 Maya** | testing | 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑 | | **⚡ Rogue** | efficiency | 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」 | > 🔍 服務:opencode 模型:gemini-2.5-flash
gitea-actions bot reviewed 2026-06-26 06:00:47 +00:00
gitea-actions bot left a comment

AI Code Review 統計

類型 🔴 嚴重 🟡 警告 🔵 建議 無法標示
新問題 1 筆 2 筆 0 筆 0 筆
舊問題 1 筆 3 筆 0 筆 0 筆

🤖 AI 助理使用量

本次審查(opencode / gemini-2.5-flash,共 15 次呼叫)

提示 token 回應 token 合計
148,695 5,011 337,751

剩餘可用

剩餘可用:無法計算百分比(自架服務,無帳號額度概念)

## AI Code Review 統計 | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | | --- | --- | --- | --- | --- | | 新問題 | 1 筆 | 2 筆 | 0 筆 | 0 筆 | | 舊問題 | 1 筆 | 3 筆 | 0 筆 | 0 筆 | ## 🤖 AI 助理使用量 **本次審查**(opencode / gemini-2.5-flash,共 15 次呼叫) | 提示 token | 回應 token | 合計 | | --- | --- | --- | | 148,695 | 5,011 | 337,751 | **剩餘可用** 剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +44,4 @@
export async function cleanupReleases(client, config) {
section('取得成品資訊')
info(`GET ${config.releaseApiUrl}`)

嚴重等級🟡 警告
審查員:Mage
問題:在 cleanupReleases 迴圈中,對於每一個 release 都呼叫 await client.deleteResource(url)。若 release 數量眾多,這種依序刪除的方式非常耗時,且若 API 負載過高,容易導致部分請求逾時。
建議:如果 API 允許,建議採用併發刪除(例如使用 Promise.all 限制併發數),或者在日誌中增加處理進度,並考慮增加對 API 錯誤的重試機制。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `cleanupReleases` 迴圈中,對於每一個 release 都呼叫 `await client.deleteResource(url)`。若 release 數量眾多,這種依序刪除的方式非常耗時,且若 API 負載過高,容易導致部分請求逾時。 **建議**:如果 API 允許,建議採用併發刪除(例如使用 `Promise.all` 限制併發數),或者在日誌中增加處理進度,並考慮增加對 API 錯誤的重試機制。
@@ -0,0 +49,4 @@
info(`GET ${config.tagApiUrl}`)
const tags = await client.fetchAllPages(config.tagApiUrl)
info(`TAG_COUNT=${tags.length}`)

嚴重等級🔴 嚴重
審查員:Mage
問題:在 cleanupOrphanTags 中,分兩次 API 呼叫取得 releases 與 tags,且過濾過程未保證原子性。若期間 Gitea 狀態變更,可能導致誤刪並非孤立的 tag。
建議:考量原子性需求,或在刪除操作前增加確認機制;並明確文件化此風險。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:在 `cleanupOrphanTags` 中,分兩次 API 呼叫取得 releases 與 tags,且過濾過程未保證原子性。若期間 Gitea 狀態變更,可能導致誤刪並非孤立的 tag。 **建議**:考量原子性需求,或在刪除操作前增加確認機制;並明確文件化此風險。
@@ -0,0 +51,4 @@
info(`TAG_COUNT=${tags.length}`)
for (const { tag, action } of categorizeTags(tags, releaseTagNames)) {
if (action === 'skip') {

嚴重等級🟡 警告
審查員:Maya
問題:在刪除 tag 的迴圈中,同樣缺少對 deleteResource 拋出例外的防禦,一旦發生網路錯誤,後續的 tag 將無法被清理。
建議:在 for 迴圈內增加 try...catch 區塊,記錄錯誤並 continue 以確保後續 tag 能正常被刪除;應比照 releases.js 新增測試驗證此行為。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:在刪除 tag 的迴圈中,同樣缺少對 `deleteResource` 拋出例外的防禦,一旦發生網路錯誤,後續的 tag 將無法被清理。 **建議**:在 `for` 迴圈內增加 `try...catch` 區塊,記錄錯誤並 `continue` 以確保後續 tag 能正常被刪除;應比照 `releases.js` 新增測試驗證此行為。
gitea-actions bot added 1 commit 2026-06-26 06:00:49 +00:00
admin closed this pull request 2026-06-26 06:03:16 +00:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: old-docker-actions/release-cleanup#5