|
@@ -0,0 +1,111 @@
|
|
|
|
|
+// 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 } 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(' ')}`);
|
|
|
|
|
+ execFileSync('npm', a, { cwd: root, stdio: 'inherit', shell: true });
|
|
|
|
|
+}
|
|
|
|
|
+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', '--', files[i]]));
|
|
|
|
|
+ const manifestBlob = execFileSync('git', ['hash-object', '-w', '--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).`);
|