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

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

變更摘要

將 release-cleanup Action 由單一 entrypoint.sh(bash)改寫為模組化的 Node.js 程式,並經多輪 AI code review 強化健壯性、安全性與測試覆蓋。對外行為維持不變:清理超出保留數量的舊 release,並刪除未被任何 release 指定的孤立 tag。

影響範圍

  • 執行環境:容器基底由 alpine+bash/curl/jq 改為 node:20-alpine;HTTP 改用 Node 內建 fetch
  • 進入點:entrypoint.sh 保留,僅負責 exec node /app/index.js
  • 結構:Node.js 程式集中於 app/,測試於 app/test/(64 項,node --test 全數通過)。

重點模組

模組 職責
app/logger.js 主控台輸出與錯誤輸出(failError)
app/validate.js 參數驗證(值/整數/URL/repository)
app/config.js 載入並驗證環境變數、組出設定(含 server URL 去尾斜線)
app/gitea-client.js Gitea API 客戶端(逾時、content-type 檢查、JSON 防呆、MAX_PAGES、sanitize)
app/releases.js 清理舊 release(決定性排序、id 整數驗證、異常結構防禦)
app/tags.js 清理孤立 tag(分類、URL 編碼、TOCTOU 已文件化)
app/index.js 進入點主流程

已強化(經 AI review 多輪)

輸入驗證(URL/repo 格式、SSRF 降風險)、所有請求 30 秒逾時、分頁 MAX_PAGES 上限、回應 content-type 與 JSON 解析防呆、刪除 URL 編碼與 id 整數驗證、錯誤訊息控制字元清理、決定性排序、端對端整合測試。

已知保留事項(設計取捨,見 .gitea/ai-review/findings.json)

DELETE 重試/錯誤閾值、逐筆刪除吞例外續行、刪除併發化、deleteResource 依狀態碼分流、release/tag 兩次讀取的 TOCTOU(已於程式碼註解文件化)。皆為需求面決策,已逐條記錄保留原因。

風險

容器基底改為 Node.js,行為已盡量等價;建議於實際 repo 驗證一次清理流程。

## 變更摘要 將 release-cleanup Action 由單一 `entrypoint.sh`(bash)改寫為模組化的 Node.js 程式,並經多輪 AI code review 強化健壯性、安全性與測試覆蓋。**對外行為維持不變**:清理超出保留數量的舊 release,並刪除未被任何 release 指定的孤立 tag。 ## 影響範圍 - **執行環境**:容器基底由 `alpine`+`bash/curl/jq` 改為 `node:20-alpine`;HTTP 改用 Node 內建 `fetch`。 - **進入點**:`entrypoint.sh` 保留,僅負責 `exec node /app/index.js`。 - **結構**:Node.js 程式集中於 `app/`,測試於 `app/test/`(64 項,`node --test` 全數通過)。 ## 重點模組 | 模組 | 職責 | | --- | --- | | `app/logger.js` | 主控台輸出與錯誤輸出(failError) | | `app/validate.js` | 參數驗證(值/整數/URL/repository) | | `app/config.js` | 載入並驗證環境變數、組出設定(含 server URL 去尾斜線) | | `app/gitea-client.js` | Gitea API 客戶端(逾時、content-type 檢查、JSON 防呆、MAX_PAGES、sanitize) | | `app/releases.js` | 清理舊 release(決定性排序、id 整數驗證、異常結構防禦) | | `app/tags.js` | 清理孤立 tag(分類、URL 編碼、TOCTOU 已文件化) | | `app/index.js` | 進入點主流程 | ## 已強化(經 AI review 多輪) 輸入驗證(URL/repo 格式、SSRF 降風險)、所有請求 30 秒逾時、分頁 MAX_PAGES 上限、回應 content-type 與 JSON 解析防呆、刪除 URL 編碼與 id 整數驗證、錯誤訊息控制字元清理、決定性排序、端對端整合測試。 ## 已知保留事項(設計取捨,見 .gitea/ai-review/findings.json) DELETE 重試/錯誤閾值、逐筆刪除吞例外續行、刪除併發化、deleteResource 依狀態碼分流、release/tag 兩次讀取的 TOCTOU(已於程式碼註解文件化)。皆為需求面決策,已逐條記錄保留原因。 ## 風險 容器基底改為 Node.js,行為已盡量等價;建議於實際 repo 驗證一次清理流程。
jiantw83 added 45 commits 2026-06-26 06:05:53 +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>
為 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>
- 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>
- 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>
排序前先一次性計算各 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>
新增 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>
排序加入次要鍵(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>
- 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>
在刪除迴圈中先檢查項目為非 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>
明確文件化 release 與 tag 兩次獨立 API 呼叫間的時序競態(已知限制),
說明現行緩解與為何屬排程任務可接受範圍。僅註解變更。

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 25s
8606fd94c3
移除已具測試覆蓋的 config 驗證 finding;保留 6 條錯誤處理/併發/TOCTOU 設計取捨,
其中 TOCTOU 已補上程式碼註解文件化。

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:06:16 +00:00
gitea-actions bot left a comment

AI Code Review 統計

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

🤖 AI 助理使用量

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

提示 token 回應 token 合計
165,789 4,122 310,136

剩餘可用

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

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

嚴重等級🔴 嚴重
審查員:Maya
問題:清理舊版本成品時,若刪除過程拋出例外或 API 回傳異常(除 204 外),未作完善處理,無法保證一致性且未針對『部分失敗』設計重試或完整性檢查機制。(合併:包含 app/releases.js:46 例外中止問題)
建議:將逐筆刪除包在 try/catch,加入錯誤計數;若失敗率過高或發生特定非預期錯誤(如 403),應明確拋出例外讓流程終止。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:清理舊版本成品時,若刪除過程拋出例外或 API 回傳異常(除 204 外),未作完善處理,無法保證一致性且未針對『部分失敗』設計重試或完整性檢查機制。(合併:包含 app/releases.js:46 例外中止問題) **建議**:將逐筆刪除包在 try/catch,加入錯誤計數;若失敗率過高或發生特定非預期錯誤(如 403),應明確拋出例外讓流程終止。
Ghost marked this conversation as resolved
@@ -0,0 +22,4 @@
return { tag, action: 'skip' }
}
if (keep.has(tag.name)) {
return { tag, action: 'keep' }

嚴重等級🟡 警告
審查員:Mage
問題:categorizeTags 函式中直接呼叫 keep.has(tag.name),若 API 回傳的 tag 物件缺少 name 欄位或 name 為非字串,可能導致非預期行為。
建議:增加明確的型別檢查,確保 tag.name 為 string 後再進行比對。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:categorizeTags 函式中直接呼叫 keep.has(tag.name),若 API 回傳的 tag 物件缺少 name 欄位或 name 為非字串,可能導致非預期行為。 **建議**:增加明確的型別檢查,確保 tag.name 為 string 後再進行比對。
Ghost marked this conversation as resolved
@@ -0,0 +49,4 @@
const currentReleases = await client.fetchAllPages(config.releaseApiUrl)
const releaseTagNames = currentReleases.map((release) => release.tag_name)
info(`GET ${config.tagApiUrl}`)

嚴重等級🟡 警告
審查員:Mage
問題:cleanupOrphanTags 分兩次 API 呼叫取得 releases 與 tags,過濾過程未保證原子性;期間 Gitea 狀態變更可能誤刪非孤立 tag。(合併:包含 app/tags.js:47, 45 之類似 TOCTOU 風險指控)
建議:考量原子性需求,或在刪除前增加確認機制;並明確文件化此風險。建議增加最終防護機制,或在測試中模擬競爭條件。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:cleanupOrphanTags 分兩次 API 呼叫取得 releases 與 tags,過濾過程未保證原子性;期間 Gitea 狀態變更可能誤刪非孤立 tag。(合併:包含 app/tags.js:47, 45 之類似 TOCTOU 風險指控) **建議**:考量原子性需求,或在刪除前增加確認機制;並明確文件化此風險。建議增加最終防護機制,或在測試中模擬競爭條件。
Ghost marked this conversation as resolved
gitea-actions bot added 1 commit 2026-06-26 06:06:19 +00:00
jiantw83 added 4 commits 2026-06-26 06:09:43 +00:00
對 null/非物件 tag 與非字串 name 一律歸為 skip,避免存取 tag.name 或比對時出錯。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增測試:含 null、name 為數字、缺 name 的項目皆歸為 skip,正常 name 維持 keep。測試共 65 項全數通過。

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
CI / AI Code Review (pull_request) Failing after 48s
7d41ca50b4
移除已修復(categorizeTags 型別防禦)的 finding;保留 6 條錯誤處理/併發/TOCTOU 設計取捨。

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:10:30 +00:00
gitea-actions bot left a comment

AI Code Review 統計

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

🤖 AI 助理使用量

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

提示 token 回應 token 合計
151,505 4,179 415,808

剩餘可用

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

## AI Code Review 統計 | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | | --- | --- | --- | --- | --- | | 新問題 | 2 筆 | 7 筆 | 2 筆 | 0 筆 | | 舊問題 | 0 筆 | 0 筆 | 0 筆 | 0 筆 | ## 🤖 AI 助理使用量 **本次審查**(opencode / gemini-2.5-flash,共 19 次呼叫) | 提示 token | 回應 token | 合計 | | --- | --- | --- | | 151,505 | 4,179 | 415,808 | **剩餘可用** 剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +112,4 @@
async deleteResource(url) {
const res = await fetch(url, {
method: 'DELETE',
headers: this.headers,

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

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:deleteResource 僅回傳狀態碼,對 401/403/404 等錯誤未做區分處理。 **建議**:在 deleteResource 內針對常見錯誤碼拋出更有意義的例外,讓呼叫方採取跳過/重試/中止策略。
Ghost marked this conversation as resolved
app/index.js Outdated
@@ -0,0 +27,4 @@
// 僅在直接以 `node index.js` 執行時啟動主流程;被測試 import 時不自動執行,方便撰寫整合測試。
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
failError(error)

嚴重等級🔵 建議
審查員:Bard
問題:手動拼接 import.meta.url 字串來判斷執行入口,寫法原始且脆弱。
建議:引用 node:url 中的 pathToFileURL,使用 import.meta.url === pathToFileURL(process.argv[1]).href 進行比較。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:手動拼接 import.meta.url 字串來判斷執行入口,寫法原始且脆弱。 **建議**:引用 node:url 中的 pathToFileURL,使用 import.meta.url === pathToFileURL(process.argv[1]).href 進行比較。
Ghost marked this conversation as resolved
app/releases.js Outdated
@@ -0,0 +18,4 @@
return Number.isNaN(time) ? 0 : time
}
// 先一次性計算每筆的時間戳,避免在排序比較中重複呼叫 Date.parse。
// 時間相同時以 id(數值,大者為新)作為次要鍵,確保排序結果具決定性。

嚴重等級🟡 警告
審查員:Mage
問題:在 selectReleasesToDelete 函式中,若 releases 陣列為空,雖不拋錯但邏輯上應考慮空值邊界。且雖然處理了 Date.parse 的 NaN,但對空物件或缺失 created_at 屬性的項目的處理依賴屬性存取鏈,可能在特定結構下產生非預期行為。
建議:建議增加對釋出項目結構的預防性檢查,確保 created_at 存在且有效。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 selectReleasesToDelete 函式中,若 releases 陣列為空,雖不拋錯但邏輯上應考慮空值邊界。且雖然處理了 Date.parse 的 NaN,但對空物件或缺失 created_at 屬性的項目的處理依賴屬性存取鏈,可能在特定結構下產生非預期行為。 **建議**:建議增加對釋出項目結構的預防性檢查,確保 created_at 存在且有效。
Ghost marked this conversation as resolved
app/releases.js Outdated
@@ -0,0 +35,4 @@
*
* 流程:取得所有 release 若總數不超過 `keepCount` 則直接結束(無需清理)
* 否則以 [[selectReleasesToDelete]] 取出待刪除清單,逐筆刪除(略過沒有 `id` 的項目),
* 依回應狀態碼 204 判定成功與否並輸出結果

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

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:清理流程對網路請求依賴強,若 API 呼叫失敗,整個 main 流程中斷,無法確保後續清理的一致性與部分成功重試。 **建議**:引入更細緻的錯誤處理(如錯誤閾值機制)或部分清理成功後的重試策略。
Ghost marked this conversation as resolved
@@ -0,0 +43,4 @@
*/
export async function cleanupReleases(client, config) {
section('取得成品資訊')
info(`GET ${config.releaseApiUrl}`)

嚴重等級🔴 嚴重
審查員:Maya
問題:清理舊版本成品時,若刪除過程拋出例外或 API 回傳異常(除 204 外),未作完善處理,導致無法保證一致性且未針對「部分失敗」設計重試或完整性檢查機制。
建議:將逐筆刪除包在 try/catch,加入錯誤計數;若失敗率過高或發生特定非預期錯誤(如 403),應明確拋出例外讓流程終止。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:清理舊版本成品時,若刪除過程拋出例外或 API 回傳異常(除 204 外),未作完善處理,導致無法保證一致性且未針對「部分失敗」設計重試或完整性檢查機制。 **建議**:將逐筆刪除包在 try/catch,加入錯誤計數;若失敗率過高或發生特定非預期錯誤(如 403),應明確拋出例外讓流程終止。
Ghost marked this conversation as resolved
@@ -0,0 +44,4 @@
export async function cleanupReleases(client, config) {
section('取得成品資訊')
info(`GET ${config.releaseApiUrl}`)

嚴重等級🟡 警告
審查員:Maya
問題:cleanupReleases 迴圈逐筆 await deleteResource,release 眾多時依序刪除耗時,且 API 負載高時易逾時。
建議:若 API 允許,採有上限的併發刪除(如限流 Promise.all),或增加進度日誌與重試。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:cleanupReleases 迴圈逐筆 await deleteResource,release 眾多時依序刪除耗時,且 API 負載高時易逾時。 **建議**:若 API 允許,採有上限的併發刪除(如限流 Promise.all),或增加進度日誌與重試。
Ghost marked this conversation as resolved
app/releases.js Outdated
@@ -0,0 +74,4 @@
// id 已驗證為正整數;仍對其編碼作為縱深防禦。
const url = `${config.releaseApiUrl}/${encodeURIComponent(id)}`
info(`DELETE ${tag} (${name})`)

嚴重等級🔴 嚴重
審查員:Assassin
問題:儘管使用了 encodeURIComponent(id),若後續處理中 id 被錯誤地解碼或拼接,仍存在目錄穿越風險(API 路徑穿越)。
建議:除了 encodeURIComponent,需確保 cleanupReleases 迴圈中對 id 為正整數的驗證邏輯不能被繞過,且移除 id 上的任何類型轉換風險。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:儘管使用了 encodeURIComponent(id),若後續處理中 id 被錯誤地解碼或拼接,仍存在目錄穿越風險(API 路徑穿越)。 **建議**:除了 encodeURIComponent,需確保 cleanupReleases 迴圈中對 id 為正整數的驗證邏輯不能被繞過,且移除 id 上的任何類型轉換風險。
Ghost marked this conversation as resolved
@@ -0,0 +49,4 @@
// 若兩次呼叫之間有新 release 關聯到某 tag,該 tag 仍可能基於過時快照被誤判為孤立。
// 此為排程清理任務可接受的殘餘競態(下次執行會自我修正),完整原子性留待人工評估。
const currentReleases = await client.fetchAllPages(config.releaseApiUrl)
const releaseTagNames = currentReleases.map((release) => release.tag_name)

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

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:cleanupOrphanTags 分兩次 API 呼叫取得 releases 與 tags,過濾過程未保證原子性,期間 Gitea 狀態變更可能誤刪非孤立 tag。 **建議**:考量原子性需求,或在刪除前增加確認機制,並明確文件化此風險。
Ghost marked this conversation as resolved
@@ -0,0 +51,4 @@
const currentReleases = await client.fetchAllPages(config.releaseApiUrl)
const releaseTagNames = currentReleases.map((release) => release.tag_name)
info(`GET ${config.tagApiUrl}`)

嚴重等級🟡 警告
審查員:Maya
問題:刪除 tag 的迴圈缺少對 deleteResource 拋例外的防禦,網路錯誤會中止後續 tag 清理。
建議:在 for 迴圈內加 try/catch,記錄錯誤並 continue;比照 releases 補測試。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:刪除 tag 的迴圈缺少對 deleteResource 拋例外的防禦,網路錯誤會中止後續 tag 清理。 **建議**:在 for 迴圈內加 try/catch,記錄錯誤並 continue;比照 releases 補測試。
Ghost marked this conversation as resolved
@@ -0,0 +147,4 @@
globalThis.fetch = async () => {
page += 1
// 前 999 頁有資料,第 1000 頁(MAX_PAGES)回空陣列,應正常結束
return jsonResponse(page < 1000 ? [{ id: page }] : [])

嚴重等級🟡 警告
審查員:Maya
問題:fetchAllPages 目前缺乏測試當 fetch 自身發生網路錯誤時的情境。
建議:補上一個測試案例,模擬 fetch 拋出非 AbortError 的一般網路錯誤,以驗證客戶端能妥善捕捉並拋出具備 URL 上下文的錯誤。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:fetchAllPages 目前缺乏測試當 fetch 自身發生網路錯誤時的情境。 **建議**:補上一個測試案例,模擬 fetch 拋出非 AbortError 的一般網路錯誤,以驗證客戶端能妥善捕捉並拋出具備 URL 上下文的錯誤。
Ghost marked this conversation as resolved
@@ -0,0 +27,4 @@
)
})
test('selectReleasesToDelete 在 created_at 相同時以 id 決定順序(具決定性)', () => {

嚴重等級🔵 建議
審查員:Maya
問題:selectReleasesToDelete 尚未明確測試傳入空陣列 [] 時的行為。
建議:增加測試案例:assert.deepEqual(selectReleasesToDelete([], 1), []),驗證穩健性。

**嚴重等級**:🔵 建議 **審查員**:Maya **問題**:selectReleasesToDelete 尚未明確測試傳入空陣列 [] 時的行為。 **建議**:增加測試案例:assert.deepEqual(selectReleasesToDelete([], 1), []),驗證穩健性。
Ghost marked this conversation as resolved
gitea-actions bot added 1 commit 2026-06-26 06:10:32 +00:00
jiantw83 added 3 commits 2026-06-26 06:14:20 +00:00
改用 node:url 的 pathToFileURL(process.argv[1]).href 比對,正確處理含空白/
特殊字元路徑與跨平台差異,取代脆弱的手動 file:// 字串拼接。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
新增 fetchAllPages 在 fetch 拋一般網路錯誤時向外拋出的測試,以及
selectReleasesToDelete 對空陣列回傳空陣列的測試。測試共 67 項全數通過。

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 51m54s
1f8f1a6939
移除本輪已修復(pathToFileURL、網路錯誤/空陣列測試)與已具防護的 finding
(id 已驗證正整數);保留 6 條錯誤處理/併發/TOCTOU 設計取捨。

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
jiantw83 added 6 commits 2026-06-26 07:28:54 +00:00
新增 deleteWithRetry(暫時性錯誤指數退避重試、永久性錯誤即止)、
runWithConcurrency(有上限併發)與 isRetryableStatus,供清理流程強化錯誤處理。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cleanupReleases/cleanupOrphanTags 改用 delete-utils:暫時性錯誤(429/5xx、
網路例外)重試、永久性錯誤(401/403/404)即止、單筆失敗只記錄不中斷並彙報失敗數,
並以有上限併發加速。解決 AI review 的重試/錯誤分流/併發/部分失敗續行系列建議。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
涵蓋 isRetryableStatus、deleteWithRetry(成功/暫時性重試/永久性即止/網路例外用盡)
與 runWithConcurrency(順序、併發上限、空陣列)。測試共 75 項全數通過。

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
將 opencode-code-review 的輸入由 comment_token(secrets.COMMENT_TOKEN)
改為 token(secrets.TOKEN)。註:此變更非由本次重構產生,一併納入提交。

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 29m46s
3ea2b29ab8
實作重試/併發/best-effort/狀態分流後移除對應 finding;僅保留 1 條 TOCTOU
(Gitea 無交易機制無法應用層原子化,已文件化並有重抓緩解)。

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
admin closed this pull request 2026-06-30 06:02:32 +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#6