JefferyandClaude Opus 4.8 7c225d4122 test(release-cleanup): 補齊 cleanup/config/main 與新驗證函式測試
新增 cleanupReleases、cleanupOrphanTags 的失敗路徑與分類測試、
loadConfig 環境變數驗證測試、main 整合測試,以及 requireUrl/
requireRepository 與 content-type 檢查測試。測試共 41 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:55:45 +08:00
2026-06-25 13:47:57 +00:00

release-cleanup

release-cleanup 是一個 Gitea Docker Action,用於自動清理儲存庫中的成品(release)與標籤(tag):刪除超出保留數量的舊版本 release,並移除未被任何 release 指定的孤立 tag。原本以 bash 實作,現已改寫為模組化的 Node.js 程式,進入點仍為 entrypoint.sh

更新時間:2026/06/26 10:40:15

專案列表

專案描述

專案名稱 專案描述
release-cleanup Gitea Docker Action 的 Node.js 實作。提供主控台輸出、參數驗證、設定載入、Gitea API 客戶端,以及清理舊版本 release 與孤立 tag 的功能。

參考專案

專案名稱 參考專案列表
release-cleanup

相依套件(npm)

專案名稱 套件列表
release-cleanup 無(僅使用 Node.js 內建模組與 node:test)

功能列表

release-cleanup

功能名稱 功能描述
separator 輸出一條等號分隔線至 stdout。
section 輸出區段標題(分隔線 + 標題 + 虛線)。
info [INFO] 前綴輸出一般資訊。
success [OK] 前綴輸出成功訊息。
warn [WARN] 前綴輸出警告訊息。
fail [ERR] 前綴輸出錯誤訊息至 stderr。
isEmptyOrNull 判斷值是否為空(空字串、null、undefined 或字串 "null")。
requireValue 要求欄位有值,空值時丟出 Error。
requireInteger 要求欄位為非負整數,否則丟出 Error。
loadConfig 從環境變數載入並驗證設定,回傳設定物件。
GiteaClient 建立帶認證標頭的 Gitea API 客戶端。
GiteaClient.fetchAllPages 逐頁讀取分頁式清單 API 並合併成單一陣列。
GiteaClient.deleteResource 對指定資源發出 DELETE 請求並回傳狀態碼。
selectReleasesToDelete 依時間排序後回傳超出保留數量的舊 release。
cleanupReleases 讀取並刪除超出保留數量的舊版本 release。
categorizeTags 將 tag 分類為保留、刪除或略過。
cleanupOrphanTags 刪除未被任何 release 指定的孤立 tag。

使用範例

separator

輸出一條前後換行的等號分隔線至 stdout,用於視覺上區隔不同階段的輸出。

import { separator } from './logger.js'

separator()
// 輸出:
//
// ==================================================

section

先印分隔線,再印標題與一條虛線,用於標示流程進入新階段。

import { section } from './logger.js'

section('參數檢查')
// ==================================================
// 參數檢查
// --------------------------------------------------

info

[INFO] 前綴輸出一般資訊訊息至 stdout。

import { info } from './logger.js'

info('GET https://gitea.example.com/api/v1/repos/owner/repo/releases')
// [INFO] GET https://gitea.example.com/api/v1/repos/owner/repo/releases

success

[OK] 前綴輸出成功訊息(前綴補空白以對齊其他標籤)。

import { success } from './logger.js'

success('成功刪除: v1.0.0 (Release 1.0.0)')
// [OK]   成功刪除: v1.0.0 (Release 1.0.0)

warn

[WARN] 前綴輸出警告訊息;為與一般輸出同流,仍寫入 stdout。

import { warn } from './logger.js'

warn('GITEA_TOKEN is empty; release API calls will be anonymous')
// [WARN] GITEA_TOKEN is empty; release API calls will be anonymous

fail

[ERR] 前綴輸出錯誤訊息至 stderr(唯一寫入 stderr 的輸出函式)。

import { fail } from './logger.js'

fail('刪除失敗: v0.9.0 (舊版), HTTP 500')
// (stderr) [ERR]  刪除失敗: v0.9.0 (舊版), HTTP 500

isEmptyOrNull

判斷值是否視為「空」。使用嚴格相等,因此 0false、字串 "0" 都不算空;undefinednull、空字串與字串 "null" 才回傳 true

import { isEmptyOrNull } from './validate.js'

isEmptyOrNull('')      // true
isEmptyOrNull('null')  // true
isEmptyOrNull(0)       // false
isEmptyOrNull('v1.0')  // false

requireValue

要求指定欄位有值,空值時丟出 Error 以中止流程。空值判定委派給 isEmptyOrNull

import { requireValue } from './validate.js'

requireValue('GITEA_SERVER_URL', 'https://gitea.example.com') // 通過
requireValue('GITEA_SERVER_URL', '')                          // 丟出 Error: GITEA_SERVER_URL is required

requireInteger

要求欄位為非負整數(可為數字或純數字字串),拒絕負數、小數、空值與非數字內容。

import { requireInteger } from './validate.js'

requireInteger('KEEP_COUNT', '2')   // 通過
requireInteger('KEEP_COUNT', 0)     // 通過
requireInteger('KEEP_COUNT', '-1')  // 丟出 Error: KEEP_COUNT must be a non-negative integer

loadConfig

從環境變數載入並驗證設定,回傳供清理流程使用的設定物件;必填項缺漏或 KEEP_COUNT 非整數時丟出 Error。

import { loadConfig } from './config.js'

// 由 process.env 讀取(GITEA_SERVER_URL / GITEA_REPOSITORY / KEEP_COUNT / GITEA_TOKEN)
const config = loadConfig()

// 測試時可注入假環境變數
const cfg = loadConfig({
  GITEA_SERVER_URL: 'https://gitea.example.com',
  GITEA_REPOSITORY: 'owner/repo',
  KEEP_COUNT: '2',
})
// cfg.releaseApiUrl === 'https://gitea.example.com/api/v1/repos/owner/repo/releases'

GiteaClient

建立與 Gitea API 溝通的客戶端;傳入 token 時於後續請求帶上 Authorization 標頭,否則以匿名方式呼叫。

import { GiteaClient } from './gitea-client.js'

const client = new GiteaClient({ token: process.env.GITEA_TOKEN })
const anonymous = new GiteaClient() // 無 token,匿名呼叫

GiteaClient.fetchAllPages

逐頁讀取分頁式清單 API(每頁以 ?page=N 遞增),直到回傳空陣列為止,合併成單一陣列;任一頁 HTTP 非 2xx 時丟出 Error。

const client = new GiteaClient({ token: process.env.GITEA_TOKEN })
const releases = await client.fetchAllPages(
  'https://gitea.example.com/api/v1/repos/owner/repo/releases',
)
console.log(`共取得 ${releases.length} 筆 release`)

GiteaClient.deleteResource

對指定資源發出 DELETE 請求,回傳 HTTP 狀態碼供呼叫端判斷成敗(成功刪除通常為 204)。

const code = await client.deleteResource(
  'https://gitea.example.com/api/v1/repos/owner/repo/releases/123',
)
if (code === 204) {
  console.log('刪除成功')
}

selectReleasesToDelete

created_at 由新到舊排序,保留最新的 keepCount 筆,回傳其餘(較舊)待刪除的 release;為純函式,不改動原陣列。

import { selectReleasesToDelete } from './releases.js'

const releases = [
  { id: 1, tag_name: 'v1.0.0', created_at: '2024-01-01T00:00:00Z' },
  { id: 2, tag_name: 'v2.0.0', created_at: '2024-02-01T00:00:00Z' },
  { id: 3, tag_name: 'v3.0.0', created_at: '2024-03-01T00:00:00Z' },
]
selectReleasesToDelete(releases, 2)
// [{ id: 1, tag_name: 'v1.0.0', ... }]  // 僅保留最新兩筆,回傳最舊的一筆

cleanupReleases

讀取全部 release,刪除超出保留數量的舊版本;總數不超過 keepCount 時不做任何刪除。

import { loadConfig } from './config.js'
import { GiteaClient } from './gitea-client.js'
import { cleanupReleases } from './releases.js'

const config = loadConfig()
const client = new GiteaClient({ token: config.token })
await cleanupReleases(client, config)

categorizeTags

將每個 tag 分類為 keep(仍被 release 指定)、delete(孤立 tag)或 skip(無名稱);為純函式,方便測試。

import { categorizeTags } from './tags.js'

const tags = [{ name: 'v2.0.0' }, { name: 'v1.0.0' }, { name: '' }]
categorizeTags(tags, ['v2.0.0'])
// [
//   { tag: { name: 'v2.0.0' }, action: 'keep' },
//   { tag: { name: 'v1.0.0' }, action: 'delete' },
//   { tag: { name: '' },       action: 'skip' },
// ]

cleanupOrphanTags

重新讀取 release 清單取得仍被指定的 tag,再刪除未被任何 release 指定的孤立 tag。

import { loadConfig } from './config.js'
import { GiteaClient } from './gitea-client.js'
import { cleanupOrphanTags } from './tags.js'

const config = loadConfig()
const client = new GiteaClient({ token: config.token })
await cleanupOrphanTags(client, config)
2026-07-01 01:36:02 +00:00
Languages
JavaScript 66.8%
Dockerfile 18.1%
Shell 15.1%