From 15381d07a796c08b3bb7bd63b86f900eee9b3b35 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 30 Jun 2026 18:03:10 +0800 Subject: [PATCH 1/6] =?UTF-8?q?docs(release-cleanup):=20=E8=A3=9C=E9=BD=8A?= =?UTF-8?q?=20CI=20workflow=20=E8=A8=BB=E8=A7=A3=E3=80=81=E7=B5=B1?= =?UTF-8?q?=E4=B8=80=E6=97=A5=E8=AA=8C=E6=A0=BC=E5=BC=8F=E4=B8=A6=E9=87=8D?= =?UTF-8?q?=E5=BB=BA=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .gitea/workflows/ci.yaml、cd.yaml:新增用途/更新日期區塊與逐行註解(指令邏輯不變) - app/index.js:日誌統一為 [等級][台灣時間]: 訊息(新增 timestamp helper,INF/WRN/ERR;行為、控制流、exit code、stdout/stderr 皆等價) - README.md:重建專案列表三表/功能列表/使用範例,新增日誌格式說明與 timestamp 功能 Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/cd.yaml | 20 +++++++++++ .gitea/workflows/ci.yaml | 29 +++++++++++++++ README.md | 76 ++++++++++++++++++++++++++++------------ app/index.js | 30 +++++++++++----- 4 files changed, 124 insertions(+), 31 deletions(-) diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 30b1952..f6b4b47 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -1,12 +1,32 @@ +# 用途:本檔為 Gitea CD(持續部署)工作流程設定。 +# 當 master 分支有 push 時自動觸發,使用公司 Gitea 上的 +# composite action「release-tag-version」對建置成品進行釋出與版本標註(tag)。 +# 更新日期(台灣時區 Asia/Taipei):2026/06/30 16:44:09 + +# 工作流程名稱,顯示於 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..e3b2487 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,19 +1,48 @@ +# =================================================================== +# 用途:Gitea CI workflow,於 Pull Request 開啟或更新時觸發 AI 程式碼審查。 +# 透過公司 Gitea 上的 composite action `opencode-code-review`, +# 對 PR 變更內容執行自動化 AI code review,並把審查結果以留言回貼到 PR。 +# 更新日期(台灣時區 Asia/Taipei):2026/06/30 16:44:09 +# =================================================================== + +# workflow 名稱,顯示於 Gitea Actions 介面 name: CI +# 觸發事件設定 on: + # 針對 Pull Request 事件觸發 pull_request: + # 目標分支過濾:忽略以下分支(即 master 不觸發本 workflow) branches-ignore: + # 排除 master 分支,避免對正式主線的 PR 執行 AI 審查 - master + # 僅在 PR 被「開啟(opened)」或「推送新 commit 同步(synchronize)」時觸發 types: [opened, synchronize] +# 定義所有 jobs jobs: + # job 識別碼:AI 程式碼審查 ai-code-review: + # job 顯示名稱 name: AI Code Review + # 指定執行環境(runner 標籤),使用 ubuntu runner runs-on: ubuntu + # 此 job 對 GITHUB_TOKEN 所需的權限範圍 permissions: + # 允許寫入 repository 內容(供 action 讀取/操作程式碼) contents: write + # 允許寫入 Pull Request(供 action 回貼審查留言) pull-requests: write + # 允許寫入 issues(PR 留言底層走 issue comment API,需此權限) issues: write + # 此 job 的執行步驟 steps: + # 步驟名稱:使用 OpenCode 進行 AI 程式碼審查 - name: AI 程式碼審查 by OpenCode + # 引用公司 Gitea 上的 composite action `opencode-code-review`, + # 版本由 repository/organization 變數 ACTION_OPENCODE_CODE_REVIEW_VERSION 指定, + # 便於集中控管所使用的 action 版本 uses: https://gitea.jsc.idv.tw/composite-actions/opencode-code-review@${{ vars.ACTION_OPENCODE_CODE_REVIEW_VERSION }} + # 傳遞給 composite action 的輸入參數 with: + # 留言用 token,從 secrets.COMMENT_TOKEN 取得, + # 供 action 以該身分將審查結果回貼到 PR 留言 comment_token: ${{ secrets.COMMENT_TOKEN }} diff --git a/README.md b/README.md index c229e66..9255268 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Release Cleanup -> 更新時間:2026/06/30 12:02:55(台灣時區 Asia/Taipei) +> 更新時間:2026/06/30 17:58:12(台灣時區 Asia/Taipei) 清理舊成品的 **Gitea / GitHub docker container action**。依保留數量清理 repo 的舊 release,並刪除沒有對應 release 的 tag;正式版與 beta 版各自獨立保留指定數量。 @@ -16,6 +16,18 @@ `GITEA_TOKEN` 為空時以匿名身分呼叫 API。取得分頁失敗或參數不合法則直接以 exit code 1 結束程序。 +## 日誌輸出格式 + +所有等級式輸出統一為 `[{等級}][{時間}]: {訊息}`(等級為 `INF`/`WRN`/`ERR`,時間為 Asia/Taipei 的 `yyyy/MM/dd HH:mm:ss`);`fail` 走 stderr,其餘走 stdout。例如: + +```text +[INF][2026/06/30 17:58:12]: RELEASE_COUNT=42 +[WRN][2026/06/30 17:58:12]: GITEA_TOKEN is empty; release API calls will be anonymous +[ERR][2026/06/30 17:58:12]: 刪除失敗: v1.0.0 (Release 1.0.0), HTTP 500 +``` + +> 註:`separator` / `section` 為純分隔線/標題輸出,不帶等級與時間戳。 + ## Action 輸入與環境變數 | 名稱 | 來源 | 必填 | 預設 | 說明 | @@ -44,7 +56,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,19 +76,22 @@ jobs: ### release-cleanup +> 說明:本專案為單一 Node 模組(`app/index.js`),下列為模組內可文件化的頂層函式(不含 `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) | +| [timestamp](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L34) | [取得 Asia/Taipei 時間字串](#timestamp) | +| [info](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L46) | [以 \[INF\] 等級輸出資訊訊息](#info) | +| [success](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L57) | [以 \[INF\] 等級輸出成功訊息](#success) | +| [warn](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L67) | [以 \[WRN\] 等級輸出警告訊息](#warn) | +| [fail](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L78) | [以 \[ERR\] 等級輸出錯誤訊息到 stderr](#fail) | +| [isEmptyOrNull](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L90) | [判斷值是否視為空 / 未設定](#isemptyornull) | +| [requireValue](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L103) | [檢查必填值,空值時結束程序](#requirevalue) | +| [requireInteger](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L120) | [驗證非負整數,不符時結束程序](#requireinteger) | +| [isBeta](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L134) | [判斷名稱是否為 beta 版本](#isbeta) | +| [main](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L154) | [Action 主流程:清理舊 release 與孤兒 tag](#main) | ## 使用範例 @@ -105,43 +120,58 @@ section('參數檢查'); // -------------------------------------------------- ``` + +### timestamp + +取得目前 **Asia/Taipei** 時區的時間字串,格式為 `yyyy/MM/dd HH:mm:ss`。使用 Node 內建 `Intl`(`toLocaleString('sv-SE')` 輸出 24 小時制再把 `-` 換成 `/`),不需額外相依。供各等級式 log helper 組裝統一格式前綴使用。 + +```js +timestamp(); +// 例如回傳: '2026/06/30 17:58:12' +info(`RELEASE_COUNT=42`); +// 內部即以 timestamp() 組出: [INF][2026/06/30 17:58:12]: RELEASE_COUNT=42 +``` + +> 前置條件:執行環境須支援 `Intl`(Node 18+ 內建)。結果:回傳當下台灣時間字串,無副作用。 + ### info -以 `[INFO]` 前綴將一般資訊訊息輸出到 stdout(`console.log`)。 +以 `[INF][時間]:` 前綴將一般資訊訊息輸出到 stdout(`console.log`,時間為 Asia/Taipei)。 ```js info('RELEASE_COUNT=10'); -// 輸出:[INFO] RELEASE_COUNT=10 +// 輸出:[INF][2026/06/30 17:58:12]: 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) +// 輸出:[INF][2026/06/30 17:58:12]: 成功刪除: v1.0.0 (Release 1.0.0) ``` ### warn -以 `[WARN]` 前綴將警告訊息輸出到 stdout(非致命情況,程式會繼續執行)。 +以 `[WRN][時間]:` 前綴將警告訊息輸出到 stdout(非致命情況,程式會繼續執行)。 ```js warn('GITEA_TOKEN is empty; release API calls will be anonymous'); -// 輸出:[WARN] GITEA_TOKEN is empty; release API calls will be anonymous +// 輸出:[WRN][2026/06/30 17:58:12]: 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'); +// 輸出(stderr):[ERR][2026/06/30 17:58:12]: 刪除失敗: v0.1.0 (Release 0.1.0), HTTP 500 process.exit(1); // 終止由呼叫端負責 ``` @@ -159,11 +189,11 @@ isEmptyOrNull('abc'); // false ### requireValue -檢查必填值。先以 `[INFO]` 印出 `name=value`,若值被判定為空則印出 `[ERR]` 並以 exit code 1 結束程序。 +檢查必填值。先以 `[INF]` 印出 `name=value`,若值被判定為空則以 `[ERR]` 輸出並以 exit code 1 結束程序。 ```js 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,11 +201,11 @@ requireValue('GITEA_SERVER_URL', process.env.GITEA_SERVER_URL); ### requireInteger -驗證值是否為非負整數字串(正則 `/^[0-9]+$/`,允許前導零、不允許負號或小數),不符合時印出 `[ERR]` 並以 exit code 1 結束程序。 +驗證值是否為非負整數字串(正則 `/^[0-9]+$/`,允許前導零、不允許負號或小數),不符合時以 `[ERR]` 輸出並以 exit code 1 結束程序。 ```js requireInteger('KEEP_COUNT', '2'); // 通過 -requireInteger('KEEP_COUNT', '-1'); // 輸出 [ERR] ... 並 process.exit(1) +requireInteger('KEEP_COUNT', '-1'); // 輸出 [ERR][時間]: ... 並 process.exit(1) ``` @@ -204,4 +234,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..83dc448 100644 --- a/app/index.js +++ b/app/index.js @@ -25,44 +25,58 @@ function section(title) { } /** - * 以 [INFO] 前綴將一般資訊訊息輸出到 stdout。 + * 取得目前 Asia/Taipei 時區的時間字串,格式為 `yyyy/MM/dd HH:mm:ss`。 + * 使用 Node 內建 Intl(`toLocaleString('sv-SE')` 天生輸出 24 小時制 + * `YYYY-MM-DD HH:mm:ss`,再把 `-` 換成 `/`),不需額外相依。 + * + * @returns {string} 例如 `2026/06/30 16:53:06`。 + */ +function timestamp() { + return new Date() + .toLocaleString('sv-SE', { timeZone: 'Asia/Taipei' }) + .replace(/-/g, '/'); +} + +/** + * 以 `[INF][時間]:` 前綴將一般資訊訊息輸出到 stdout(時間為 Asia/Taipei)。 * * @param {string} msg 要輸出的訊息內容。 * @returns {void} */ function info(msg) { - console.log(`[INFO] ${msg}`); + console.log(`[INF][${timestamp()}]: ${msg}`); } /** - * 以 [OK] 前綴將成功訊息輸出到 stdout。 + * 以 `[INF][時間]:` 前綴將成功訊息輸出到 stdout(時間為 Asia/Taipei)。 + * 規範等級碼無「成功」,故等級採 INF,成功語意保留於訊息文字。 * * @param {string} msg 要輸出的訊息內容。 * @returns {void} */ function success(msg) { - console.log(`[OK] ${msg}`); + console.log(`[INF][${timestamp()}]: ${msg}`); } /** - * 以 [WARN] 前綴將警告訊息輸出到 stdout。 + * 以 `[WRN][時間]:` 前綴將警告訊息輸出到 stdout(時間為 Asia/Taipei)。 * * @param {string} msg 要輸出的訊息內容。 * @returns {void} */ function warn(msg) { - console.log(`[WARN] ${msg}`); + console.log(`[WRN][${timestamp()}]: ${msg}`); } /** - * 以 [ERR] 前綴將錯誤訊息輸出到 stderr。 + * 以 `[ERR][時間]:` 前綴將錯誤訊息輸出到 stderr(時間為 Asia/Taipei)。 * 僅負責輸出,不會結束程序(是否離開由呼叫端決定)。 * * @param {string} msg 要輸出的錯誤訊息內容。 * @returns {void} */ function fail(msg) { - console.error(`[ERR] ${msg}`); + console.error(`[ERR][${timestamp()}]: ${msg}`); } /** From a9b9f95a7224d2483901e7c75ea7ce4a4ec74b58 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 30 Jun 2026 18:18:24 +0800 Subject: [PATCH 2/6] =?UTF-8?q?ci(release-cleanup):=20ci.yaml=20=E6=94=B9?= =?UTF-8?q?=E7=82=BA=20PR=20=E8=A7=B8=E7=99=BC=20beta=20=E6=A8=99=E7=89=88?= =?UTF-8?q?=20+=20=E8=87=AA=E6=88=91=E6=B8=85=E7=90=86=E7=AE=A1=E7=B7=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - release-tag-version job:以 is_beta: true 標註 beta 版並輸出 version - release-cleanup job:needs 前者,於該 beta 版本上執行 release-cleanup action(dogfooding 自我清理) Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/ci.yaml | 51 +++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index e3b2487..0387c72 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -19,30 +19,33 @@ on: types: [opened, synchronize] # 定義所有 jobs jobs: - # job 識別碼:AI 程式碼審查 - ai-code-review: - # job 顯示名稱 - name: AI Code Review - # 指定執行環境(runner 標籤),使用 ubuntu runner + # 第一個 job: 釋出並標註成品版本,並將版本號往外拋給後續 job 使用。 + release-tag-version: + # job 顯示名稱。 + name: Release Tag Version + # 指定執行環境 (runner) 標籤為 ubuntu。 runs-on: ubuntu - # 此 job 對 GITHUB_TOKEN 所需的權限範圍 - permissions: - # 允許寫入 repository 內容(供 action 讀取/操作程式碼) - contents: write - # 允許寫入 Pull Request(供 action 回貼審查留言) - pull-requests: write - # 允許寫入 issues(PR 留言底層走 issue comment API,需此權限) - issues: write - # 此 job 的執行步驟 + # 宣告此 job 的輸出,供後續 needs 此 job 的其他 job 取用。 + outputs: + # 將下方 id 為 release-tag-version 的 step 所輸出的 version,設為此 job 的對外 version 輸出 (關鍵點: 透過 outputs 把標註出的版本往外拋)。 + version: ${{ steps.release-tag-version.outputs.version }} + # 此 job 依序執行的步驟。 steps: - # 步驟名稱:使用 OpenCode 進行 AI 程式碼審查 - - name: AI 程式碼審查 by OpenCode - # 引用公司 Gitea 上的 composite action `opencode-code-review`, - # 版本由 repository/organization 變數 ACTION_OPENCODE_CODE_REVIEW_VERSION 指定, - # 便於集中控管所使用的 action 版本 - uses: https://gitea.jsc.idv.tw/composite-actions/opencode-code-review@${{ vars.ACTION_OPENCODE_CODE_REVIEW_VERSION }} - # 傳遞給 composite action 的輸入參數 + # 步驟: 呼叫 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: - # 留言用 token,從 secrets.COMMENT_TOKEN 取得, - # 供 action 以該身分將審查結果回貼到 PR 留言 - comment_token: ${{ secrets.COMMENT_TOKEN }} + # is_beta 設為 true 表示走 beta 標版流程 (標註的是 beta 成品版本,而非正式版)。 + is_beta: true + release-cleanup: + name: Release Cleanup + runs-on: ubuntu + needs: release-tag-version + steps: + - name: 清理舊成品 + uses: https://gitea.jsc.idv.tw/docker-actions/release-cleanup@v${{ needs.release-tag-version.outputs.version }} + From 9d8cbb6f1c2e0377d7322faa083963f9ed036f12 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 30 Jun 2026 18:29:03 +0800 Subject: [PATCH 3/6] =?UTF-8?q?docs(release-cleanup):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20ci.yaml=20=E9=81=8E=E6=99=82=E7=94=A8=E9=80=94=E8=A8=BB?= =?UTF-8?q?=E8=A7=A3=E4=B8=A6=E6=9B=B4=E6=96=B0=20entrypoint.sh=20?= =?UTF-8?q?=E7=94=A2=E7=94=9F=E6=99=82=E9=96=93=E6=88=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci.yaml:開頭用途註解改為描述實際的 release-tag-version(beta) + release-cleanup 自我清理管線(邏輯不變) - entrypoint.sh:更新產生時間戳為 2026/06/30 18:19:59 Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/ci.yaml | 18 ++++++++++++------ entrypoint.sh | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 0387c72..a577c47 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,8 +1,8 @@ # =================================================================== -# 用途:Gitea CI workflow,於 Pull Request 開啟或更新時觸發 AI 程式碼審查。 -# 透過公司 Gitea 上的 composite action `opencode-code-review`, -# 對 PR 變更內容執行自動化 AI code review,並把審查結果以留言回貼到 PR。 -# 更新日期(台灣時區 Asia/Taipei):2026/06/30 16:44:09 +# 用途: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/06/30 18:19:59 # =================================================================== # workflow 名稱,顯示於 Gitea Actions 介面 @@ -13,7 +13,7 @@ on: pull_request: # 目標分支過濾:忽略以下分支(即 master 不觸發本 workflow) branches-ignore: - # 排除 master 分支,避免對正式主線的 PR 執行 AI 審查 + # 排除 master 分支,避免對正式主線的 PR 執行本流程 - master # 僅在 PR 被「開啟(opened)」或「推送新 commit 同步(synchronize)」時觸發 types: [opened, synchronize] @@ -41,11 +41,17 @@ jobs: with: # 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/entrypoint.sh b/entrypoint.sh index 0dd682b..a6046a8 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -7,7 +7,7 @@ # 中辨識正在執行哪一個 action 及其版本;接著以 exec 啟動 node 主程式 # /app/index.js,並把所有外部傳入的參數原樣交給它。 # -# 更新日期(台灣時區):2026/06/30 12:02:55 +# 更新日期(台灣時區):2026/06/30 18:19:59 # ============================================================================= # 啟用「遇到任何指令失敗(非 0 結束碼)即立刻中止腳本」的模式。 @@ -19,7 +19,7 @@ ACTION_NAME='Release Cleanup' # action 用途說明,供下方橫幅輸出使用。 ACTION_DESC='清理舊成品' # action 更新時間(固定字串,台灣時區),供下方橫幅輸出使用。 -ACTION_UPDATED='2026/06/30 12:02:55' +ACTION_UPDATED='2026/06/30 18:19:59' # 輸出一個空行 + 分隔線,作為橫幅的上邊框,讓 log 中的資訊區塊更易閱讀。 printf '\n%s\n' '==================================================' From c37b33f397223e674640c468241afcac39d5e2ce Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 1 Jul 2026 09:07:05 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix(release-cleanup):=20=E6=97=A5=E8=AA=8C?= =?UTF-8?q?=E6=94=B9=E7=82=BA=20[=E9=9A=8E=E6=AE=B5][=E7=AD=89=E7=B4=9A][?= =?UTF-8?q?=E6=99=82=E9=96=93]=20=E6=A0=BC=E5=BC=8F=E4=B8=A6=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=8D=80=E6=AE=B5=E5=88=86=E9=9A=94=E7=B7=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 stage() 設定目前階段、formatLog() 共用組裝,log 統一為 [階段][等級][時間]: 訊息 - 階段對應 參數檢查/取得舊版本/刪除舊版本;移除 separator()/section() 的 ==== / ---- 分隔區塊 - 行為、控制流、exit code 不變;fail 仍走 stderr、其餘走 stdout Co-Authored-By: Claude Opus 4.8 (1M context) --- app/index.js | 69 +++++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/app/index.js b/app/index.js index 83dc448..eca265b 100644 --- a/app/index.js +++ b/app/index.js @@ -2,26 +2,21 @@ 'use strict'; /** - * 在 stdout 輸出一條視覺分隔線(前後各一個換行 + 50 個等號), - * 用於在 log 中切割不同區段。 - * - * @returns {void} + * 目前流程階段名稱,作為每行 log 的 `[階段]` 前綴使用; + * 為空字串時省略整個 `[階段]` 區塊。 + * @type {string} */ -function separator() { - process.stdout.write('\n==================================================\n'); -} +let currentStage = ''; /** - * 輸出一個區段標題區塊:上緣等號分隔線、標題文字、下緣虛線, - * 用於在 log 中標示流程進入新階段。 + * 設定目前流程階段名稱,之後輸出的 log 會以 `[階段]` 前綴標示所屬階段。 + * 傳入空字串可清除階段前綴。 * - * @param {string} title 區段標題文字。 + * @param {string} name 階段名稱(顯示於每行 log 開頭的 `[階段]` 區塊)。 * @returns {void} */ -function section(title) { - separator(); - process.stdout.write(`${title}\n`); - process.stdout.write('--------------------------------------------------\n'); +function stage(name) { + currentStage = name; } /** @@ -38,45 +33,59 @@ function timestamp() { } /** - * 以 `[INF][時間]:` 前綴將一般資訊訊息輸出到 stdout(時間為 Asia/Taipei)。 + * 依統一格式組出一行 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(`[INF][${timestamp()}]: ${msg}`); + console.log(formatLog('INF', msg)); } /** - * 以 `[INF][時間]:` 前綴將成功訊息輸出到 stdout(時間為 Asia/Taipei)。 + * 以 `[階段?][INF][時間]:` 前綴將成功訊息輸出到 stdout(時間為 Asia/Taipei)。 * 規範等級碼無「成功」,故等級採 INF,成功語意保留於訊息文字。 * * @param {string} msg 要輸出的訊息內容。 * @returns {void} */ function success(msg) { - console.log(`[INF][${timestamp()}]: ${msg}`); + console.log(formatLog('INF', msg)); } /** - * 以 `[WRN][時間]:` 前綴將警告訊息輸出到 stdout(時間為 Asia/Taipei)。 + * 以 `[階段?][WRN][時間]:` 前綴將警告訊息輸出到 stdout(時間為 Asia/Taipei)。 * * @param {string} msg 要輸出的訊息內容。 * @returns {void} */ function warn(msg) { - console.log(`[WRN][${timestamp()}]: ${msg}`); + console.log(formatLog('WRN', msg)); } /** - * 以 `[ERR][時間]:` 前綴將錯誤訊息輸出到 stderr(時間為 Asia/Taipei)。 + * 以 `[階段?][ERR][時間]:` 前綴將錯誤訊息輸出到 stderr(時間為 Asia/Taipei)。 * 僅負責輸出,不會結束程序(是否離開由呼叫端決定)。 * * @param {string} msg 要輸出的錯誤訊息內容。 * @returns {void} */ function fail(msg) { - console.error(`[ERR][${timestamp()}]: ${msg}`); + console.error(formatLog('ERR', msg)); } /** @@ -92,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 要驗證的參數值。 @@ -157,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); @@ -225,7 +236,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); @@ -251,7 +262,7 @@ async function main() { if (releasesToDelete.length === 0) { success('沒有需要清理的舊版本成品'); } else { - section('刪除舊版本成品'); + stage('刪除舊版本'); info(`DELETE_RELEASE_COUNT=${releasesToDelete.length}`); for (const release of releasesToDelete) { @@ -276,7 +287,7 @@ async function main() { } } - section('刪除未指定 release 的 tag'); + stage('刪除舊版本'); // 重新取得 release 清單,得到刪除舊版本後仍指定 tag 的成品 const currentReleases = await fetchAllPages(releaseApiUrl); @@ -311,8 +322,6 @@ async function main() { fail(`刪除 tag 失敗: ${tagName}, HTTP ${code}`); } } - - separator(); } main().catch((err) => { From fcf6b0838bfd2312310e55a34571d991ea66b220 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 1 Jul 2026 09:07:05 +0800 Subject: [PATCH 5/6] =?UTF-8?q?docs(README):=20=E9=85=8D=E5=90=88=E6=97=A5?= =?UTF-8?q?=E8=AA=8C=E6=A0=BC=E5=BC=8F=E8=AA=BF=E6=95=B4=E9=87=8D=E5=BB=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=88=97=E8=A1=A8=E8=88=87=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=AF=84=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 separator/section、新增 stage/formatLog 條目與錨點 - 更新「日誌輸出格式」章節與各範例為 [階段][等級][時間],同步函式起始行號 Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 129 +++++++++++++++++++++++++++++------------------------- 1 file changed, 69 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 9255268..e1b77a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Release Cleanup -> 更新時間:2026/06/30 17:58:12(台灣時區 Asia/Taipei) +> 更新時間:2026/07/01 09:00:53(台灣時區 Asia/Taipei) 清理舊成品的 **Gitea / GitHub docker container action**。依保留數量清理 repo 的舊 release,並刪除沒有對應 release 的 tag;正式版與 beta 版各自獨立保留指定數量。 @@ -10,23 +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 結束程序。 ## 日誌輸出格式 -所有等級式輸出統一為 `[{等級}][{時間}]: {訊息}`(等級為 `INF`/`WRN`/`ERR`,時間為 Asia/Taipei 的 `yyyy/MM/dd HH:mm:ss`);`fail` 走 stderr,其餘走 stdout。例如: +所有等級式輸出統一為 `[{階段}][{等級}][{時間}]: {訊息}`: + +- `階段`:目前流程階段(`參數檢查` / `取得舊版本` / `刪除舊版本`),由 `stage()` 設定;未設定時省略整個 `[階段]` 區塊。 +- `等級`:`INF`(含成功訊息)/ `WRN` / `ERR`。 +- `時間`:Asia/Taipei 的 `yyyy/MM/dd HH:mm:ss`。 +- `fail` 走 stderr,其餘走 stdout。 + +範例: ```text -[INF][2026/06/30 17:58:12]: RELEASE_COUNT=42 -[WRN][2026/06/30 17:58:12]: GITEA_TOKEN is empty; release API calls will be anonymous -[ERR][2026/06/30 17:58:12]: 刪除失敗: v1.0.0 (Release 1.0.0), HTTP 500 +[參數檢查][INF][2026/07/01 09:00:53]: GITEA_SERVER_URL=https://gitea.jsc.idv.tw +[取得舊版本][INF][2026/07/01 09:00:53]: RELEASE_COUNT=3 +[取得舊版本][INF][2026/07/01 09:00:53]: 沒有需要清理的舊版本成品 +[刪除舊版本][ERR][2026/07/01 09:00:53]: 刪除 tag 失敗: v0.0.1, HTTP 500 ``` -> 註:`separator` / `section` 為純分隔線/標題輸出,不帶等級與時間戳。 +> 容器啟動橫幅(名稱/用途/更新時間)由 `entrypoint.sh` 輸出,不帶等級與階段前綴。 ## Action 輸入與環境變數 @@ -56,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 版各自保留指定數量,並以統一的 `[階段][等級][時間]` 日誌輸出處理過程 | ### 參考專案 @@ -80,98 +87,99 @@ jobs: | 功能名稱 | 功能描述 | | --- | --- | -| [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) | -| [timestamp](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L34) | [取得 Asia/Taipei 時間字串](#timestamp) | -| [info](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L46) | [以 \[INF\] 等級輸出資訊訊息](#info) | -| [success](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L57) | [以 \[INF\] 等級輸出成功訊息](#success) | -| [warn](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L67) | [以 \[WRN\] 等級輸出警告訊息](#warn) | -| [fail](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L78) | [以 \[ERR\] 等級輸出錯誤訊息到 stderr](#fail) | -| [isEmptyOrNull](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L90) | [判斷值是否視為空 / 未設定](#isemptyornull) | -| [requireValue](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L103) | [檢查必填值,空值時結束程序](#requirevalue) | -| [requireInteger](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L120) | [驗證非負整數,不符時結束程序](#requireinteger) | -| [isBeta](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L134) | [判斷名稱是否為 beta 版本](#isbeta) | -| [main](https://gitea.jsc.idv.tw/docker-actions/release-cleanup/src/branch/develop/app/index.js#L154) | [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:00:53]: RELEASE_COUNT=3 ``` - -### section - -輸出一個區段標題區塊:上緣等號分隔線、標題文字、下緣虛線,用於標示流程進入新階段。 - -```js -section('參數檢查'); -// 輸出: -// ================================================== -// 參數檢查 -// -------------------------------------------------- -``` +> 前置條件:無。結果:更新模組層級的目前階段字串;本身不輸出任何內容。 ### timestamp -取得目前 **Asia/Taipei** 時區的時間字串,格式為 `yyyy/MM/dd HH:mm:ss`。使用 Node 內建 `Intl`(`toLocaleString('sv-SE')` 輸出 24 小時制再把 `-` 換成 `/`),不需額外相依。供各等級式 log helper 組裝統一格式前綴使用。 +取得目前 **Asia/Taipei** 時區的時間字串,格式為 `yyyy/MM/dd HH:mm:ss`。使用 Node 內建 `Intl`(`toLocaleString('sv-SE')` 輸出 24 小時制再把 `-` 換成 `/`),不需額外相依。供 `formatLog` 組裝統一格式前綴使用。 ```js timestamp(); -// 例如回傳: '2026/06/30 17:58:12' -info(`RELEASE_COUNT=42`); -// 內部即以 timestamp() 組出: [INF][2026/06/30 17:58:12]: RELEASE_COUNT=42 +// 例如回傳: '2026/07/01 09:00:53' ``` > 前置條件:執行環境須支援 `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:00:53]: 刪除 tag 失敗: v0.0.1, HTTP 500' +``` + ### info -以 `[INF][時間]:` 前綴將一般資訊訊息輸出到 stdout(`console.log`,時間為 Asia/Taipei)。 +以 `[階段?][INF][時間]:` 前綴將一般資訊訊息輸出到 stdout(`console.log`)。 ```js +stage('取得舊版本'); info('RELEASE_COUNT=10'); -// 輸出:[INF][2026/06/30 17:58:12]: RELEASE_COUNT=10 +// 輸出:[取得舊版本][INF][2026/07/01 09:00:53]: RELEASE_COUNT=10 ``` ### success -以 `[INF][時間]:` 前綴將成功訊息輸出到 stdout。規範等級碼無「成功」一項,故等級採 `INF`,成功語意保留於訊息文字。 +以 `[階段?][INF][時間]:` 前綴將成功訊息輸出到 stdout。規範等級碼無「成功」一項,故等級採 `INF`,成功語意保留於訊息文字。 ```js -success('成功刪除: v1.0.0 (Release 1.0.0)'); -// 輸出:[INF][2026/06/30 17:58:12]: 成功刪除: v1.0.0 (Release 1.0.0) +stage('取得舊版本'); +success('沒有需要清理的舊版本成品'); +// 輸出:[取得舊版本][INF][2026/07/01 09:00:53]: 沒有需要清理的舊版本成品 ``` ### warn -以 `[WRN][時間]:` 前綴將警告訊息輸出到 stdout(非致命情況,程式會繼續執行)。 +以 `[階段?][WRN][時間]:` 前綴將警告訊息輸出到 stdout(非致命情況,程式會繼續執行)。 ```js +stage('參數檢查'); warn('GITEA_TOKEN is empty; release API calls will be anonymous'); -// 輸出:[WRN][2026/06/30 17:58:12]: GITEA_TOKEN is empty; release API calls will be anonymous +// 輸出:[參數檢查][WRN][2026/07/01 09:00:53]: 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'); -// 輸出(stderr):[ERR][2026/06/30 17:58:12]: 刪除失敗: v0.1.0 (Release 0.1.0), HTTP 500 +stage('刪除舊版本'); +fail('刪除 tag 失敗: v0.0.1, HTTP 500'); +// 輸出(stderr):[刪除舊版本][ERR][2026/07/01 09:00:53]: 刪除 tag 失敗: v0.0.1, HTTP 500 process.exit(1); // 終止由呼叫端負責 ``` @@ -189,11 +197,12 @@ isEmptyOrNull('abc'); // false ### requireValue -檢查必填值。先以 `[INF]` 印出 `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 方式輸出)。 @@ -205,7 +214,7 @@ requireValue('GITEA_SERVER_URL', process.env.GITEA_SERVER_URL); ```js requireInteger('KEEP_COUNT', '2'); // 通過 -requireInteger('KEEP_COUNT', '-1'); // 輸出 [ERR][時間]: ... 並 process.exit(1) +requireInteger('KEEP_COUNT', '-1'); // 輸出 [ERR] 訊息並 process.exit(1) ``` @@ -222,7 +231,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 @@ -234,4 +243,4 @@ main().catch((err) => { }); ``` -> 前置條件:執行環境須提供必要環境變數,且可連線到 Gitea API。結果:清理符合條件的舊 release 與孤兒 tag,並以統一等級式日誌輸出處理過程。 +> 前置條件:執行環境須提供必要環境變數,且可連線到 Gitea API。結果:清理符合條件的舊 release 與孤兒 tag,並以統一的 `[階段][等級][時間]` 日誌輸出處理過程。 From a4fa7fec85f8161dd41360a8a249a7d681f65dd2 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 1 Jul 2026 09:28:03 +0800 Subject: [PATCH 6/6] =?UTF-8?q?docs(release-cleanup):=20=E6=A0=A1=E6=AD=A3?= =?UTF-8?q?=20JSDoc=20=E4=BE=8B=E5=A4=96=E8=AA=AA=E6=98=8E=E3=80=81?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E6=8C=87=E4=BB=A4=E6=AA=94=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=99=82=E9=96=93=E4=B8=A6=E9=87=8D=E5=BB=BA=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/cd.yaml | 4 +++- .gitea/workflows/ci.yaml | 2 +- Dockerfile | 4 ++-- README.md | 37 +++++++++++++++++++------------------ app/index.js | 5 ++++- entrypoint.sh | 6 ++++-- 6 files changed, 33 insertions(+), 25 deletions(-) diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index f6b4b47..3509416 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -1,7 +1,9 @@ +# =================================================================== # 用途:本檔為 Gitea CD(持續部署)工作流程設定。 # 當 master 分支有 push 時自動觸發,使用公司 Gitea 上的 # composite action「release-tag-version」對建置成品進行釋出與版本標註(tag)。 -# 更新日期(台灣時區 Asia/Taipei):2026/06/30 16:44:09 +# 更新日期(台灣時區 Asia/Taipei):2026/07/01 09:14:10 +# =================================================================== # 工作流程名稱,顯示於 Gitea Actions 介面,用以辨識此 CD 流程 name: CD diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index a577c47..86d4dfe 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -2,7 +2,7 @@ # 用途: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/06/30 18:19:59 +# 更新日期(台灣時區 Asia/Taipei):2026/07/01 09:14:10 # =================================================================== # workflow 名稱,顯示於 Gitea Actions 介面 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 e1b77a6..da84580 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Release Cleanup -> 更新時間:2026/07/01 09:00:53(台灣時區 Asia/Taipei) +> 更新時間:2026/07/01 09:20:52(台灣時區 Asia/Taipei) 清理舊成品的 **Gitea / GitHub docker container action**。依保留數量清理 repo 的舊 release,並刪除沒有對應 release 的 tag;正式版與 beta 版各自獨立保留指定數量。 @@ -13,7 +13,7 @@ 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 結束。 ## 日誌輸出格式 @@ -27,10 +27,10 @@ 範例: ```text -[參數檢查][INF][2026/07/01 09:00:53]: GITEA_SERVER_URL=https://gitea.jsc.idv.tw -[取得舊版本][INF][2026/07/01 09:00:53]: RELEASE_COUNT=3 -[取得舊版本][INF][2026/07/01 09:00:53]: 沒有需要清理的舊版本成品 -[刪除舊版本][ERR][2026/07/01 09:00:53]: 刪除 tag 失敗: v0.0.1, HTTP 500 +[參數檢查][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` 輸出,不帶等級與階段前綴。 @@ -83,7 +83,7 @@ jobs: ### release-cleanup -> 說明:本專案為單一 Node 模組(`app/index.js`),下列為模組內可文件化的頂層函式(不含 `main` 內部的巢狀私有函式 `fetchAllPages` / `deleteUrl`)。 +> 說明:本專案為單一 Node 模組(`app/index.js`),未 `module.exports` 任何函式,下列為模組內可文件化的頂層函式(不含 `main` 內部的巢狀私有函式 `fetchAllPages` / `deleteUrl`)。 | 功能名稱 | 功能描述 | | --- | --- | @@ -110,7 +110,7 @@ jobs: ```js stage('取得舊版本'); info('RELEASE_COUNT=3'); -// 輸出:[取得舊版本][INF][2026/07/01 09:00:53]: RELEASE_COUNT=3 +// 輸出:[取得舊版本][INF][2026/07/01 09:20:52]: RELEASE_COUNT=3 ``` > 前置條件:無。結果:更新模組層級的目前階段字串;本身不輸出任何內容。 @@ -122,7 +122,7 @@ info('RELEASE_COUNT=3'); ```js timestamp(); -// 例如回傳: '2026/07/01 09:00:53' +// 例如回傳: '2026/07/01 09:20:52' ``` > 前置條件:執行環境須支援 `Intl`(Node 18+ 內建)。結果:回傳當下台灣時間字串,無副作用。 @@ -135,7 +135,7 @@ timestamp(); ```js stage('刪除舊版本'); formatLog('ERR', '刪除 tag 失敗: v0.0.1, HTTP 500'); -// 回傳: '[刪除舊版本][ERR][2026/07/01 09:00:53]: 刪除 tag 失敗: v0.0.1, HTTP 500' +// 回傳: '[刪除舊版本][ERR][2026/07/01 09:20:52]: 刪除 tag 失敗: v0.0.1, HTTP 500' ``` @@ -146,7 +146,7 @@ formatLog('ERR', '刪除 tag 失敗: v0.0.1, HTTP 500'); ```js stage('取得舊版本'); info('RELEASE_COUNT=10'); -// 輸出:[取得舊版本][INF][2026/07/01 09:00:53]: RELEASE_COUNT=10 +// 輸出:[取得舊版本][INF][2026/07/01 09:20:52]: RELEASE_COUNT=10 ``` @@ -157,7 +157,7 @@ info('RELEASE_COUNT=10'); ```js stage('取得舊版本'); success('沒有需要清理的舊版本成品'); -// 輸出:[取得舊版本][INF][2026/07/01 09:00:53]: 沒有需要清理的舊版本成品 +// 輸出:[取得舊版本][INF][2026/07/01 09:20:52]: 沒有需要清理的舊版本成品 ``` @@ -168,7 +168,7 @@ success('沒有需要清理的舊版本成品'); ```js stage('參數檢查'); warn('GITEA_TOKEN is empty; release API calls will be anonymous'); -// 輸出:[參數檢查][WRN][2026/07/01 09:00:53]: 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 ``` @@ -179,19 +179,20 @@ warn('GITEA_TOKEN is empty; release API calls will be anonymous'); ```js stage('刪除舊版本'); fail('刪除 tag 失敗: v0.0.1, HTTP 500'); -// 輸出(stderr):[刪除舊版本][ERR][2026/07/01 09:00:53]: 刪除 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(非空字串) ``` @@ -210,7 +211,7 @@ 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'); // 通過 @@ -220,7 +221,7 @@ 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 @@ -231,7 +232,7 @@ isBeta('v1.2.3-beta'); // false(無結尾的點,不符合 -beta.) ### main -Action 主流程(`async`)。讀取環境變數後依序執行參數檢查、取得 release、刪除舊版本(正式版與 beta 版各自保留 `KEEP_COUNT` 筆)、刪除未指定 release 的 tag;各階段以 `stage()` 標示,log 皆帶 `[階段]` 前綴。由檔尾 `main().catch(...)` 立即執行。 +Action 主流程(`async`)。讀取環境變數後依序執行參數檢查、取得 release、刪除舊版本(正式版與 beta 版各自保留 `KEEP_COUNT` 筆)、刪除未指定 release 的 tag;各階段以 `stage()` 標示,log 皆帶 `[階段]` 前綴。由檔尾 `main().catch(...)` 立即執行並收斂未攔截的例外。 ```js // 容器啟動時由 entrypoint.sh 執行:exec node /app/index.js diff --git a/app/index.js b/app/index.js index eca265b..3f66a1d 100644 --- a/app/index.js +++ b/app/index.js @@ -196,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 = []; @@ -228,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 }); diff --git a/entrypoint.sh b/entrypoint.sh index a6046a8..84e4957 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -7,7 +7,7 @@ # 中辨識正在執行哪一個 action 及其版本;接著以 exec 啟動 node 主程式 # /app/index.js,並把所有外部傳入的參數原樣交給它。 # -# 更新日期(台灣時區):2026/06/30 18:19:59 +# 更新日期(台灣時區 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 18:19:59' +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 "$@"