diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml
index 30b1952..3509416 100644
--- a/.gitea/workflows/cd.yaml
+++ b/.gitea/workflows/cd.yaml
@@ -1,12 +1,34 @@
+# ===================================================================
+# 用途:本檔為 Gitea CD(持續部署)工作流程設定。
+# 當 master 分支有 push 時自動觸發,使用公司 Gitea 上的
+# composite action「release-tag-version」對建置成品進行釋出與版本標註(tag)。
+# 更新日期(台灣時區 Asia/Taipei):2026/07/01 09:14:10
+# ===================================================================
+
+# 工作流程名稱,顯示於 Gitea Actions 介面,用以辨識此 CD 流程
name: CD
+# 定義觸發此工作流程的事件
on:
+ # 當有程式碼 push(推送)至指定分支時觸發
push:
+ # 限定僅特定分支的 push 才觸發
branches:
+ # 僅 master 分支(正式發布主線)push 時才執行此 CD 流程
- master
+# 定義此工作流程包含的所有 job(工作)
jobs:
+ # job 識別代號:release-tag-version(釋出並標註版本)
release-tag-version:
+ # job 的顯示名稱,呈現於 Gitea Actions 執行畫面
name: Release Tag Version
+ # 指定此 job 執行所使用的 runner 標籤(ubuntu runner)
runs-on: ubuntu
+ # 定義此 job 依序執行的步驟清單
steps:
+ # 步驟名稱:釋出並標註成品版本
- name: 釋出並標註成品版本
+ # 引用公司 Gitea 上的 composite action「release-tag-version」執行釋出與打 tag。
+ # 版本(@ 之後)由 Gitea 變數 vars.ACTION_RELEASE_TAG_VERSION 動態決定,
+ # 便於集中管理所引用 action 的版本,不需修改本檔即可切換版本。
+ # 需人工確認:此 action 之具體釋出/標註行為與所需 secrets/權限,須參閱該 composite action 定義。
uses: https://gitea.jsc.idv.tw/composite-actions/release-tag-version@${{ vars.ACTION_RELEASE_TAG_VERSION }}
diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml
index b9d4eed..86d4dfe 100644
--- a/.gitea/workflows/ci.yaml
+++ b/.gitea/workflows/ci.yaml
@@ -1,19 +1,57 @@
+# ===================================================================
+# 用途:Gitea CI workflow,於 Pull Request 開啟(opened)或同步更新(synchronize)時觸發(master 分支除外)。
+# 流程先呼叫 composite action `release-tag-version`(is_beta: true)標註一個 beta 成品版本並輸出版本號,
+# 再以該 beta 版本(@v${version})執行本專案的 `release-cleanup` action,對自身成品做清理(dogfooding 自我清理驗證)。
+# 更新日期(台灣時區 Asia/Taipei):2026/07/01 09:14:10
+# ===================================================================
+
+# workflow 名稱,顯示於 Gitea Actions 介面
name: CI
+# 觸發事件設定
on:
+ # 針對 Pull Request 事件觸發
pull_request:
+ # 目標分支過濾:忽略以下分支(即 master 不觸發本 workflow)
branches-ignore:
+ # 排除 master 分支,避免對正式主線的 PR 執行本流程
- master
+ # 僅在 PR 被「開啟(opened)」或「推送新 commit 同步(synchronize)」時觸發
types: [opened, synchronize]
+# 定義所有 jobs
jobs:
- ai-code-review:
- name: AI Code Review
+ # 第一個 job: 釋出並標註成品版本,並將版本號往外拋給後續 job 使用。
+ release-tag-version:
+ # job 顯示名稱。
+ name: Release Tag Version
+ # 指定執行環境 (runner) 標籤為 ubuntu。
runs-on: ubuntu
- permissions:
- contents: write
- pull-requests: write
- issues: write
+ # 宣告此 job 的輸出,供後續 needs 此 job 的其他 job 取用。
+ outputs:
+ # 將下方 id 為 release-tag-version 的 step 所輸出的 version,設為此 job 的對外 version 輸出 (關鍵點: 透過 outputs 把標註出的版本往外拋)。
+ version: ${{ steps.release-tag-version.outputs.version }}
+ # 此 job 依序執行的步驟。
steps:
- - name: AI 程式碼審查 by OpenCode
- uses: https://gitea.jsc.idv.tw/composite-actions/opencode-code-review@${{ vars.ACTION_OPENCODE_CODE_REVIEW_VERSION }}
+ # 步驟: 呼叫 composite action 進行釋出並標註成品版本。
+ - name: 釋出並標註成品版本
+ # 設定此 step 的 id,讓上方 outputs 可用 steps.release-tag-version.outputs.version 取得其輸出。
+ id: release-tag-version
+ # 引用 release-tag-version composite action,版本以變數 vars.ACTION_RELEASE_TAG_VERSION 釘選 (集中於 repo/org 變數管理,便於統一升版)。
+ uses: https://gitea.jsc.idv.tw/composite-actions/release-tag-version@${{ vars.ACTION_RELEASE_TAG_VERSION }}
+ # 傳遞給該 action 的輸入參數。
with:
- comment_token: ${{ secrets.COMMENT_TOKEN }}
+ # is_beta 設為 true 表示走 beta 標版流程 (標註的是 beta 成品版本,而非正式版)。
+ is_beta: true
+ # 第二個 job: 以前一個 job 標註出的 beta 版本執行清理成品 action (dogfooding 自我清理)。
+ release-cleanup:
+ # job 顯示名稱。
+ name: Release Cleanup
+ # 指定執行環境 (runner) 標籤為 ubuntu。
+ runs-on: ubuntu
+ # 宣告相依於 release-tag-version job,確保其先完成且可取用其 outputs.version。
+ needs: release-tag-version
+ # 此 job 依序執行的步驟。
+ steps:
+ # 步驟: 呼叫 release-cleanup action 清理舊成品。
+ - name: 清理舊成品
+ # 引用本專案 release-cleanup action,版本釘選為前一個 job 輸出的 beta 版本 v${version} (以剛標出的 beta 版自我驗證清理流程)。
+ uses: https://gitea.jsc.idv.tw/docker-actions/release-cleanup@v${{ needs.release-tag-version.outputs.version }}
diff --git a/Dockerfile b/Dockerfile
index 593c511..4f76c3f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,7 +5,7 @@
# 建置 Release Cleanup docker container action 的映像。
# 以 node alpine 為基底,複製 app/ 主程式與 entrypoint.sh,設定入口。
#
-# 更新日期:2026/06/30 12:02:55(台灣時區)
+# 更新日期(台灣時區 Asia/Taipei):2026/07/01 09:14:10
# =============================================================================
# -----------------------------------------------------------------------------
@@ -29,7 +29,7 @@ WORKDIR /app
# -----------------------------------------------------------------------------
# 3. 複製檔案:主程式(app/)與啟動腳本
# -----------------------------------------------------------------------------
-# 複製 app/ 目錄全部內容到映像內 /app/(主程式,含 index.js 進入點)
+# 複製 app/ 目錄全部內容到映像內 /app/(主程式,含 index.js 進入點;須與 entrypoint.sh 的 /app/index.js 一致)
COPY app/ /app/
# 複製啟動腳本到映像根目錄 /entrypoint.sh(供 ENTRYPOINT 使用)
COPY entrypoint.sh /entrypoint.sh
diff --git a/README.md b/README.md
index c229e66..da84580 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Release Cleanup
-> 更新時間:2026/06/30 12:02:55(台灣時區 Asia/Taipei)
+> 更新時間:2026/07/01 09:20:52(台灣時區 Asia/Taipei)
清理舊成品的 **Gitea / GitHub docker container action**。依保留數量清理 repo 的舊 release,並刪除沒有對應 release 的 tag;正式版與 beta 版各自獨立保留指定數量。
@@ -10,11 +10,30 @@
## 行為總覽
1. **參數檢查**:驗證 `GITEA_SERVER_URL`、`GITEA_REPOSITORY`、`KEEP_COUNT`(缺值或 `KEEP_COUNT` 非整數時以 exit code 1 結束)。
-2. **取得成品資訊**:取得所有 release,依建立時間由新到舊排序,並以 tag 名稱含 `-beta.` 與否分為正式版與 beta 版。
-3. **刪除舊版本成品**:正式版與 beta 版**各自**保留最新 `KEEP_COUNT` 筆,刪除其餘 release(單筆刪除失敗只記錄錯誤,不中斷)。
-4. **刪除未指定 release 的 tag**:重新取得 release 後,刪除所有未被任何 release 指定的 tag。
+2. **取得舊版本**:取得所有 release,依建立時間由新到舊排序,並以 tag 名稱含 `-beta.` 與否分為正式版與 beta 版。
+3. **刪除舊版本**:正式版與 beta 版**各自**保留最新 `KEEP_COUNT` 筆,刪除其餘 release(單筆刪除失敗只記錄錯誤,不中斷);接著重新取得 release 後,刪除所有未被任何 release 指定的 tag。
-`GITEA_TOKEN` 為空時以匿名身分呼叫 API。取得分頁失敗或參數不合法則直接以 exit code 1 結束程序。
+`GITEA_TOKEN` 為空時以匿名身分呼叫 API。取得分頁失敗或參數不合法則直接以 exit code 1 結束程序;底層 `fetch` 網路錯誤或回應非 JSON 造成的解析失敗會上拋,由檔尾 `main().catch` 統一捕捉後以 exit code 1 結束。
+
+## 日誌輸出格式
+
+所有等級式輸出統一為 `[{階段}][{等級}][{時間}]: {訊息}`:
+
+- `階段`:目前流程階段(`參數檢查` / `取得舊版本` / `刪除舊版本`),由 `stage()` 設定;未設定時省略整個 `[階段]` 區塊。
+- `等級`:`INF`(含成功訊息)/ `WRN` / `ERR`。
+- `時間`:Asia/Taipei 的 `yyyy/MM/dd HH:mm:ss`。
+- `fail` 走 stderr,其餘走 stdout。
+
+範例:
+
+```text
+[參數檢查][INF][2026/07/01 09:20:52]: GITEA_SERVER_URL=https://gitea.jsc.idv.tw
+[取得舊版本][INF][2026/07/01 09:20:52]: RELEASE_COUNT=3
+[取得舊版本][INF][2026/07/01 09:20:52]: 沒有需要清理的舊版本成品
+[刪除舊版本][ERR][2026/07/01 09:20:52]: 刪除 tag 失敗: v0.0.1, HTTP 500
+```
+
+> 容器啟動橫幅(名稱/用途/更新時間)由 `entrypoint.sh` 輸出,不帶等級與階段前綴。
## Action 輸入與環境變數
@@ -44,7 +63,7 @@ jobs:
| 專案名稱 | 專案描述 |
| --- | --- |
-| [release-cleanup](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app) | 清理 Gitea repo 舊 release 與孤兒 tag 的 docker container action;正式版與 beta 版各自保留指定數量 |
+| [release-cleanup](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app) | 清理 Gitea repo 舊 release 與孤兒 tag 的 docker container action;正式版與 beta 版各自保留指定數量,並以統一的 `[階段][等級][時間]` 日誌輸出處理過程 |
### 參考專案
@@ -64,106 +83,127 @@ jobs:
### release-cleanup
+> 說明:本專案為單一 Node 模組(`app/index.js`),未 `module.exports` 任何函式,下列為模組內可文件化的頂層函式(不含 `main` 內部的巢狀私有函式 `fetchAllPages` / `deleteUrl`)。
+
| 功能名稱 | 功能描述 |
| --- | --- |
-| [separator](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L10) | [在 stdout 輸出視覺分隔線](#separator) |
-| [section](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L21) | [輸出區段標題區塊](#section) |
-| [info](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L33) | [以 \[INFO\] 前綴輸出資訊訊息](#info) |
-| [success](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L43) | [以 \[OK\] 前綴輸出成功訊息](#success) |
-| [warn](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L53) | [以 \[WARN\] 前綴輸出警告訊息](#warn) |
-| [fail](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L64) | [以 \[ERR\] 前綴輸出錯誤訊息到 stderr](#fail) |
-| [isEmptyOrNull](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L76) | [判斷值是否視為空 / 未設定](#isemptyornull) |
-| [requireValue](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L89) | [檢查必填值,空值時結束程序](#requirevalue) |
-| [requireInteger](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L106) | [驗證非負整數,不符時結束程序](#requireinteger) |
-| [isBeta](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L120) | [判斷名稱是否為 beta 版本](#isbeta) |
-| [main](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L140) | [Action 主流程:清理舊 release 與孤兒 tag](#main) |
+| [stage](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L18) | [設定目前流程階段名稱(log 的 \[階段\] 前綴)](#stage) |
+| [timestamp](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L29) | [取得 Asia/Taipei 時間字串](#timestamp) |
+| [formatLog](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L44) | [組出統一格式的 log 字串](#formatlog) |
+| [info](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L55) | [以 \[INF\] 等級輸出資訊訊息](#info) |
+| [success](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L66) | [以 \[INF\] 等級輸出成功訊息](#success) |
+| [warn](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L76) | [以 \[WRN\] 等級輸出警告訊息](#warn) |
+| [fail](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L87) | [以 \[ERR\] 等級輸出錯誤訊息到 stderr](#fail) |
+| [isEmptyOrNull](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L99) | [判斷值是否視為空 / 未設定](#isemptyornull) |
+| [requireValue](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L112) | [檢查必填值,空值時結束程序](#requirevalue) |
+| [requireInteger](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L129) | [驗證非負整數,不符時結束程序](#requireinteger) |
+| [isBeta](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L143) | [判斷名稱是否為 beta 版本](#isbeta) |
+| [main](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L163) | [Action 主流程:清理舊 release 與孤兒 tag](#main) |
## 使用範例
-
-### separator
+
+### stage
-在 stdout 輸出一條前後帶換行、由 50 個等號組成的視覺分隔線,用於在 log 中切割區段。無參數、無回傳值。
+設定目前流程階段名稱,之後 `info`/`success`/`warn`/`fail` 輸出的每行 log 會以 `[階段]` 前綴標示所屬階段;傳入空字串可清除前綴。
```js
-separator();
-// 輸出:
-//
-// ==================================================
+stage('取得舊版本');
+info('RELEASE_COUNT=3');
+// 輸出:[取得舊版本][INF][2026/07/01 09:20:52]: RELEASE_COUNT=3
```
-
-### section
+> 前置條件:無。結果:更新模組層級的目前階段字串;本身不輸出任何內容。
-輸出一個區段標題區塊:上緣等號分隔線、標題文字、下緣虛線,用於標示流程進入新階段。
+
+### timestamp
+
+取得目前 **Asia/Taipei** 時區的時間字串,格式為 `yyyy/MM/dd HH:mm:ss`。使用 Node 內建 `Intl`(`toLocaleString('sv-SE')` 輸出 24 小時制再把 `-` 換成 `/`),不需額外相依。供 `formatLog` 組裝統一格式前綴使用。
```js
-section('參數檢查');
-// 輸出:
-// ==================================================
-// 參數檢查
-// --------------------------------------------------
+timestamp();
+// 例如回傳: '2026/07/01 09:20:52'
+```
+
+> 前置條件:執行環境須支援 `Intl`(Node 18+ 內建)。結果:回傳當下台灣時間字串,無副作用。
+
+
+### formatLog
+
+依統一格式組出一行 log 字串:`[階段?][等級][時間]: 訊息`。階段取自目前 `currentStage`(為空字串時省略整個 `[階段]` 區塊),時間為 Asia/Taipei。為 `info`/`success`/`warn`/`fail` 共用的內部格式化函式,本身不輸出。
+
+```js
+stage('刪除舊版本');
+formatLog('ERR', '刪除 tag 失敗: v0.0.1, HTTP 500');
+// 回傳: '[刪除舊版本][ERR][2026/07/01 09:20:52]: 刪除 tag 失敗: v0.0.1, HTTP 500'
```
### info
-以 `[INFO]` 前綴將一般資訊訊息輸出到 stdout(`console.log`)。
+以 `[階段?][INF][時間]:` 前綴將一般資訊訊息輸出到 stdout(`console.log`)。
```js
+stage('取得舊版本');
info('RELEASE_COUNT=10');
-// 輸出:[INFO] RELEASE_COUNT=10
+// 輸出:[取得舊版本][INF][2026/07/01 09:20:52]: RELEASE_COUNT=10
```
### success
-以 `[OK]` 前綴將成功訊息輸出到 stdout。
+以 `[階段?][INF][時間]:` 前綴將成功訊息輸出到 stdout。規範等級碼無「成功」一項,故等級採 `INF`,成功語意保留於訊息文字。
```js
-success('成功刪除: v1.0.0 (Release 1.0.0)');
-// 輸出:[OK] 成功刪除: v1.0.0 (Release 1.0.0)
+stage('取得舊版本');
+success('沒有需要清理的舊版本成品');
+// 輸出:[取得舊版本][INF][2026/07/01 09:20:52]: 沒有需要清理的舊版本成品
```
### warn
-以 `[WARN]` 前綴將警告訊息輸出到 stdout(非致命情況,程式會繼續執行)。
+以 `[階段?][WRN][時間]:` 前綴將警告訊息輸出到 stdout(非致命情況,程式會繼續執行)。
```js
+stage('參數檢查');
warn('GITEA_TOKEN is empty; release API calls will be anonymous');
-// 輸出:[WARN] GITEA_TOKEN is empty; release API calls will be anonymous
+// 輸出:[參數檢查][WRN][2026/07/01 09:20:52]: GITEA_TOKEN is empty; release API calls will be anonymous
```
### fail
-以 `[ERR]` 前綴將錯誤訊息輸出到 **stderr**(`console.error`)。僅負責輸出,不會結束程序;是否離開由呼叫端決定。
+以 `[階段?][ERR][時間]:` 前綴將錯誤訊息輸出到 **stderr**(`console.error`)。僅負責輸出,不會結束程序;是否離開由呼叫端決定。
```js
-fail('刪除失敗: v0.1.0 (Release 0.1.0), HTTP 500');
+stage('刪除舊版本');
+fail('刪除 tag 失敗: v0.0.1, HTTP 500');
+// 輸出(stderr):[刪除舊版本][ERR][2026/07/01 09:20:52]: 刪除 tag 失敗: v0.0.1, HTTP 500
process.exit(1); // 終止由呼叫端負責
```
### isEmptyOrNull
-判斷值是否視為「空 / 未設定」,涵蓋 `undefined`、`null`、空字串,以及字面字串 `'null'`(用於處理 CI/環境變數把未設定值帶成字串 `"null"` 的情況)。
+判斷值是否視為「空 / 未設定」,涵蓋 `undefined`、`null`、空字串,以及字面字串 `'null'`(用於處理 CI/環境變數把未設定值帶成字串 `"null"` 的情況)。以嚴格相等比對,故 `0`、`false`、`'0'`、純空白字串**不**視為空。
```js
isEmptyOrNull(''); // true
isEmptyOrNull('null'); // true(字面字串)
isEmptyOrNull('abc'); // false
+isEmptyOrNull('0'); // false(非空字串)
```
### requireValue
-檢查必填值。先以 `[INFO]` 印出 `name=value`,若值被判定為空則印出 `[ERR]` 並以 exit code 1 結束程序。
+檢查必填值。先以 `info`(`[INF]`)印出 `name=value`,若值被判定為空則以 `fail`(`[ERR]`)輸出並以 exit code 1 結束程序。
```js
+stage('參數檢查');
requireValue('GITEA_SERVER_URL', process.env.GITEA_SERVER_URL);
-// 值為空 → 輸出 [ERR] GITEA_SERVER_URL is required,並 process.exit(1)
+// 值為空 → 輸出 [參數檢查][ERR][時間]: GITEA_SERVER_URL is required,並 process.exit(1)
```
> 前置條件:應在程式啟動早期呼叫。注意此函式會將值原樣印到 stdout,故不用於機密值(程式對 `GITEA_TOKEN` 改以 redacted 方式輸出)。
@@ -171,17 +211,17 @@ requireValue('GITEA_SERVER_URL', process.env.GITEA_SERVER_URL);
### requireInteger
-驗證值是否為非負整數字串(正則 `/^[0-9]+$/`,允許前導零、不允許負號或小數),不符合時印出 `[ERR]` 並以 exit code 1 結束程序。
+驗證值是否為非負整數字串(正則 `/^[0-9]+$/`,允許前導零、不允許負號或小數),不符合時以 `[ERR]` 輸出並以 exit code 1 結束程序。本函式不處理空值語意,需搭配 `requireValue` 先攔缺值。
```js
requireInteger('KEEP_COUNT', '2'); // 通過
-requireInteger('KEEP_COUNT', '-1'); // 輸出 [ERR] ... 並 process.exit(1)
+requireInteger('KEEP_COUNT', '-1'); // 輸出 [ERR] 訊息並 process.exit(1)
```
### isBeta
-判斷 release / tag 名稱是否為 beta 版本,規則為名稱中含有子字串 `-beta.`。
+判斷 release / tag 名稱是否為 beta 版本,規則為名稱中含有子字串 `-beta.`(區分大小寫)。
```js
isBeta('v1.2.3-beta.1'); // true
@@ -192,7 +232,7 @@ isBeta('v1.2.3-beta'); // false(無結尾的點,不符合 -beta.)
### main
-Action 主流程(`async`)。讀取環境變數後依序執行參數檢查、取得 release、刪除舊版本(正式版與 beta 版各自保留 `KEEP_COUNT` 筆)、刪除未指定 release 的 tag。由檔尾 `main().catch(...)` 立即執行。
+Action 主流程(`async`)。讀取環境變數後依序執行參數檢查、取得 release、刪除舊版本(正式版與 beta 版各自保留 `KEEP_COUNT` 筆)、刪除未指定 release 的 tag;各階段以 `stage()` 標示,log 皆帶 `[階段]` 前綴。由檔尾 `main().catch(...)` 立即執行並收斂未攔截的例外。
```js
// 容器啟動時由 entrypoint.sh 執行:exec node /app/index.js
@@ -204,4 +244,4 @@ main().catch((err) => {
});
```
-> 前置條件:執行環境須提供必要環境變數,且可連線到 Gitea API。結果:清理符合條件的舊 release 與孤兒 tag,並輸出處理過程 log。
+> 前置條件:執行環境須提供必要環境變數,且可連線到 Gitea API。結果:清理符合條件的舊 release 與孤兒 tag,並以統一的 `[階段][等級][時間]` 日誌輸出處理過程。
diff --git a/app/index.js b/app/index.js
index ab96512..3f66a1d 100644
--- a/app/index.js
+++ b/app/index.js
@@ -2,67 +2,90 @@
'use strict';
/**
- * 在 stdout 輸出一條視覺分隔線(前後各一個換行 + 50 個等號),
- * 用於在 log 中切割不同區段。
+ * 目前流程階段名稱,作為每行 log 的 `[階段]` 前綴使用;
+ * 為空字串時省略整個 `[階段]` 區塊。
+ * @type {string}
+ */
+let currentStage = '';
+
+/**
+ * 設定目前流程階段名稱,之後輸出的 log 會以 `[階段]` 前綴標示所屬階段。
+ * 傳入空字串可清除階段前綴。
*
+ * @param {string} name 階段名稱(顯示於每行 log 開頭的 `[階段]` 區塊)。
* @returns {void}
*/
-function separator() {
- process.stdout.write('\n==================================================\n');
+function stage(name) {
+ currentStage = name;
}
/**
- * 輸出一個區段標題區塊:上緣等號分隔線、標題文字、下緣虛線,
- * 用於在 log 中標示流程進入新階段。
+ * 取得目前 Asia/Taipei 時區的時間字串,格式為 `yyyy/MM/dd HH:mm:ss`。
+ * 使用 Node 內建 Intl(`toLocaleString('sv-SE')` 天生輸出 24 小時制
+ * `YYYY-MM-DD HH:mm:ss`,再把 `-` 換成 `/`),不需額外相依。
*
- * @param {string} title 區段標題文字。
- * @returns {void}
+ * @returns {string} 例如 `2026/06/30 16:53:06`。
*/
-function section(title) {
- separator();
- process.stdout.write(`${title}\n`);
- process.stdout.write('--------------------------------------------------\n');
+function timestamp() {
+ return new Date()
+ .toLocaleString('sv-SE', { timeZone: 'Asia/Taipei' })
+ .replace(/-/g, '/');
}
/**
- * 以 [INFO] 前綴將一般資訊訊息輸出到 stdout。
+ * 依統一格式組出一行 log:`[階段?][等級][時間]: 訊息`。
+ * 階段取自目前 currentStage(為空字串時省略整個 `[階段]` 區塊);
+ * 時間為 Asia/Taipei 的 `yyyy/MM/dd HH:mm:ss`。
+ *
+ * @param {string} level 等級碼(INF/WRN/ERR)。
+ * @param {string} msg 訊息內容。
+ * @returns {string} 組裝後的 log 字串。
+ */
+function formatLog(level, msg) {
+ const stagePart = currentStage ? `[${currentStage}]` : '';
+ return `${stagePart}[${level}][${timestamp()}]: ${msg}`;
+}
+
+/**
+ * 以 `[階段?][INF][時間]:` 前綴將一般資訊訊息輸出到 stdout(時間為 Asia/Taipei)。
*
* @param {string} msg 要輸出的訊息內容。
* @returns {void}
*/
function info(msg) {
- console.log(`[INFO] ${msg}`);
+ console.log(formatLog('INF', msg));
}
/**
- * 以 [OK] 前綴將成功訊息輸出到 stdout。
+ * 以 `[階段?][INF][時間]:` 前綴將成功訊息輸出到 stdout(時間為 Asia/Taipei)。
+ * 規範等級碼無「成功」,故等級採 INF,成功語意保留於訊息文字。
*
* @param {string} msg 要輸出的訊息內容。
* @returns {void}
*/
function success(msg) {
- console.log(`[OK] ${msg}`);
+ console.log(formatLog('INF', msg));
}
/**
- * 以 [WARN] 前綴將警告訊息輸出到 stdout。
+ * 以 `[階段?][WRN][時間]:` 前綴將警告訊息輸出到 stdout(時間為 Asia/Taipei)。
*
* @param {string} msg 要輸出的訊息內容。
* @returns {void}
*/
function warn(msg) {
- console.log(`[WARN] ${msg}`);
+ console.log(formatLog('WRN', msg));
}
/**
- * 以 [ERR] 前綴將錯誤訊息輸出到 stderr。
+ * 以 `[階段?][ERR][時間]:` 前綴將錯誤訊息輸出到 stderr(時間為 Asia/Taipei)。
* 僅負責輸出,不會結束程序(是否離開由呼叫端決定)。
*
* @param {string} msg 要輸出的錯誤訊息內容。
* @returns {void}
*/
function fail(msg) {
- console.error(`[ERR] ${msg}`);
+ console.error(formatLog('ERR', msg));
}
/**
@@ -78,8 +101,8 @@ function isEmptyOrNull(value) {
}
/**
- * 檢查必填值。先以 [INFO] 印出 name=value,
- * 若值被判定為空(見 isEmptyOrNull),印出 [ERR] 並以 exit code 1 結束程序。
+ * 檢查必填值。先以 info([INF])印出 name=value,
+ * 若值被判定為空(見 isEmptyOrNull),以 fail([ERR])輸出並以 exit code 1 結束程序。
*
* @param {string} name 參數名稱(用於 log 與錯誤訊息)。
* @param {*} value 要驗證的參數值。
@@ -143,7 +166,9 @@ async function main() {
const GITEA_TOKEN = process.env.GITEA_TOKEN;
const KEEP_COUNT_RAW = process.env.KEEP_COUNT;
- section('參數檢查');
+ // 於容器啟動橫幅與後續 log 之間留一空行,方便在 CI log 中閱讀
+ process.stdout.write('\n');
+ stage('參數檢查');
requireValue('GITEA_SERVER_URL', GITEA_SERVER_URL);
requireValue('GITEA_REPOSITORY', GITEA_REPOSITORY);
requireValue('KEEP_COUNT', KEEP_COUNT_RAW);
@@ -171,7 +196,9 @@ async function main() {
* @async
* @param {string} baseUrl 不含分頁參數的 API 端點(例如 .../releases 或 .../tags)。
* @returns {Promise>} 所有頁面項目合併後的陣列。
- * @throws 不擲出例外;HTTP 失敗時直接呼叫 process.exit(1) 結束程序。
+ * @throws HTTP 非 2xx 時不擲例外,直接呼叫 process.exit(1) 結束程序;
+ * 但底層 fetch 網路錯誤或回應非 JSON 造成的 res.json() 解析失敗仍會上拋,
+ * 最終由檔尾 main().catch 統一捕捉後 process.exit(1)。
*/
async function fetchAllPages(baseUrl) {
const all = [];
@@ -203,6 +230,7 @@ async function main() {
* @async
* @param {string} url 要刪除之資源的完整 URL(release 或 tag 端點)。
* @returns {Promise} 回應的 HTTP 狀態碼。
+ * @throws 不主動擲例外;但底層 fetch 網路錯誤會上拋,由 main().catch 統一捕捉後 process.exit(1)。
*/
async function deleteUrl(url) {
const res = await fetch(url, { method: 'DELETE', headers: authHeaders });
@@ -211,7 +239,7 @@ async function main() {
const releaseApiUrl = `${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases`;
- section('取得成品資訊');
+ stage('取得舊版本');
info(`GET ${releaseApiUrl}`);
let releases = await fetchAllPages(releaseApiUrl);
@@ -237,7 +265,7 @@ async function main() {
if (releasesToDelete.length === 0) {
success('沒有需要清理的舊版本成品');
} else {
- section('刪除舊版本成品');
+ stage('刪除舊版本');
info(`DELETE_RELEASE_COUNT=${releasesToDelete.length}`);
for (const release of releasesToDelete) {
@@ -262,7 +290,7 @@ async function main() {
}
}
- section('刪除未指定 release 的 tag');
+ stage('刪除舊版本');
// 重新取得 release 清單,得到刪除舊版本後仍指定 tag 的成品
const currentReleases = await fetchAllPages(releaseApiUrl);
@@ -297,8 +325,6 @@ async function main() {
fail(`刪除 tag 失敗: ${tagName}, HTTP ${code}`);
}
}
-
- separator();
}
main().catch((err) => {
diff --git a/entrypoint.sh b/entrypoint.sh
index 0dd682b..84e4957 100755
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -7,7 +7,7 @@
# 中辨識正在執行哪一個 action 及其版本;接著以 exec 啟動 node 主程式
# /app/index.js,並把所有外部傳入的參數原樣交給它。
#
-# 更新日期(台灣時區):2026/06/30 12:02:55
+# 更新日期(台灣時區 Asia/Taipei):2026/07/01 09:14:10
# =============================================================================
# 啟用「遇到任何指令失敗(非 0 結束碼)即立刻中止腳本」的模式。
@@ -19,7 +19,7 @@ ACTION_NAME='Release Cleanup'
# action 用途說明,供下方橫幅輸出使用。
ACTION_DESC='清理舊成品'
# action 更新時間(固定字串,台灣時區),供下方橫幅輸出使用。
-ACTION_UPDATED='2026/06/30 12:02:55'
+ACTION_UPDATED='2026/07/01 09:14:10'
# 輸出一個空行 + 分隔線,作為橫幅的上邊框,讓 log 中的資訊區塊更易閱讀。
printf '\n%s\n' '=================================================='
@@ -36,5 +36,7 @@ printf '%s\n' '=================================================='
# 而非另開子行程。如此可讓 node 成為 PID 1(或直接承接原 shell 的 PID),
# 正確接收容器傳來的訊號(例如 SIGTERM/SIGINT)以利優雅關閉,
# 並讓 node 的結束碼直接成為容器的結束碼。
+# 路徑 /app/index.js 必須與 Dockerfile 的 COPY app/ /app/ 一致:主程式進入點被複製到 /app/ 下,
+# 兩處若不同步(例如改了 WORKDIR 或複製目的地)會導致此處找不到檔案而啟動失敗。
# "$@" 將腳本收到的所有參數原樣(逐一保留分隔,避免被重新分詞)轉交給 node 主程式。
exec node /app/index.js "$@"