ci(release): automate app versioning from Git tags
This commit is contained in:
1 parent
7f2876c1ec
commit
391fc877a7
9 files changed
+250
-54
No files matched your search
@@ -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 +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');
|
||||
|
||||
@@ -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)' : ''}`);
|
||||
Reference in new issue
Block a user