refactor(release-cleanup): 將清理邏輯由 bash 改寫為 Node.js 模組
依功能分組為 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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b478872c5f
commit
b1aa8730a2
+16
-4
@@ -1,10 +1,22 @@
|
||||
FROM alpine:latest
|
||||
# =============================================================================
|
||||
# 用途: 建置 release-cleanup Action 的容器映像。
|
||||
# 以 Node.js 執行環境打包 /app 下的 Node.js 程式 (專案已從 bash 改寫為 Node.js),
|
||||
# 並透過 entrypoint.sh 啟動。
|
||||
# 更新日期: 2026/06/26 10:28:36
|
||||
# =============================================================================
|
||||
|
||||
# 安裝必要的工具
|
||||
RUN apk add --no-cache --no-check-certificate bash curl jq
|
||||
# 基底映像:Node.js 20 的 Alpine 版本 (體積小)。
|
||||
FROM node:20-alpine
|
||||
|
||||
# 複製 Node.js 應用程式(不含測試)
|
||||
# 先單獨複製 package.json,再複製所有 *.js;有利於 Docker layer 快取。
|
||||
COPY app/package.json /app/package.json
|
||||
COPY app/*.js /app/
|
||||
|
||||
# 複製容器進入點腳本至根目錄。
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
# 賦予進入點腳本可執行權限,否則 ENTRYPOINT 無法執行。
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# 設定容器啟動時執行的進入點 (內部會 exec node /app/index.js)。
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// 讀取並驗證環境變數,組出後續流程所需的設定物件。
|
||||
// 對應原本 entrypoint.sh 的「參數檢查」區段。
|
||||
|
||||
import { isEmptyOrNull, requireValue, requireInteger } from './validate.js'
|
||||
import { section, info, warn } from './logger.js'
|
||||
|
||||
/**
|
||||
* 從環境變數載入設定並完成驗證。
|
||||
* @param {NodeJS.ProcessEnv} env 環境變數來源,預設為 process.env
|
||||
* @returns 包含 API 位址、token、保留數量等資訊的設定物件
|
||||
*/
|
||||
export function loadConfig(env = process.env) {
|
||||
section('參數檢查')
|
||||
|
||||
const serverUrl = env.GITEA_SERVER_URL
|
||||
const repository = env.GITEA_REPOSITORY
|
||||
const keepCountRaw = env.KEEP_COUNT
|
||||
const token = env.GITEA_TOKEN
|
||||
|
||||
info(`GITEA_SERVER_URL=${serverUrl}`)
|
||||
requireValue('GITEA_SERVER_URL', serverUrl)
|
||||
|
||||
info(`GITEA_REPOSITORY=${repository}`)
|
||||
requireValue('GITEA_REPOSITORY', repository)
|
||||
|
||||
info(`KEEP_COUNT=${keepCountRaw}`)
|
||||
requireValue('KEEP_COUNT', keepCountRaw)
|
||||
requireInteger('KEEP_COUNT', keepCountRaw)
|
||||
|
||||
if (isEmptyOrNull(token)) {
|
||||
warn('GITEA_TOKEN is empty; release API calls will be anonymous')
|
||||
} else {
|
||||
info('GITEA_TOKEN=[redacted]')
|
||||
}
|
||||
|
||||
return {
|
||||
serverUrl,
|
||||
repository,
|
||||
token: isEmptyOrNull(token) ? null : token,
|
||||
keepCount: Number(keepCountRaw),
|
||||
releaseApiUrl: `${serverUrl}/api/v1/repos/${repository}/releases`,
|
||||
tagApiUrl: `${serverUrl}/api/v1/repos/${repository}/tags`,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 與 Gitea API 溝通的 HTTP 客戶端,封裝認證標頭、分頁讀取與刪除請求。
|
||||
// 對應原本 entrypoint.sh 的 fetch_all_pages 與 curl DELETE 呼叫。
|
||||
|
||||
export class GiteaClient {
|
||||
/**
|
||||
* @param {{ token?: string | null }} options 認證設定;有 token 時帶上 Authorization 標頭
|
||||
*/
|
||||
constructor({ token = null } = {}) {
|
||||
this.headers = {}
|
||||
if (token) {
|
||||
this.headers.Authorization = `token ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐頁讀取分頁式清單 API,直到回傳空陣列為止,合併成單一陣列。
|
||||
* @param {string} baseUrl 不含 query string 的 API 位址
|
||||
* @returns {Promise<any[]>} 所有頁面合併後的項目
|
||||
*/
|
||||
async fetchAllPages(baseUrl) {
|
||||
const all = []
|
||||
let page = 1
|
||||
|
||||
while (true) {
|
||||
const url = `${baseUrl}?page=${page}`
|
||||
const res = await fetch(url, { headers: this.headers })
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`GET ${url} failed: HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
const items = await res.json()
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
all.push(...items)
|
||||
page += 1
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
/**
|
||||
* 對指定資源發出 DELETE 請求。
|
||||
* @param {string} url 目標資源位址
|
||||
* @returns {Promise<number>} HTTP 狀態碼
|
||||
*/
|
||||
async deleteResource(url) {
|
||||
const res = await fetch(url, { method: 'DELETE', headers: this.headers })
|
||||
return res.status
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 進入點:載入設定、建立 Gitea 客戶端,依序清理舊成品與孤立 tag。
|
||||
|
||||
import { loadConfig } from './config.js'
|
||||
import { GiteaClient } from './gitea-client.js'
|
||||
import { cleanupReleases } from './releases.js'
|
||||
import { cleanupOrphanTags } from './tags.js'
|
||||
import { separator, fail } from './logger.js'
|
||||
|
||||
async function main() {
|
||||
const config = loadConfig()
|
||||
const client = new GiteaClient({ token: config.token })
|
||||
|
||||
await cleanupReleases(client, config)
|
||||
await cleanupOrphanTags(client, config)
|
||||
|
||||
separator()
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
fail(error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
// 統一的主控台輸出格式,對應原本 entrypoint.sh 的 separator/section/info/... 等函式。
|
||||
|
||||
const LINE = '=================================================='
|
||||
const SUBLINE = '--------------------------------------------------'
|
||||
|
||||
/**
|
||||
* 在前後換行的情況下輸出一條等號分隔線至 stdout,用於視覺上區隔不同階段的輸出。
|
||||
*/
|
||||
export function separator() {
|
||||
process.stdout.write(`\n${LINE}\n`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 輸出一個區段標題:先印分隔線,再印標題文字與一條虛線,用於標示流程進入新階段。
|
||||
* @param {string} title 區段標題文字
|
||||
*/
|
||||
export function section(title) {
|
||||
separator()
|
||||
process.stdout.write(`${title}\n`)
|
||||
process.stdout.write(`${SUBLINE}\n`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 `[INFO]` 前綴輸出一般資訊訊息至 stdout。
|
||||
* @param {string} message 訊息內容
|
||||
*/
|
||||
export function info(message) {
|
||||
process.stdout.write(`[INFO] ${message}\n`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 `[OK]` 前綴輸出成功訊息至 stdout(前綴補空白以與其他標籤對齊)。
|
||||
* @param {string} message 訊息內容
|
||||
*/
|
||||
export function success(message) {
|
||||
process.stdout.write(`[OK] ${message}\n`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 `[WARN]` 前綴輸出警告訊息;為與一般輸出同流,仍寫入 stdout。
|
||||
* @param {string} message 訊息內容
|
||||
*/
|
||||
export function warn(message) {
|
||||
process.stdout.write(`[WARN] ${message}\n`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 `[ERR]` 前綴輸出錯誤訊息至 stderr(唯一寫入 stderr 的輸出函式)。
|
||||
* @param {string} message 訊息內容
|
||||
*/
|
||||
export function fail(message) {
|
||||
process.stderr.write(`[ERR] ${message}\n`)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "release-cleanup",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "清理 Gitea 舊版本成品與未指定 release 的 tag",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "node --test"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 清理舊版本成品的功能模組。
|
||||
// 對應原本 entrypoint.sh 的「取得成品資訊」與「刪除舊版本成品」區段。
|
||||
|
||||
import { section, info, success, fail, warn } from './logger.js'
|
||||
import { isEmptyOrNull } from './validate.js'
|
||||
|
||||
/**
|
||||
* 依建立時間由新到舊排序,保留最新的 keepCount 筆,回傳其餘待刪除的成品。
|
||||
* 純函式,方便單元測試。
|
||||
* @param {any[]} releases 成品清單
|
||||
* @param {number} keepCount 要保留的筆數
|
||||
* @returns {any[]} 需要刪除的成品(較舊者)
|
||||
*/
|
||||
export function selectReleasesToDelete(releases, keepCount) {
|
||||
const sorted = [...releases].sort(
|
||||
(a, b) => new Date(b.created_at) - new Date(a.created_at),
|
||||
)
|
||||
return sorted.slice(keepCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* 讀取成品清單,刪除超出保留數量的舊版本成品。
|
||||
* @param {import('./gitea-client.js').GiteaClient} client
|
||||
* @param {ReturnType<import('./config.js').loadConfig>} config
|
||||
*/
|
||||
export async function cleanupReleases(client, config) {
|
||||
section('取得成品資訊')
|
||||
info(`GET ${config.releaseApiUrl}`)
|
||||
|
||||
const releases = await client.fetchAllPages(config.releaseApiUrl)
|
||||
info(`RELEASE_COUNT=${releases.length}`)
|
||||
info(`KEEP_COUNT=${config.keepCount}`)
|
||||
|
||||
if (releases.length <= config.keepCount) {
|
||||
success('沒有需要清理的舊版本成品')
|
||||
return
|
||||
}
|
||||
|
||||
section('刪除舊版本成品')
|
||||
const toDelete = selectReleasesToDelete(releases, config.keepCount)
|
||||
|
||||
for (const release of toDelete) {
|
||||
const { id, tag_name: tag, name } = release
|
||||
|
||||
if (isEmptyOrNull(id)) {
|
||||
warn(`略過沒有 id 的成品: ${tag} (${name})`)
|
||||
continue
|
||||
}
|
||||
|
||||
const url = `${config.releaseApiUrl}/${id}`
|
||||
info(`DELETE ${tag} (${name})`)
|
||||
|
||||
const code = await client.deleteResource(url)
|
||||
if (code === 204) {
|
||||
success(`成功刪除: ${tag} (${name})`)
|
||||
} else {
|
||||
fail(`刪除失敗: ${tag} (${name}), HTTP ${code}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// 清理未指定 release 的 tag 的功能模組。
|
||||
// 對應原本 entrypoint.sh 的「刪除未指定 release 的 tag」區段。
|
||||
|
||||
import { section, info, success, fail, warn } from './logger.js'
|
||||
import { isEmptyOrNull } from './validate.js'
|
||||
|
||||
/**
|
||||
* 將 tag 分類為保留、刪除或略過(無名稱)。
|
||||
* 仍被任一 release 指定的 tag 予以保留,其餘視為孤立 tag 待刪除。
|
||||
* 純函式,方便單元測試。
|
||||
* @param {any[]} tags tag 清單
|
||||
* @param {Iterable<string>} releaseTagNames 仍被 release 指定的 tag 名稱
|
||||
* @returns {{ tag: any, action: 'keep' | 'delete' | 'skip' }[]}
|
||||
*/
|
||||
export function categorizeTags(tags, releaseTagNames) {
|
||||
const keep = new Set(releaseTagNames)
|
||||
|
||||
return tags.map((tag) => {
|
||||
if (isEmptyOrNull(tag.name)) {
|
||||
return { tag, action: 'skip' }
|
||||
}
|
||||
if (keep.has(tag.name)) {
|
||||
return { tag, action: 'keep' }
|
||||
}
|
||||
return { tag, action: 'delete' }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新讀取成品清單以取得仍被指定的 tag,再刪除未指定 release 的孤立 tag。
|
||||
* @param {import('./gitea-client.js').GiteaClient} client
|
||||
* @param {ReturnType<import('./config.js').loadConfig>} config
|
||||
*/
|
||||
export async function cleanupOrphanTags(client, config) {
|
||||
section('刪除未指定 release 的 tag')
|
||||
|
||||
// 重新取得 release 清單,得到刪除舊版本後仍指定 tag 的成品
|
||||
const currentReleases = await client.fetchAllPages(config.releaseApiUrl)
|
||||
const releaseTagNames = currentReleases.map((release) => release.tag_name)
|
||||
|
||||
info(`GET ${config.tagApiUrl}`)
|
||||
const tags = await client.fetchAllPages(config.tagApiUrl)
|
||||
info(`TAG_COUNT=${tags.length}`)
|
||||
|
||||
for (const { tag, action } of categorizeTags(tags, releaseTagNames)) {
|
||||
if (action === 'skip') {
|
||||
warn('略過沒有名稱的 tag')
|
||||
continue
|
||||
}
|
||||
|
||||
if (action === 'keep') {
|
||||
info(`保留指定 release 的 tag: ${tag.name}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const url = `${config.tagApiUrl}/${tag.name}`
|
||||
info(`DELETE tag ${tag.name}`)
|
||||
|
||||
const code = await client.deleteResource(url)
|
||||
if (code === 204) {
|
||||
success(`成功刪除未指定 release 的 tag: ${tag.name}`)
|
||||
} else {
|
||||
fail(`刪除 tag 失敗: ${tag.name}, HTTP ${code}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// 參數驗證,對應原本 entrypoint.sh 的 is_empty_or_null/require_value/require_integer。
|
||||
// 驗證失敗時丟出 Error,由進入點統一捕捉後以非零狀態結束。
|
||||
|
||||
/**
|
||||
* 判斷值是否視為「空」。使用嚴格相等,因此 `0`、`false`、字串 `"0"` 都不算空。
|
||||
* @param {*} value 待判斷的值
|
||||
* @returns {boolean} 當值為 `undefined`、`null`、空字串或字串 `"null"` 時回傳 true
|
||||
*/
|
||||
export function isEmptyOrNull(value) {
|
||||
return value === undefined || value === null || value === '' || value === 'null'
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求指定欄位有值,空值時丟出 Error 以中止流程。
|
||||
* @param {string} name 欄位名稱,用於組出錯誤訊息
|
||||
* @param {*} value 待檢查的值,空值判定委派給 [[isEmptyOrNull]]
|
||||
* @throws {Error} 當 value 為空時丟出 `${name} is required`
|
||||
*/
|
||||
export function requireValue(name, value) {
|
||||
if (isEmptyOrNull(value)) {
|
||||
throw new Error(`${name} is required`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求指定欄位為非負整數。會先轉成字串再以 `/^[0-9]+$/` 比對,因此可接受數字或純數字字串,
|
||||
* 但拒絕負數、小數、空值與非數字內容。
|
||||
* @param {string} name 欄位名稱,用於組出錯誤訊息
|
||||
* @param {string|number} value 待檢查的值
|
||||
* @throws {Error} 當 value 不是非負整數時丟出 `${name} must be a non-negative integer`
|
||||
*/
|
||||
export function requireInteger(name, value) {
|
||||
if (!/^[0-9]+$/.test(String(value))) {
|
||||
throw new Error(`${name} must be a non-negative integer`)
|
||||
}
|
||||
}
|
||||
+12
-175
@@ -1,177 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
#!/usr/bin/env sh
|
||||
# =============================================================================
|
||||
# 用途: release-cleanup Action 的容器進入點 (entrypoint)。
|
||||
# 專案已由 bash 改寫為 Node.js,本腳本僅負責啟動 /app/index.js,
|
||||
# 由 Node.js 程式執行實際的 release 清理邏輯。
|
||||
# 更新日期: 2026/06/26 10:28:36
|
||||
# =============================================================================
|
||||
|
||||
separator() {
|
||||
printf '\n%s\n' '=================================================='
|
||||
}
|
||||
# set -e: 任一指令失敗即中止; set -u: 使用未定義變數即報錯。確保失敗能即時暴露。
|
||||
set -eu
|
||||
|
||||
section() {
|
||||
separator
|
||||
printf '%s\n' "$1"
|
||||
printf '%s\n' '--------------------------------------------------'
|
||||
}
|
||||
|
||||
info() {
|
||||
printf '[INFO] %s\n' "$1"
|
||||
}
|
||||
|
||||
success() {
|
||||
printf '[OK] %s\n' "$1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '[WARN] %s\n' "$1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[ERR] %s\n' "$1" >&2
|
||||
}
|
||||
|
||||
is_empty_or_null() {
|
||||
[ -z "${1:-}" ] || [ "${1:-}" = "null" ]
|
||||
}
|
||||
|
||||
require_value() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
|
||||
info "$name=$value"
|
||||
if is_empty_or_null "$value"; then
|
||||
fail "$name is required"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_integer() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
|
||||
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||
fail "$name must be a non-negative integer"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_all_pages() {
|
||||
local base_url="$1"
|
||||
local all_json='[]'
|
||||
local page=1
|
||||
local page_url page_json
|
||||
|
||||
while :; do
|
||||
page_url="$base_url?page=$page"
|
||||
page_json="$(curl -fsS "${auth_header[@]}" "$page_url")"
|
||||
|
||||
if [ "$(jq 'length' <<<"$page_json")" -eq 0 ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
all_json="$(jq -s 'add' <<<"$all_json"$'\n'"$page_json")"
|
||||
page=$((page + 1))
|
||||
done
|
||||
|
||||
printf '%s' "$all_json"
|
||||
}
|
||||
|
||||
section "參數檢查"
|
||||
require_value "GITEA_SERVER_URL" "$GITEA_SERVER_URL"
|
||||
require_value "GITEA_REPOSITORY" "$GITEA_REPOSITORY"
|
||||
require_value "KEEP_COUNT" "$KEEP_COUNT"
|
||||
require_integer "KEEP_COUNT" "$KEEP_COUNT"
|
||||
|
||||
if is_empty_or_null "${GITEA_TOKEN:-}"; then
|
||||
warn "GITEA_TOKEN is empty; release API calls will be anonymous"
|
||||
else
|
||||
info "GITEA_TOKEN=[redacted]"
|
||||
fi
|
||||
|
||||
release_api_url="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/releases"
|
||||
auth_header=()
|
||||
|
||||
if ! is_empty_or_null "${GITEA_TOKEN:-}"; then
|
||||
auth_header=(-H "Authorization: token $GITEA_TOKEN")
|
||||
fi
|
||||
|
||||
section "取得成品資訊"
|
||||
info "GET $release_api_url"
|
||||
|
||||
release_json="$(fetch_all_pages "$release_api_url")"
|
||||
release_json="$(jq -e 'sort_by(.created_at) | reverse' <<<"$release_json")"
|
||||
release_count="$(jq 'length' <<<"$release_json")"
|
||||
|
||||
info "RELEASE_COUNT=$release_count"
|
||||
info "KEEP_COUNT=$KEEP_COUNT"
|
||||
|
||||
if [ "$release_count" -le "$KEEP_COUNT" ]; then
|
||||
success "沒有需要清理的舊版本成品"
|
||||
else
|
||||
section "刪除舊版本成品"
|
||||
|
||||
release_to_delete="$(jq -c ".[$KEEP_COUNT:]" <<<"$release_json")"
|
||||
|
||||
while IFS= read -r release_item; do
|
||||
[ -z "$release_item" ] && continue
|
||||
|
||||
release_id="$(jq -r '.id' <<<"$release_item")"
|
||||
release_tag="$(jq -r '.tag_name' <<<"$release_item")"
|
||||
release_name="$(jq -r '.name' <<<"$release_item")"
|
||||
|
||||
if is_empty_or_null "$release_id"; then
|
||||
warn "略過沒有 id 的成品: $release_tag ($release_name)"
|
||||
continue
|
||||
fi
|
||||
|
||||
delete_url="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/releases/$release_id"
|
||||
info "DELETE $release_tag ($release_name)"
|
||||
|
||||
delete_code="$(curl -sS -o /dev/null -w "%{http_code}" -X DELETE "${auth_header[@]}" "$delete_url")"
|
||||
|
||||
if [ "$delete_code" -eq 204 ]; then
|
||||
success "成功刪除: $release_tag ($release_name)"
|
||||
else
|
||||
fail "刪除失敗: $release_tag ($release_name), HTTP $delete_code"
|
||||
fi
|
||||
done < <(jq -c '.[]' <<<"$release_to_delete")
|
||||
fi
|
||||
|
||||
section "刪除未指定 release 的 tag"
|
||||
|
||||
# 重新取得 release 清單,得到刪除舊版本後仍指定 tag 的成品
|
||||
current_release_json="$(fetch_all_pages "$release_api_url")"
|
||||
release_tags_json="$(jq -c '[.[].tag_name]' <<<"$current_release_json")"
|
||||
|
||||
tag_api_url="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/tags"
|
||||
info "GET $tag_api_url"
|
||||
|
||||
tag_json="$(fetch_all_pages "$tag_api_url")"
|
||||
tag_count="$(jq 'length' <<<"$tag_json")"
|
||||
info "TAG_COUNT=$tag_count"
|
||||
|
||||
while IFS= read -r tag_item; do
|
||||
[ -z "$tag_item" ] && continue
|
||||
|
||||
tag_name="$(jq -r '.name' <<<"$tag_item")"
|
||||
|
||||
if is_empty_or_null "$tag_name"; then
|
||||
warn "略過沒有名稱的 tag"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$(jq --arg name "$tag_name" 'any(.[]; . == $name)' <<<"$release_tags_json")" = "true" ]; then
|
||||
info "保留指定 release 的 tag: $tag_name"
|
||||
continue
|
||||
fi
|
||||
|
||||
delete_url="$tag_api_url/$tag_name"
|
||||
info "DELETE tag $tag_name"
|
||||
|
||||
delete_code="$(curl -sS -o /dev/null -w "%{http_code}" -X DELETE "${auth_header[@]}" "$delete_url")"
|
||||
|
||||
if [ "$delete_code" -eq 204 ]; then
|
||||
success "成功刪除未指定 release 的 tag: $tag_name"
|
||||
else
|
||||
fail "刪除 tag 失敗: $tag_name, HTTP $delete_code"
|
||||
fi
|
||||
done < <(jq -c '.[]' <<<"$tag_json")
|
||||
|
||||
separator
|
||||
# 進入點:實際邏輯改由 Node.js 實作,集中於 /app 目錄。
|
||||
# 以 exec 取代當前 shell process,讓 node 成為容器的 PID 1,正確接收訊號 (SIGTERM 等)。
|
||||
exec node /app/index.js
|
||||
|
||||
Reference in New Issue
Block a user