release.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. // Publishes a new version (readme §2): npm run release -- <x.y.z> [--no-push] [--skip-checks]
  2. //
  3. // 1. checks (clean tree on main, tests and lint), bumps the version;
  4. // 2. builds the Windows exes (npm run dist);
  5. // 3. stores the game files (app.asar, its unpacked natives, assets/) with a manifest.json in a
  6. // parentless commit at refs/builds/v<x.y.z>, next to the tag v<x.y.z> of the source;
  7. // 4. pushes main, the tag and the build: installed games pick it up at their next launch.
  8. import { execFileSync } from 'node:child_process';
  9. import { createHash } from 'node:crypto';
  10. import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
  11. import { tmpdir } from 'node:os';
  12. import { join, relative, resolve } from 'node:path';
  13. import { BUILD_REF_PREFIX, compareVersions, isVersion, MANIFEST_FILE, type BuildManifest } from '../src/main/updates';
  14. const root = resolve(import.meta.dirname, '..');
  15. const resources = join(root, 'dist', 'win-unpacked', 'resources');
  16. /** What a build carries: everything else in resources/ belongs to the installed runtime. */
  17. const PAYLOAD = ['app.asar', 'app.asar.unpacked', 'assets'];
  18. const args = process.argv.slice(2);
  19. const version = args.find((a) => !a.startsWith('--')) ?? '';
  20. const push = !args.includes('--no-push');
  21. const checks = !args.includes('--skip-checks');
  22. function run(cmd: string, cmdArgs: string[], env?: NodeJS.ProcessEnv): string {
  23. return execFileSync(cmd, cmdArgs, { cwd: root, encoding: 'utf8', env: { ...process.env, ...env }, stdio: ['pipe', 'pipe', 'inherit'] }).trim();
  24. }
  25. const git = (...a: string[]): string => run('git', a);
  26. function npm(...a: string[]): void {
  27. console.log(`\n> npm ${a.join(' ')}`);
  28. execFileSync('npm', a, { cwd: root, stdio: 'inherit', shell: true });
  29. }
  30. function fail(message: string): never {
  31. console.error(`\n✗ ${message}`);
  32. process.exit(1);
  33. }
  34. function listFiles(dir: string): string[] {
  35. return readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
  36. e.isDirectory() ? listFiles(join(dir, e.name)) : [join(dir, e.name)],
  37. );
  38. }
  39. /** Writes the payload into the git object store and returns the commit of the build. */
  40. function commitBuild(): string {
  41. const electron = (JSON.parse(readFileSync(join(root, 'node_modules', 'electron', 'package.json'), 'utf8')) as { version: string }).version;
  42. const files = PAYLOAD.flatMap((p) => {
  43. const abs = join(resources, p);
  44. return statSync(abs).isDirectory() ? listFiles(abs) : [abs];
  45. });
  46. const manifest: BuildManifest = {
  47. version,
  48. electron,
  49. files: files.map((abs) => ({
  50. path: relative(resources, abs).split('\\').join('/'),
  51. size: statSync(abs).size,
  52. sha256: createHash('sha256').update(readFileSync(abs)).digest('hex'),
  53. })),
  54. };
  55. // A throwaway index, so the working tree and the real index stay untouched.
  56. const tmp = mkdtempSync(join(tmpdir(), 'goutermageddon-release-'));
  57. const env = { GIT_INDEX_FILE: join(tmp, 'index') };
  58. try {
  59. const add = (path: string, blob: string): void => {
  60. run('git', ['update-index', '--add', '--cacheinfo', `100644,${blob},${path}`], env);
  61. };
  62. for (const [i, f] of manifest.files.entries()) add(f.path, run('git', ['hash-object', '-w', '--', files[i]]));
  63. const manifestBlob = execFileSync('git', ['hash-object', '-w', '--stdin'], { cwd: root, input: JSON.stringify(manifest, null, 1), encoding: 'utf8' }).trim();
  64. add(MANIFEST_FILE, manifestBlob);
  65. const tree = run('git', ['write-tree'], env);
  66. const size = manifest.files.reduce((s, f) => s + f.size, 0);
  67. console.log(`\nBuild v${version}: ${manifest.files.length} files, ${(size / 1e6).toFixed(1)} MB (Electron ${electron})`);
  68. return git('commit-tree', tree, '-m', `build v${version}`);
  69. } finally {
  70. rmSync(tmp, { recursive: true, force: true });
  71. }
  72. }
  73. const current = (JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { version: string }).version;
  74. if (!isVersion(version)) fail(`usage: npm run release -- <x.y.z> (current version: ${current})`);
  75. if (compareVersions(version, current) <= 0) fail(`${version} must be newer than the current version ${current}`);
  76. if (git('rev-parse', '--abbrev-ref', 'HEAD') !== 'main') fail('releases are made from main');
  77. if (git('status', '--porcelain', '--untracked-files=no')) fail('commit or stash your changes first');
  78. if (git('tag', '--list', `v${version}`)) fail(`tag v${version} already exists`);
  79. if (checks) {
  80. npm('test');
  81. npm('run', 'lint');
  82. }
  83. npm('version', version, '--no-git-tag-version');
  84. try {
  85. npm('run', 'dist');
  86. } catch {
  87. git('checkout', '--', 'package.json', 'package-lock.json');
  88. fail('build failed; version bump undone');
  89. }
  90. const build = commitBuild();
  91. git('commit', '-q', '-m', `chore: release v${version}`, '--', 'package.json', 'package-lock.json');
  92. git('tag', '-a', `v${version}`, '-m', `v${version}`);
  93. git('update-ref', `${BUILD_REF_PREFIX}${version}`, build);
  94. console.log(`Tag v${version} and ${BUILD_REF_PREFIX}${version} (${build.slice(0, 7)}) created.`);
  95. if (push) {
  96. console.log('\n> git push');
  97. execFileSync('git', ['push', 'origin', 'main', `v${version}`, `${BUILD_REF_PREFIX}${version}`], { cwd: root, stdio: 'inherit' });
  98. console.log(`\n✓ v${version} published: installed games update at their next launch.`);
  99. } else {
  100. console.log(`\nNot pushed. To publish: git push origin main v${version} ${BUILD_REF_PREFIX}${version}`);
  101. }
  102. console.log(`First install for a new player: dist/Goutermageddon-${version}-setup.exe (or -portable.exe).`);