Files
clash-proxy/scripts/release-version.mjs

395 lines
11 KiB
JavaScript
Raw Normal View History

/**
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
*
* Usage:
* pnpm release-version <version>
*
* <version> can be:
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3-rc.1)
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
2025-11-03 19:47:50 +08:00
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3-autobuild.1022.r2+cc39b2)
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3-autobuild.1022.r2+a1b2c3d)
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3-deploytest.1022.r2+cc39b2)
*
* Examples:
* pnpm release-version 1.2.3
* pnpm release-version v1.2.3-beta
* pnpm release-version beta
* pnpm release-version autobuild
* pnpm release-version autobuild-latest
* pnpm release-version deploytest
*
* The script will:
* - Validate and normalize the version argument
* - Update the version field in package.json
* - Update the version field in src-tauri/Cargo.toml
* - Update the version field in src-tauri/tauri.conf.json
*
* Errors are logged and the process exits with code 1 on failure.
*/
import { execSync } from "child_process";
import fs from "fs/promises";
import process from "node:process";
import path from "path";
import { program } from "commander";
/**
* 获取当前 git commit hash
* @returns {string}
*/
function getGitShortCommit() {
try {
return execSync("git rev-parse --short HEAD").toString().trim();
} catch {
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
return "nogit";
}
}
/**
* 获取最新 Tauri 相关提交的短 hash
* @returns {string}
*/
function getLatestTauriCommit() {
try {
const fullHash = execSync(
"bash ./scripts-workflow/get_latest_tauri_commit.bash",
)
.toString()
.trim();
const shortHash = execSync(`git rev-parse --short ${fullHash}`)
.toString()
.trim();
console.log(`[INFO]: Latest Tauri-related commit: ${shortHash}`);
return shortHash;
} catch (error) {
console.warn(
"[WARN]: Failed to get latest Tauri commit, fallback to current git short commit",
);
console.warn(`[WARN]: Error details: ${error.message}`);
return getGitShortCommit();
}
}
/**
* 获取 Asia/Shanghai 时区的日期片段
* @returns {string}
*/
function getLocalDatePart() {
const now = new Date();
2025-09-05 11:21:34 +08:00
const dateFormatter = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Shanghai",
month: "2-digit",
day: "2-digit",
2025-09-05 11:21:34 +08:00
});
const dateParts = Object.fromEntries(
dateFormatter.formatToParts(now).map((part) => [part.type, part.value]),
);
2025-09-05 11:21:34 +08:00
const month = dateParts.month ?? "00";
const day = dateParts.day ?? "00";
2025-09-05 11:21:34 +08:00
return `${month}${day}`;
}
/**
* 获取 GitHub Actions 运行编号若存在
* @returns {string|null}
*/
function getRunIdentifier() {
const runNumber = process.env.GITHUB_RUN_NUMBER;
if (runNumber && /^[0-9]+$/.test(runNumber)) {
const runNum = Number.parseInt(runNumber, 10);
if (!Number.isNaN(runNum)) {
const base = `r${runNum.toString(36)}`;
const attempt = process.env.GITHUB_RUN_ATTEMPT;
if (attempt && /^[0-9]+$/.test(attempt)) {
const attemptNumber = Number.parseInt(attempt, 10);
if (!Number.isNaN(attemptNumber) && attemptNumber > 1) {
return `${base}${attemptNumber.toString(36)}`;
}
}
return base;
}
}
const attempt = process.env.GITHUB_RUN_ATTEMPT;
if (attempt && /^[0-9]+$/.test(attempt)) {
const attemptNumber = Number.parseInt(attempt, 10);
if (!Number.isNaN(attemptNumber)) {
return `r${attemptNumber.toString(36)}`;
}
}
return null;
}
/**
* 生成用于自动构建类渠道的版本后缀
* @param {Object} options
* @param {boolean} [options.includeCommit=false]
* @param {"current"|"tauri"} [options.commitSource="current"]
* @param {boolean} [options.includeRun=true]
* @returns {string}
*/
function generateChannelSuffix({
includeCommit = false,
commitSource = "current",
includeRun = true,
} = {}) {
const segments = [];
const date = getLocalDatePart();
segments.push(date);
if (includeCommit) {
const commit =
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
segments.push(commit);
}
if (includeRun) {
const run = getRunIdentifier();
if (run) {
segments.push(run);
}
}
return segments.join(".");
}
2025-11-03 19:47:50 +08:00
/**
* autobuild 渠道构建版本片段
* @param {Object} options
* @param {"current"|"tauri"} [options.commitSource="current"]
* @returns {{date: string, run: string, metadata: string}}
*/
function generateAutobuildComponents({ commitSource = "current" } = {}) {
const date = getLocalDatePart();
const run = getRunIdentifier() ?? `manual${Date.now().toString(36)}`;
const commitHash =
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
return {
date,
run,
metadata: commitHash || "nogit",
};
}
/**
* 验证版本号格式
* @param {string} version
* @returns {boolean}
*/
function isValidVersion(version) {
return /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
version,
);
}
/**
* 标准化版本号
* @param {string} version
* @returns {string}
*/
function normalizeVersion(version) {
return version.startsWith("v") ? version : `v${version}`;
}
/**
* 提取基础版本号去掉所有 pre-release build metadata
* @param {string} version
* @returns {string}
*/
function getBaseVersion(version) {
const cleaned = version.startsWith("v") ? version.slice(1) : version;
const withoutBuild = cleaned.split("+")[0];
const [base] = withoutBuild.split("-");
return base;
}
/**
* 更新 package.json 版本号
* @param {string} newVersion
*/
async function updatePackageVersion(newVersion) {
const _dirname = process.cwd();
const packageJsonPath = path.join(_dirname, "package.json");
try {
const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data);
console.log(
"[INFO]: Current package.json version is: ",
packageJson.version,
);
packageJson.version = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
"utf8",
);
console.log(
`[INFO]: package.json version updated to: ${packageJson.version}`,
);
} catch (error) {
console.error("Error updating package.json version:", error);
throw error;
}
}
/**
* 更新 Cargo.toml 版本号
* @param {string} newVersion
*/
async function updateCargoVersion(newVersion) {
const _dirname = process.cwd();
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
try {
const data = await fs.readFile(cargoTomlPath, "utf8");
const lines = data.split("\n");
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const updatedLines = lines.map((line) => {
if (line.trim().startsWith("version =")) {
return line.replace(
/version\s*=\s*"[^"]+"/,
`version = "${versionWithoutV}"`,
);
}
return line;
});
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
} catch (error) {
console.error("Error updating Cargo.toml version:", error);
throw error;
}
}
/**
* 更新 tauri.conf.json 版本号
* @param {string} newVersion
*/
async function updateTauriConfigVersion(newVersion) {
const _dirname = process.cwd();
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
try {
const data = await fs.readFile(tauriConfigPath, "utf8");
const tauriConfig = JSON.parse(data);
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
console.log(
"[INFO]: Current tauri.conf.json version is: ",
tauriConfig.version,
);
// 使用完整版本信息包含build metadata
tauriConfig.version = versionWithoutV;
await fs.writeFile(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2),
"utf8",
);
console.log(
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
);
} catch (error) {
console.error("Error updating tauri.conf.json version:", error);
throw error;
}
}
/**
* 获取当前版本号
*/
async function getCurrentVersion() {
const _dirname = process.cwd();
const packageJsonPath = path.join(_dirname, "package.json");
try {
const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data);
return packageJson.version;
} catch (error) {
console.error("Error getting current version:", error);
throw error;
}
}
/**
* 主函数
*/
async function main(versionArg) {
if (!versionArg) {
console.error("Error: Version argument is required");
process.exit(1);
}
try {
let newVersion;
const validTags = [
"alpha",
"beta",
"rc",
"autobuild",
"autobuild-latest",
"deploytest",
];
if (validTags.includes(versionArg.toLowerCase())) {
const currentVersion = await getCurrentVersion();
const baseVersion = getBaseVersion(currentVersion);
if (versionArg.toLowerCase() === "autobuild") {
2025-11-03 19:47:50 +08:00
// 格式: 2.3.0-autobuild.1022.r2+cc39b2
const parts = generateAutobuildComponents({ commitSource: "tauri" });
newVersion = `${baseVersion}-autobuild.${parts.date}.${parts.run}+${parts.metadata}`;
} else if (versionArg.toLowerCase() === "autobuild-latest") {
2025-11-03 19:47:50 +08:00
// 格式: 2.3.0-autobuild.1022.r2+a1b2c3d (使用最新 Tauri 提交)
const parts = generateAutobuildComponents({ commitSource: "tauri" });
newVersion = `${baseVersion}-autobuild.${parts.date}.${parts.run}+${parts.metadata}`;
} else if (versionArg.toLowerCase() === "deploytest") {
2025-11-03 19:47:50 +08:00
// 格式: 2.3.0-deploytest.1022.r2+cc39b2
const parts = generateAutobuildComponents({ commitSource: "tauri" });
newVersion = `${baseVersion}-deploytest.${parts.date}.${parts.run}+${parts.metadata}`;
} else {
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
}
} else {
if (!isValidVersion(versionArg)) {
console.error("Error: Invalid version format");
process.exit(1);
}
newVersion = normalizeVersion(versionArg);
}
console.log(`[INFO]: Updating versions to: ${newVersion}`);
await updatePackageVersion(newVersion);
await updateCargoVersion(newVersion);
await updateTauriConfigVersion(newVersion);
console.log("[SUCCESS]: All version updates completed successfully!");
} catch (error) {
console.error("[ERROR]: Failed to update versions:", error);
process.exit(1);
}
}
program
.name("pnpm release-version")
.description("Update project version numbers")
.argument("<version>", "version tag or full version")
.action(main)
.parse(process.argv);