Files
calculate-version/app/releases.js
T
JefferyandClaude Opus 4.8 bc4cea6daf refactor(calculate-version): 抽出 truncateString 並明確化 repository 驗證命名
- releases 將片段截斷邏輯抽為可重用、可獨立測試的 truncateString(text, limit) 並 export
- config 將 assertRepository 更名為 assertGiteaRepositoryFormat,凸顯其驗證 Gitea owner/repo 格式的領域語義

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:21:45 +08:00

104 lines
3.4 KiB
JavaScript

'use strict';
// 每頁取得的 release 筆數
const RELEASES_PER_PAGE = 10;
// JSON 解析失敗時,附在錯誤訊息中的回應內容片段最大字元數
const API_ERROR_SNIPPET_LENGTH = 200;
// 以字元(而非 UTF-16 碼元)安全截斷字串至 limit 個字元,避免拆分多位元組字元造成亂碼;
// 先以長度上限粗略截掉過長輸入,避免將可能極大的字串整個陣列化
function truncateString(text, limit) {
const bounded = text.slice(0, limit * 2);
return Array.from(bounded).slice(0, limit).join('');
}
/**
* 以分頁方式取得指定 Gitea repo 的所有 release,並回傳合併後的陣列。
*
* 使用全域 fetch 逐頁請求,每頁 limit 為 10;當某頁回傳空資料、null
* 或筆數少於每頁上限時即停止取得。
*
* @param {string} baseUrl - release API 的基底 URL(不含 query string)。
* @param {object} [options={}] - 選用設定。
* @param {string} [options.token] - 授權 token;有值時以 `Authorization: token <token>`
* 進行授權請求,否則以匿名方式請求。
* @param {{ info: (message: string) => void }} [options.logger] - 選用的記錄器,
* 需提供 info() 方法以輸出進度訊息。
* @returns {Promise<Array>} 解析為所有 release 物件合併後的陣列。
* @throws {Error} 當網路請求失敗、response 非 2xx、回傳資料無法解析,
* 或回傳非陣列資料時拋出。
*/
async function fetchReleases(baseUrl, options = {}) {
const { token, logger } = options;
const headers = {};
if (token) {
logger?.info('使用授權 token 取得 release');
headers.Authorization = `token ${token}`;
} else {
logger?.info('使用匿名請求取得 release');
}
let page = 1;
const combined = [];
while (true) {
const url = `${baseUrl}?limit=${RELEASES_PER_PAGE}&page=${page}`;
let response;
try {
response = await fetch(url, { headers });
} catch (error) {
throw new Error(`release API 請求失敗 (page=${page}): ${error.message}`);
}
if (!response.ok) {
throw new Error(`release API 請求失敗 (page=${page})`);
}
let text;
try {
text = await response.text();
} catch (error) {
throw new Error(`release API 回應讀取失敗 (page=${page}): ${error.message}`);
}
// 空字串或 null 代表已無更多資料
if (!text || text === 'null') {
break;
}
let pageJson;
try {
pageJson = JSON.parse(text);
} catch {
const contentSnippet = truncateString(text, API_ERROR_SNIPPET_LENGTH);
throw new Error(`release API 回傳資料無法解析 (page=${page}),回應內容片段:「${contentSnippet}」`);
}
if (pageJson === null) {
break;
}
if (!Array.isArray(pageJson)) {
throw new Error(`release API 回傳非陣列資料 (page=${page})`);
}
const count = pageJson.length;
logger?.info(`第 ${page} 頁取得 ${count} 筆 release`);
combined.push(...pageJson);
// 不足一頁代表已取完
if (count < RELEASES_PER_PAGE) {
break;
}
page += 1;
}
return combined;
}
module.exports = { RELEASES_PER_PAGE, truncateString, fetchReleases };