feat(release-cleanup): 改為 Node 主程式並容器化

將 action 由 bash(curl/jq) 改寫為 Node 主程式 app/index.js(沿用原清理邏輯:
正式版與 beta 版各自保留 KEEP_COUNT 筆、刪除多餘 release 與未綁定 release 的 tag),
entrypoint.sh 改為輸出橫幅後 exec node 主程式,Dockerfile 改用 node alpine 六步結構,
並補上 app/package.json 與 action.yaml 輸入說明。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-06-30 12:41:10 +08:00
co-authored by Claude Opus 4.8
parent cb943e49a7
commit 7ac64709b9
5 changed files with 402 additions and 194 deletions
Regular → Executable
+35 -190
View File
@@ -1,195 +1,40 @@
#!/usr/bin/env bash
set -Eeuo pipefail
#!/usr/bin/env sh
separator() {
printf '\n%s\n' '=================================================='
}
# =============================================================================
# 此指令檔的用途:
# 本檔為 Release Cleanup action 的容器啟動腳本(container entrypoint)。
# 啟動時先輸出一段橫幅,顯示 action 名稱/用途/更新時間,方便在 CI/CD log
# 中辨識正在執行哪一個 action 及其版本;接著以 exec 啟動 node 主程式
# /app/index.js,並把所有外部傳入的參數原樣交給它。
#
# 更新日期(台灣時區):2026/06/30 12:02:55
# =============================================================================
section() {
separator
printf '%s\n' "$1"
printf '%s\n' '--------------------------------------------------'
}
# 啟用「遇到任何指令失敗(非 0 結束碼)即立刻中止腳本」的模式。
# 用於避免前置步驟失敗後仍繼續往下執行,確保啟動失敗能正確反映成非 0 exit code。
set -e
info() {
printf '[INFO] %s\n' "$1"
}
# action 顯示名稱,供下方橫幅輸出使用。
ACTION_NAME='Release Cleanup'
# action 用途說明,供下方橫幅輸出使用。
ACTION_DESC='清理舊成品'
# action 更新時間(固定字串,台灣時區),供下方橫幅輸出使用。
ACTION_UPDATED='2026/06/30 12:02:55'
success() {
printf '[OK] %s\n' "$1"
}
# 輸出一個空行 + 分隔線,作為橫幅的上邊框,讓 log 中的資訊區塊更易閱讀。
printf '\n%s\n' '=================================================='
# 輸出 action 名稱;以 "$ACTION_NAME" 帶引號傳入,避免變數內容被分詞或萬用字元展開。
printf '名稱 : %s\n' "$ACTION_NAME"
# 輸出 action 用途;同樣以帶引號變數傳入確保字串完整。
printf '用途 : %s\n' "$ACTION_DESC"
# 輸出 action 更新時間;同樣以帶引號變數傳入確保字串完整。
printf '更新 : %s\n' "$ACTION_UPDATED"
# 輸出分隔線,作為橫幅的下邊框。
printf '%s\n' '=================================================='
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")"
normal_release_json="$(jq -c '[.[] | select(((.tag_name // "") | test("-beta\\.")) | not)]' <<<"$release_json")"
beta_release_json="$(jq -c '[.[] | select((.tag_name // "") | test("-beta\\."))]' <<<"$release_json")"
normal_release_count="$(jq 'length' <<<"$normal_release_json")"
beta_release_count="$(jq 'length' <<<"$beta_release_json")"
info "RELEASE_COUNT=$release_count"
info "NORMAL_RELEASE_COUNT=$normal_release_count"
info "BETA_RELEASE_COUNT=$beta_release_count"
info "KEEP_COUNT=$KEEP_COUNT"
normal_release_to_delete='[]'
if [ "$normal_release_count" -gt "$KEEP_COUNT" ]; then
normal_release_to_delete="$(jq -c ".[$KEEP_COUNT:]" <<<"$normal_release_json")"
fi
beta_release_to_delete='[]'
if [ "$beta_release_count" -gt "$KEEP_COUNT" ]; then
beta_release_to_delete="$(jq -c ".[$KEEP_COUNT:]" <<<"$beta_release_json")"
fi
release_to_delete="$(jq -c -s 'add' <<<"$normal_release_to_delete"$'\n'"$beta_release_to_delete")"
release_to_delete_count="$(jq 'length' <<<"$release_to_delete")"
if [ "$release_to_delete_count" -eq 0 ]; then
success "沒有需要清理的舊版本成品"
else
section "刪除舊版本成品"
info "DELETE_RELEASE_COUNT=$release_to_delete_count"
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
# 以 exec 啟動 node 主程式:exec 會用 node 行程「取代」目前的 shell 行程,
# 而非另開子行程。如此可讓 node 成為 PID 1(或直接承接原 shell 的 PID),
# 正確接收容器傳來的訊號(例如 SIGTERM/SIGINT)以利優雅關閉,
# 並讓 node 的結束碼直接成為容器的結束碼。
# "$@" 將腳本收到的所有參數原樣(逐一保留分隔,避免被重新分詞)轉交給 node 主程式。
exec node /app/index.js "$@"