fix(release-cleanup): 日誌改為 [階段][等級][時間] 格式並移除區段分隔線

- 新增 stage() 設定目前階段、formatLog() 共用組裝,log 統一為 [階段][等級][時間]: 訊息
- 階段對應 參數檢查/取得舊版本/刪除舊版本;移除 separator()/section() 的 ==== / ---- 分隔區塊
- 行為、控制流、exit code 不變;fail 仍走 stderr、其餘走 stdout

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-07-01 09:07:05 +08:00
co-authored by Claude Opus 4.8
parent 9d8cbb6f1c
commit c37b33f397
+39 -30
View File
@@ -2,26 +2,21 @@
'use strict'; 'use strict';
/** /**
* 在 stdout 輸出一條視覺分隔線(前後各一個換行 + 50 個等號), * 目前流程階段名稱,作為每行 log 的 `[階段]` 前綴使用;
* 用於在 log 中切割不同區段 * 為空字串時省略整個 `[階段]` 區塊
* * @type {string}
* @returns {void}
*/ */
function separator() { let currentStage = '';
process.stdout.write('\n==================================================\n');
}
/** /**
* 輸出一個區段標題區塊:上緣等號分隔線、標題文字、下緣虛線, * 設定目前流程階段名稱,之後輸出的 log 會以 `[階段]` 前綴標示所屬階段。
* 用於在 log 中標示流程進入新階段 * 傳入空字串可清除階段前綴
* *
* @param {string} title 區段標題文字 * @param {string} name 階段名稱(顯示於每行 log 開頭的 `[階段]` 區塊)
* @returns {void} * @returns {void}
*/ */
function section(title) { function stage(name) {
separator(); currentStage = name;
process.stdout.write(`${title}\n`);
process.stdout.write('--------------------------------------------------\n');
} }
/** /**
@@ -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 要輸出的訊息內容。 * @param {string} msg 要輸出的訊息內容。
* @returns {void} * @returns {void}
*/ */
function info(msg) { function info(msg) {
console.log(`[INF][${timestamp()}]: ${msg}`); console.log(formatLog('INF', msg));
} }
/** /**
* 以 `[INF][時間]:` 前綴將成功訊息輸出到 stdout(時間為 Asia/Taipei)。 * 以 `[階段?][INF][時間]:` 前綴將成功訊息輸出到 stdout(時間為 Asia/Taipei)。
* 規範等級碼無「成功」,故等級採 INF,成功語意保留於訊息文字。 * 規範等級碼無「成功」,故等級採 INF,成功語意保留於訊息文字。
* *
* @param {string} msg 要輸出的訊息內容。 * @param {string} msg 要輸出的訊息內容。
* @returns {void} * @returns {void}
*/ */
function success(msg) { function success(msg) {
console.log(`[INF][${timestamp()}]: ${msg}`); console.log(formatLog('INF', msg));
} }
/** /**
* 以 `[WRN][時間]:` 前綴將警告訊息輸出到 stdout(時間為 Asia/Taipei)。 * 以 `[階段?][WRN][時間]:` 前綴將警告訊息輸出到 stdout(時間為 Asia/Taipei)。
* *
* @param {string} msg 要輸出的訊息內容。 * @param {string} msg 要輸出的訊息內容。
* @returns {void} * @returns {void}
*/ */
function warn(msg) { function warn(msg) {
console.log(`[WRN][${timestamp()}]: ${msg}`); console.log(formatLog('WRN', msg));
} }
/** /**
* 以 `[ERR][時間]:` 前綴將錯誤訊息輸出到 stderr(時間為 Asia/Taipei)。 * 以 `[階段?][ERR][時間]:` 前綴將錯誤訊息輸出到 stderr(時間為 Asia/Taipei)。
* 僅負責輸出,不會結束程序(是否離開由呼叫端決定)。 * 僅負責輸出,不會結束程序(是否離開由呼叫端決定)。
* *
* @param {string} msg 要輸出的錯誤訊息內容。 * @param {string} msg 要輸出的錯誤訊息內容。
* @returns {void} * @returns {void}
*/ */
function fail(msg) { function fail(msg) {
console.error(`[ERR][${timestamp()}]: ${msg}`); console.error(formatLog('ERR', msg));
} }
/** /**
@@ -92,8 +101,8 @@ function isEmptyOrNull(value) {
} }
/** /**
* 檢查必填值。先以 [INFO] 印出 name=value * 檢查必填值。先以 info[INF]印出 name=value
* 若值被判定為空(見 isEmptyOrNull),印出 [ERR] 並以 exit code 1 結束程序。 * 若值被判定為空(見 isEmptyOrNull),以 fail[ERR])輸出並以 exit code 1 結束程序。
* *
* @param {string} name 參數名稱(用於 log 與錯誤訊息)。 * @param {string} name 參數名稱(用於 log 與錯誤訊息)。
* @param {*} value 要驗證的參數值。 * @param {*} value 要驗證的參數值。
@@ -157,7 +166,9 @@ async function main() {
const GITEA_TOKEN = process.env.GITEA_TOKEN; const GITEA_TOKEN = process.env.GITEA_TOKEN;
const KEEP_COUNT_RAW = process.env.KEEP_COUNT; 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_SERVER_URL', GITEA_SERVER_URL);
requireValue('GITEA_REPOSITORY', GITEA_REPOSITORY); requireValue('GITEA_REPOSITORY', GITEA_REPOSITORY);
requireValue('KEEP_COUNT', KEEP_COUNT_RAW); requireValue('KEEP_COUNT', KEEP_COUNT_RAW);
@@ -225,7 +236,7 @@ async function main() {
const releaseApiUrl = `${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases`; const releaseApiUrl = `${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases`;
section('取得成品資訊'); stage('取得舊版本');
info(`GET ${releaseApiUrl}`); info(`GET ${releaseApiUrl}`);
let releases = await fetchAllPages(releaseApiUrl); let releases = await fetchAllPages(releaseApiUrl);
@@ -251,7 +262,7 @@ async function main() {
if (releasesToDelete.length === 0) { if (releasesToDelete.length === 0) {
success('沒有需要清理的舊版本成品'); success('沒有需要清理的舊版本成品');
} else { } else {
section('刪除舊版本成品'); stage('刪除舊版本');
info(`DELETE_RELEASE_COUNT=${releasesToDelete.length}`); info(`DELETE_RELEASE_COUNT=${releasesToDelete.length}`);
for (const release of releasesToDelete) { for (const release of releasesToDelete) {
@@ -276,7 +287,7 @@ async function main() {
} }
} }
section('刪除未指定 release 的 tag'); stage('刪除舊版本');
// 重新取得 release 清單,得到刪除舊版本後仍指定 tag 的成品 // 重新取得 release 清單,得到刪除舊版本後仍指定 tag 的成品
const currentReleases = await fetchAllPages(releaseApiUrl); const currentReleases = await fetchAllPages(releaseApiUrl);
@@ -311,8 +322,6 @@ async function main() {
fail(`刪除 tag 失敗: ${tagName}, HTTP ${code}`); fail(`刪除 tag 失敗: ${tagName}, HTTP ${code}`);
} }
} }
separator();
} }
main().catch((err) => { main().catch((err) => {