ci(release): automate app versioning from Git tags

This commit is contained in:
lzt404 committed 2026-08-05 15:37:43 +08:00
1 parent 7f2876c1ec
commit 391fc877a7
9 files changed
+250 -54

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
import { readFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const semverPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
export function validateAppVersion(value, source = 'app version') {
const version = String(value).trim();
if (!semverPattern.test(version)) {
throw new Error(`Invalid ${source}: ${version}`);
}
return version;
}
export function parseCargoPackageVersion(contents, source = 'Cargo.toml') {
const packageSection = contents.match(/(?:^|\r?\n)\[package\][ \t]*\r?\n([\s\S]*?)(?=\r?\n\[|$)/);
if (!packageSection) {
throw new Error(`Missing [package] section in ${source}`);
}
const versionMatch = packageSection[1].match(/^[ \t]*version[ \t]*=[ \t]*"([^"]+)"[ \t]*(?:#.*)?$/m);
if (!versionMatch) {
throw new Error(`Missing package version in ${source}`);
}
return validateAppVersion(versionMatch[1], `package version in ${source}`);
}
export function setCargoPackageVersion(contents, version, source = 'Cargo.toml') {
const nextVersion = validateAppVersion(version);
const packageSection = contents.match(/(?:^|\r?\n)\[package\][ \t]*\r?\n([\s\S]*?)(?=\r?\n\[|$)/);
if (!packageSection) {
throw new Error(`Missing [package] section in ${source}`);
}
const versionPattern = /^[ \t]*version[ \t]*=[ \t]*"[^"]+"[ \t]*(?:#.*)?$/m;
if (!versionPattern.test(packageSection[0])) {
throw new Error(`Missing package version in ${source}`);
}
const updatedSection = packageSection[0].replace(versionPattern, `version = "${nextVersion}"`);
return contents.replace(packageSection[0], updatedSection);
}
export function setCargoLockPackageVersion(contents, packageName, version, source = 'Cargo.lock') {
const nextVersion = validateAppVersion(version);
const packageBlocks = contents.match(/\[\[package\]\]\r?\n[\s\S]*?(?=\r?\n\[\[package\]\]|$)/g) ?? [];
const matchingBlocks = packageBlocks.filter((block) => (
new RegExp(`^name = "${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"$`, 'm').test(block)
&& !/^source = /m.test(block)
));
if (matchingBlocks.length !== 1) {
throw new Error(`Expected one local ${packageName} package in ${source}, found ${matchingBlocks.length}`);
}
const versionPattern = /^version = "[^"]+"$/m;
if (!versionPattern.test(matchingBlocks[0])) {
throw new Error(`Missing ${packageName} version in ${source}`);
}
const updatedBlock = matchingBlocks[0].replace(versionPattern, `version = "${nextVersion}"`);
return contents.replace(matchingBlocks[0], updatedBlock);
}
export async function readAppVersion() {
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const manifest = join(root, 'src-tauri', 'Cargo.toml');
return parseCargoPackageVersion(await readFile(manifest, 'utf8'), manifest);
}
const entryPoint = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : '';
if (import.meta.url === entryPoint) {
console.log(process.argv[2]
? validateAppVersion(process.argv[2], 'app version argument')
: await readAppVersion());
}
+2 -6
View File
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
import { existsSync } from 'node:fs';
import { chmod, copyFile, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { basename, join, resolve } from 'node:path';
import { readAppVersion } from './app-version.mjs';
const args = new Map();
for (let index = 2; index < process.argv.length; index += 2) {
@@ -22,12 +23,7 @@ if (!existsSync(binary)) throw new Error(`GUI binary not found: ${binary}`);
const rawVersion = (await readFile(join(root, 'core-version.txt'), 'utf8')).trim();
if (!/^v?\d+(?:\.\d+)+$/.test(rawVersion)) throw new Error(`Invalid core-version.txt: ${rawVersion}`);
const version = rawVersion.replace(/^v/i, '');
const packageMetadata = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
const appVersion = String(packageMetadata.version ?? '').trim();
const semverPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
if (!semverPattern.test(appVersion)) {
throw new Error(`Invalid package.json version: ${appVersion}`);
}
const appVersion = await readAppVersion();
const extension = targetOS === 'windows' ? 'zip' : 'tar.gz';
const assetName = `CLIProxyAPI_${version}_${targetOS}_${targetArch}.${extension}`;
const sourceDir = join(root, 'cpa-core');
+45
View File
@@ -0,0 +1,45 @@
import { spawnSync } from 'node:child_process';
import { readFile, writeFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
parseCargoPackageVersion,
setCargoLockPackageVersion,
setCargoPackageVersion,
validateAppVersion,
} from './app-version.mjs';
const requestedVersion = process.argv[2];
if (!requestedVersion) {
throw new Error('Usage: node scripts/set-app-version.mjs <semver>');
}
const version = validateAppVersion(requestedVersion, 'requested app version');
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const manifest = join(root, 'src-tauri', 'Cargo.toml');
const lockfile = join(root, 'src-tauri', 'Cargo.lock');
const contents = await readFile(manifest, 'utf8');
const lockContents = await readFile(lockfile, 'utf8');
const previousVersion = parseCargoPackageVersion(contents, manifest);
if (previousVersion !== version) {
await Promise.all([
writeFile(manifest, setCargoPackageVersion(contents, version, manifest)),
writeFile(lockfile, setCargoLockPackageVersion(lockContents, 'cpa-gui', version, lockfile)),
]);
}
const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo';
const metadata = spawnSync(cargo, [
'metadata',
'--manifest-path', manifest,
'--no-deps',
'--format-version', '1',
], { stdio: 'ignore' });
if (metadata.error) throw metadata.error;
if (metadata.status !== 0) {
throw new Error(`cargo metadata failed with exit code ${metadata.status}`);
}
console.log(`Applied app version ${version}${previousVersion === version ? ' (unchanged)' : ''}`);