| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 |
- // Publishes a new version (readme §2): npm run release -- <x.y.z> [--no-push] [--skip-checks]
- //
- // 1. checks (clean tree on main, tests and lint), bumps the version;
- // 2. builds the Windows exes (npm run dist);
- // 3. stores the game files (app.asar, its unpacked natives, assets/) with a manifest.json in a
- // parentless commit at refs/builds/v<x.y.z>, next to the tag v<x.y.z> of the source;
- // 4. pushes main, the tag and the build: installed games pick it up at their next launch.
- import { execFileSync, execSync } from 'node:child_process';
- import { createHash } from 'node:crypto';
- import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
- import { tmpdir } from 'node:os';
- import { join, relative, resolve } from 'node:path';
- import { BUILD_REF_PREFIX, compareVersions, isVersion, MANIFEST_FILE, type BuildManifest } from '../src/main/updates';
- const root = resolve(import.meta.dirname, '..');
- const resources = join(root, 'dist', 'win-unpacked', 'resources');
- /** What a build carries: everything else in resources/ belongs to the installed runtime. */
- const PAYLOAD = ['app.asar', 'app.asar.unpacked', 'assets'];
- const args = process.argv.slice(2);
- const version = args.find((a) => !a.startsWith('--')) ?? '';
- const push = !args.includes('--no-push');
- const checks = !args.includes('--skip-checks');
- function run(cmd: string, cmdArgs: string[], env?: NodeJS.ProcessEnv): string {
- return execFileSync(cmd, cmdArgs, { cwd: root, encoding: 'utf8', env: { ...process.env, ...env }, stdio: ['pipe', 'pipe', 'inherit'] }).trim();
- }
- const git = (...a: string[]): string => run('git', a);
- function npm(...a: string[]): void {
- console.log(`\n> npm ${a.join(' ')}`);
- execSync(`npm ${a.join(' ')}`, { cwd: root, stdio: 'inherit' });
- }
- function fail(message: string): never {
- console.error(`\n✗ ${message}`);
- process.exit(1);
- }
- function listFiles(dir: string): string[] {
- return readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
- e.isDirectory() ? listFiles(join(dir, e.name)) : [join(dir, e.name)],
- );
- }
- /** Writes the payload into the git object store and returns the commit of the build. */
- function commitBuild(): string {
- const electron = (JSON.parse(readFileSync(join(root, 'node_modules', 'electron', 'package.json'), 'utf8')) as { version: string }).version;
- const files = PAYLOAD.flatMap((p) => {
- const abs = join(resources, p);
- return statSync(abs).isDirectory() ? listFiles(abs) : [abs];
- });
- const manifest: BuildManifest = {
- version,
- electron,
- files: files.map((abs) => ({
- path: relative(resources, abs).split('\\').join('/'),
- size: statSync(abs).size,
- sha256: createHash('sha256').update(readFileSync(abs)).digest('hex'),
- })),
- };
- // A throwaway index, so the working tree and the real index stay untouched.
- const tmp = mkdtempSync(join(tmpdir(), 'goutermageddon-release-'));
- const env = { GIT_INDEX_FILE: join(tmp, 'index') };
- try {
- const add = (path: string, blob: string): void => {
- run('git', ['update-index', '--add', '--cacheinfo', `100644,${blob},${path}`], env);
- };
- for (const [i, f] of manifest.files.entries()) add(f.path, run('git', ['hash-object', '-w', '--no-filters', '--', files[i]]));
- const manifestBlob = execFileSync('git', ['hash-object', '-w', '--no-filters', '--stdin'], { cwd: root, input: JSON.stringify(manifest, null, 1), encoding: 'utf8' }).trim();
- add(MANIFEST_FILE, manifestBlob);
- const tree = run('git', ['write-tree'], env);
- const size = manifest.files.reduce((s, f) => s + f.size, 0);
- console.log(`\nBuild v${version}: ${manifest.files.length} files, ${(size / 1e6).toFixed(1)} MB (Electron ${electron})`);
- return git('commit-tree', tree, '-m', `build v${version}`);
- } finally {
- rmSync(tmp, { recursive: true, force: true });
- }
- }
- const current = (JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { version: string }).version;
- if (!isVersion(version)) fail(`usage: npm run release -- <x.y.z> (current version: ${current})`);
- if (compareVersions(version, current) <= 0) fail(`${version} must be newer than the current version ${current}`);
- if (git('rev-parse', '--abbrev-ref', 'HEAD') !== 'main') fail('releases are made from main');
- if (git('status', '--porcelain', '--untracked-files=no')) fail('commit or stash your changes first');
- if (git('tag', '--list', `v${version}`)) fail(`tag v${version} already exists`);
- if (checks) {
- npm('test');
- npm('run', 'lint');
- }
- npm('version', version, '--no-git-tag-version');
- try {
- npm('run', 'dist');
- } catch {
- git('checkout', '--', 'package.json', 'package-lock.json');
- fail('build failed; version bump undone');
- }
- const build = commitBuild();
- git('commit', '-q', '-m', `chore: release v${version}`, '--', 'package.json', 'package-lock.json');
- git('tag', '-a', `v${version}`, '-m', `v${version}`);
- git('update-ref', `${BUILD_REF_PREFIX}${version}`, build);
- console.log(`Tag v${version} and ${BUILD_REF_PREFIX}${version} (${build.slice(0, 7)}) created.`);
- if (push) {
- console.log('\n> git push');
- execFileSync('git', ['push', 'origin', 'main', `v${version}`, `${BUILD_REF_PREFIX}${version}`], { cwd: root, stdio: 'inherit' });
- console.log(`\n✓ v${version} published: installed games update at their next launch.`);
- } else {
- console.log(`\nNot pushed. To publish: git push origin main v${version} ${BUILD_REF_PREFIX}${version}`);
- }
- console.log(`First install for a new player: dist/Goutermageddon-${version}-setup.exe (or -portable.exe).`);
|