fix(calculate-version): 修復 AI 審查的安全與健全性問題
- Dockerfile 移除 --no-check-certificate,恢復 TLS 憑證檢查以防中間人攻擊 - config 對 GITEA_SERVER_URL 加入 http/https URL 格式驗證 - logger.fail 改為 logger.error(僅輸出不終止行程),退出移至 index 頂層處理 - index.main 改用相依注入並於頂層攔截錯誤後 exit 1,避免 library 內呼叫 process.exit - releases 將 response.text() 包入 try-catch 並附帶頁碼 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f71d2cc35c
commit
fdf33b41d8
+2
-2
@@ -9,8 +9,8 @@
|
|||||||
FROM node:lts-alpine
|
FROM node:lts-alpine
|
||||||
|
|
||||||
# entrypoint.sh 以 bash 撰寫,Alpine 預設無 bash 需另行安裝
|
# entrypoint.sh 以 bash 撰寫,Alpine 預設無 bash 需另行安裝
|
||||||
# --no-cache 不保留 apk 索引快取以縮小映像;--no-check-certificate 略過憑證檢查 (需人工確認:略過憑證驗證有安全風險)。
|
# --no-cache 不保留 apk 索引快取以縮小映像(保留 TLS 憑證檢查以防中間人攻擊)。
|
||||||
RUN apk add --no-cache --no-check-certificate bash
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
# 複製容器進入點腳本至根目錄。
|
# 複製容器進入點腳本至根目錄。
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|||||||
+17
-3
@@ -42,18 +42,32 @@ function normalizeBetaFlag(value) {
|
|||||||
return value === 'true';
|
return value === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 驗證字串為合法的 http/https URL,否則拋出錯誤(避免指向非預期協定或格式錯誤的位址)
|
||||||
|
function assertHttpUrl(name, value) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = new URL(value);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`${name} 格式錯誤,必須為合法的 URL`);
|
||||||
|
}
|
||||||
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||||
|
throw new Error(`${name} 必須使用 http 或 https 協定`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 從環境變數載入並驗證執行所需的設定。
|
* 從環境變數載入並驗證執行所需的設定。
|
||||||
*
|
*
|
||||||
* GITEA_SERVER_URL 與 GITEA_REPOSITORY 為必填,未設定時會拋出錯誤;
|
* GITEA_SERVER_URL 與 GITEA_REPOSITORY 為必填,未設定時會拋出錯誤;GITEA_SERVER_URL
|
||||||
* GITEA_TOKEN 為非必填,未設定時為 null;IS_BETA 會被正規化為布林值。
|
* 另需為合法的 http/https URL;GITEA_TOKEN 為非必填,未設定時為 null;IS_BETA 會被正規化為布林值。
|
||||||
*
|
*
|
||||||
* @param {Object} [env=process.env] - 環境變數來源物件,預設為 process.env。
|
* @param {Object} [env=process.env] - 環境變數來源物件,預設為 process.env。
|
||||||
* @returns {{ serverUrl: string, repository: string, token: (string|null), isBeta: boolean }} 已驗證的設定物件。
|
* @returns {{ serverUrl: string, repository: string, token: (string|null), isBeta: boolean }} 已驗證的設定物件。
|
||||||
* @throws {Error} 當 GITEA_SERVER_URL 或 GITEA_REPOSITORY 未設定時拋出。
|
* @throws {Error} 當 GITEA_SERVER_URL 或 GITEA_REPOSITORY 未設定,或 GITEA_SERVER_URL 非合法 http/https URL 時拋出。
|
||||||
*/
|
*/
|
||||||
function loadConfig(env = process.env) {
|
function loadConfig(env = process.env) {
|
||||||
const serverUrl = requireEnv('GITEA_SERVER_URL', env.GITEA_SERVER_URL);
|
const serverUrl = requireEnv('GITEA_SERVER_URL', env.GITEA_SERVER_URL);
|
||||||
|
assertHttpUrl('GITEA_SERVER_URL', serverUrl);
|
||||||
const repository = requireEnv('GITEA_REPOSITORY', env.GITEA_REPOSITORY);
|
const repository = requireEnv('GITEA_REPOSITORY', env.GITEA_REPOSITORY);
|
||||||
const token = isUnset(env.GITEA_TOKEN) ? null : env.GITEA_TOKEN;
|
const token = isUnset(env.GITEA_TOKEN) ? null : env.GITEA_TOKEN;
|
||||||
const isBeta = normalizeBetaFlag(env.IS_BETA);
|
const isBeta = normalizeBetaFlag(env.IS_BETA);
|
||||||
|
|||||||
+42
-37
@@ -9,52 +9,57 @@ const { writeOutput } = require('./output');
|
|||||||
/**
|
/**
|
||||||
* Action 進入點:協調設定載入、release 取得、版本號計算與輸出寫出。
|
* Action 進入點:協調設定載入、release 取得、版本號計算與輸出寫出。
|
||||||
*
|
*
|
||||||
* 流程:印出「參數檢查」區段並呼叫 loadConfig() 載入設定(失敗則以 logger.fail
|
* 失敗時(loadConfig 或 fetchReleases 拋錯)直接向外拋出例外,由呼叫端決定如何結束,
|
||||||
* 回報並結束);印出設定值(token 以 *** 或「未提供」顯示);組出 release URL 並
|
* 以利單元測試覆蓋失敗路徑。相依模組可透過 deps 注入,預設使用各兄弟模組的實作。
|
||||||
* 以 fetchReleases() 取得 release 清單(失敗則以 logger.fail 回報並結束);以
|
|
||||||
* calculateVersion() 計算最新版與新版號,最後以 writeOutput('version', version)
|
|
||||||
* 寫出 version 輸出。
|
|
||||||
*
|
*
|
||||||
* Side effects:透過 logger 輸出至 stdout/stderr、失敗時使 process 以失敗狀態
|
* @param {Object} [deps={}] - 可注入的相依,供測試替換。
|
||||||
* 結束、並寫入 GITHUB_OUTPUT。此函式於模組載入時即被呼叫。
|
* @param {Function} [deps.loadConfig] - 載入設定的函式。
|
||||||
*
|
* @param {Function} [deps.fetchReleases] - 取得 release 的函式。
|
||||||
* @returns {Promise<void>}
|
* @param {Function} [deps.calculateVersion] - 計算版本號的函式。
|
||||||
|
* @param {Function} [deps.writeOutput] - 寫出 output 的函式。
|
||||||
|
* @param {{section:Function, info:Function, error:Function}} [deps.log] - log 記錄器。
|
||||||
|
* @returns {Promise<string>} 計算出的版本號。
|
||||||
*/
|
*/
|
||||||
async function main() {
|
async function main(deps = {}) {
|
||||||
logger.section('參數檢查');
|
const {
|
||||||
|
loadConfig: loadConfigFn = loadConfig,
|
||||||
|
fetchReleases: fetchReleasesFn = fetchReleases,
|
||||||
|
calculateVersion: calculateVersionFn = calculateVersion,
|
||||||
|
writeOutput: writeOutputFn = writeOutput,
|
||||||
|
log = logger,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
let config;
|
log.section('參數檢查');
|
||||||
try {
|
|
||||||
config = loadConfig();
|
|
||||||
} catch (error) {
|
|
||||||
logger.fail(error.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`GITEA_SERVER_URL=${config.serverUrl}`);
|
const config = loadConfigFn();
|
||||||
logger.info(`GITEA_REPOSITORY=${config.repository}`);
|
log.info(`GITEA_SERVER_URL=${config.serverUrl}`);
|
||||||
logger.info(config.token ? 'GITEA_TOKEN=***' : 'GITEA_TOKEN=未提供');
|
log.info(`GITEA_REPOSITORY=${config.repository}`);
|
||||||
logger.info(`IS_BETA=${config.isBeta}`);
|
log.info(config.token ? 'GITEA_TOKEN=***' : 'GITEA_TOKEN=未提供');
|
||||||
|
log.info(`IS_BETA=${config.isBeta}`);
|
||||||
|
|
||||||
logger.section('取得版本資料');
|
log.section('取得版本資料');
|
||||||
|
|
||||||
const releaseUrl = `${config.serverUrl}/api/v1/repos/${config.repository}/releases`;
|
const releaseUrl = `${config.serverUrl}/api/v1/repos/${config.repository}/releases`;
|
||||||
logger.info(`RELEASE_URL=${releaseUrl}`);
|
log.info(`RELEASE_URL=${releaseUrl}`);
|
||||||
|
|
||||||
let releases;
|
const releases = await fetchReleasesFn(releaseUrl, { token: config.token, logger: log });
|
||||||
try {
|
|
||||||
releases = await fetchReleases(releaseUrl, { token: config.token, logger });
|
const { latest, version } = calculateVersionFn(releases, config.isBeta);
|
||||||
} catch (error) {
|
log.info(`LATEST_VERSION=${latest}`);
|
||||||
logger.fail(error.message);
|
|
||||||
return;
|
log.section('計算版本號');
|
||||||
|
log.info(`NEW_VERSION=${version}`);
|
||||||
|
writeOutputFn('version', version);
|
||||||
|
|
||||||
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { latest, version } = calculateVersion(releases, config.isBeta);
|
// 僅在被直接執行時啟動(被 require/測試載入時不自動執行),失敗則在頂層回報並以狀態碼 1 結束
|
||||||
logger.info(`LATEST_VERSION=${latest}`);
|
if (require.main === module) {
|
||||||
|
main().catch((error) => {
|
||||||
logger.section('計算版本號');
|
logger.error(error.message);
|
||||||
logger.info(`NEW_VERSION=${version}`);
|
process.exit(1);
|
||||||
writeOutput('version', version);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
module.exports = { main };
|
||||||
|
|||||||
+5
-8
@@ -28,18 +28,15 @@ function info(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 輸出錯誤層級的 log 訊息至標準錯誤輸出(stderr),並以狀態碼 1
|
* 輸出錯誤層級的 log 訊息至標準錯誤輸出(stderr),自動加上 `[error]` 前綴與換行。
|
||||||
* 立即終止整個行程。用於發生無法復原的錯誤時中止執行。
|
|
||||||
*
|
*
|
||||||
* 注意:此函式呼叫 `process.exit(1)`,正常情況下不會回傳給呼叫端,
|
* 僅負責輸出,不終止行程;是否結束由呼叫端(進入點)決定,以利測試與錯誤復原。
|
||||||
* 其後的程式碼不會被執行。
|
|
||||||
*
|
*
|
||||||
* @param {string} message - 要輸出的錯誤描述內容。
|
* @param {string} message - 要輸出的錯誤描述內容。
|
||||||
* @returns {never} 不會正常回傳(行程會被終止)。
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function fail(message) {
|
function error(message) {
|
||||||
process.stderr.write(`[error] ${message}\n`);
|
process.stderr.write(`[error] ${message}\n`);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { section, info, fail };
|
module.exports = { section, info, error };
|
||||||
|
|||||||
+6
-1
@@ -47,7 +47,12 @@ async function fetchReleases(baseUrl, options = {}) {
|
|||||||
throw new Error(`release API 請求失敗 (page=${page})`);
|
throw new Error(`release API 請求失敗 (page=${page})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = await response.text();
|
let text;
|
||||||
|
try {
|
||||||
|
text = await response.text();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`release API 回應讀取失敗 (page=${page}): ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 空字串或 null 代表已無更多資料
|
// 空字串或 null 代表已無更多資料
|
||||||
if (!text || text === 'null') {
|
if (!text || text === 'null') {
|
||||||
|
|||||||
Reference in New Issue
Block a user