Files
clash-verge-rev/scripts/release-version.mjs
T
2026-09-20 19:49:13 +08:00

291 lines
8.8 KiB
JavaScript

/**
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, Cargo.lock, 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+build)
* - 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)
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3+autobuild.0614.a1b2c3d)
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3+deploytest.2406101530)
*
* 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
*
*/
import { execSync } from 'child_process'
import fs from 'fs/promises'
import path from 'path'
import { program } from 'commander'
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'
}
}
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()
}
}
/** Generates `MMDD`, optionally followed by a commit, in the Asia/Shanghai timezone. */
function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
const now = new Date()
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
month: '2-digit',
day: '2-digit',
})
const parts = formatter.formatToParts(now)
const month = parts.find((part) => part.type === 'month').value
const day = parts.find((part) => part.type === 'day').value
if (withCommit) {
const gitShort = useTauriCommit
? getLatestTauriCommit()
: getGitShortCommit()
return `${month}${day}.${gitShort}`
}
return `${month}${day}`
}
function isValidVersion(version) {
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
version,
)
}
function normalizeVersion(version) {
return version.startsWith('v') ? version : `v${version}`
}
function getBaseVersion(version) {
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, '')
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, '')
return base
}
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,
)
const versionWithoutV = newVersion.startsWith('v')
? newVersion.slice(1)
: newVersion
const updatedData = data.replace(
/^(\s*"version"\s*:\s*)"[^"]+"/m,
`$1"${versionWithoutV}"`,
)
if (updatedData === data) {
throw new Error('version field was not found in package.json')
}
await fs.writeFile(packageJsonPath, updatedData, 'utf8')
console.log(`[INFO]: package.json version updated to: ${versionWithoutV}`)
} catch (error) {
console.error('Error updating package.json version:', error)
throw error
}
}
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
}
}
async function updateCargoLockVersion(newVersion) {
const _dirname = process.cwd()
const cargoLockPath = path.join(_dirname, 'Cargo.lock')
const versionWithoutV = newVersion.startsWith('v')
? newVersion.slice(1)
: newVersion
const packageVersionPattern =
/(\[\[package\]\]\r?\nname = "clash-verge"\r?\nversion = )"[^"]+"/
try {
const data = await fs.readFile(cargoLockPath, 'utf8')
const updatedData = data.replace(
packageVersionPattern,
`$1"${versionWithoutV}"`,
)
if (updatedData === data) {
throw new Error('clash-verge package entry was not found in Cargo.lock')
}
await fs.writeFile(cargoLockPath, updatedData, 'utf8')
console.log(`[INFO]: Cargo.lock version updated to: ${versionWithoutV}`)
} catch (error) {
console.error('Error updating Cargo.lock version:', error)
throw error
}
}
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,
)
const updatedData = data.replace(
/^(\s*"version"\s*:\s*)"[^"]+"/m,
`$1"${versionWithoutV}"`,
)
if (updatedData === data) {
throw new Error('version field was not found in tauri.conf.json')
}
await fs.writeFile(tauriConfigPath, updatedData, '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') {
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`
} else if (versionArg.toLowerCase() === 'autobuild-latest') {
const latestTauriCommit = getLatestTauriCommit()
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`
} else if (versionArg.toLowerCase() === 'deploytest') {
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`
} 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 updateCargoLockVersion(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)