fix(cleanup-release): 資料異常改判定失敗、分頁上限可設定並收斂錯誤輸出

- release id 非正整數、tag 缺少名稱時改視為失敗(fail + return false),壞資料不再被靜默當成功
- 分頁上限改為可用環境變數 MAX_PAGES 覆寫(預設 1000),超限錯誤訊息附調高方式
- 未捕捉錯誤預設只輸出 error.message,RUNNER_DEBUG=1 時才輸出完整 stack

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-07-15 10:43:21 +08:00
co-authored by Claude Fable 5
parent c4a83a8969
commit 4aecd48b5a
+23 -13
View File
@@ -1,7 +1,7 @@
const https = require('https');
const DELETE_CONCURRENCY = 4;
const MAX_PAGES = 1000;
const DEFAULT_MAX_PAGES = 1000;
const keepAliveAgent = new https.Agent({ keepAlive: true });
@@ -295,18 +295,19 @@ function request(url, { method = 'GET', headers = {}, collectBody = true } = {})
}
/**
* 逐頁抓取 JSON 陣列資料,直到回傳空頁為止;超過 `MAX_PAGES` 即中止並回報異常。
* 逐頁抓取 JSON 陣列資料,直到回傳空頁為止;超過 `maxPages` 即中止並回報異常。
*
* @param {string} baseUrl 不含 page 參數的 API URL。
* @param {Record<string, string>} headers request 標頭。
* @param {number} [maxPages=DEFAULT_MAX_PAGES] 分頁上限,可由環境變數 `MAX_PAGES` 覆寫。
* @returns {Promise<any[]>} 合併後的陣列資料。
*/
async function fetchAllPages(baseUrl, headers) {
async function fetchAllPages(baseUrl, headers, maxPages = DEFAULT_MAX_PAGES) {
const all = [];
for (let page = 1; ; page += 1) {
if (page > MAX_PAGES) {
throw new Error(`GET ${baseUrl} 分頁超過 ${MAX_PAGES} 頁上限,中止抓取以避免無限迴圈`);
if (page > maxPages) {
throw new Error(`GET ${baseUrl} 分頁超過 ${maxPages} 頁上限,中止抓取以避免無限迴圈;資料量更大時可用 MAX_PAGES 環境變數調高上限`);
}
const pageUrl = `${baseUrl}?page=${page}`;
@@ -401,6 +402,7 @@ async function main() {
const GITEA_REPOSITORY = normalizeEnvValue(process.env.GITEA_REPOSITORY);
const RUNNER_TOKEN = normalizeEnvValue(process.env.RUNNER_TOKEN) ?? '';
const KEEP_COUNT = normalizeEnvValue(process.env.KEEP_COUNT) ?? '';
const MAX_PAGES = normalizeEnvValue(process.env.MAX_PAGES) ?? '';
section('參數檢查');
requireValue('GITEA_SERVER_URL', GITEA_SERVER_URL, maskUrlForLog(GITEA_SERVER_URL));
@@ -410,6 +412,13 @@ async function main() {
requireValue('KEEP_COUNT', KEEP_COUNT);
requirePositiveInteger('KEEP_COUNT', KEEP_COUNT);
let maxPages = DEFAULT_MAX_PAGES;
if (!isEmptyOrNull(MAX_PAGES)) {
info(`MAX_PAGES=${MAX_PAGES}`);
requirePositiveInteger('MAX_PAGES', MAX_PAGES);
maxPages = Number(MAX_PAGES);
}
const keepCount = Number(KEEP_COUNT);
const authHeaders = {};
if (isEmptyOrNull(RUNNER_TOKEN)) {
@@ -432,7 +441,7 @@ async function main() {
section('取得成品資訊');
info(`GET ${releaseApiUrl}`);
const releaseJson = await fetchAllPages(releaseApiUrl, authHeaders);
const releaseJson = await fetchAllPages(releaseApiUrl, authHeaders, maxPages);
releaseJson.sort((left, right) => {
if (left.created_at < right.created_at) {
return 1;
@@ -458,10 +467,10 @@ async function main() {
const releaseResults = await processInBatches(releaseToDelete, DELETE_CONCURRENCY, async (releaseItem) => {
const releaseId = releaseItem?.id;
if (!Number.isSafeInteger(releaseId) || releaseId <= 0) {
warn(
`略過 id 不是正整數的成品: ${sanitizeLogText(releaseItem?.tag_name || '')} (${sanitizeLogText(releaseItem?.name || '')})`,
fail(
`成品 id 不是正整數,視為資料異常: ${sanitizeLogText(releaseItem?.tag_name || '')} (${sanitizeLogText(releaseItem?.name || '')})`,
);
return true;
return false;
}
const releaseTag = sanitizeLogText(releaseItem.tag_name || '');
@@ -496,14 +505,14 @@ async function main() {
const tagApiUrl = `${serverBaseUrl}/api/v1/repos/${repositoryPath}/tags`;
info(`GET ${tagApiUrl}`);
const tagJson = await fetchAllPages(tagApiUrl, authHeaders);
const tagJson = await fetchAllPages(tagApiUrl, authHeaders, maxPages);
info(`TAG_COUNT=${tagJson.length}`);
const tagResults = await processInBatches(tagJson, DELETE_CONCURRENCY, async (tagItem) => {
const tagName = tagItem?.name;
if (isEmptyOrNull(tagName)) {
warn('略過沒有名稱的 tag');
return true;
fail('tag 缺少名稱,視為資料異常');
return false;
}
const safeTagName = sanitizeLogText(tagName);
@@ -532,7 +541,8 @@ async function main() {
main()
.catch((error) => {
fail(error instanceof Error ? error.stack || error.message : String(error));
const showStack = process.env.RUNNER_DEBUG === '1';
fail(error instanceof Error ? (showStack && error.stack) || error.message : String(error));
process.exitCode = 1;
})
.finally(() => {