prepare-release.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. #!/usr/bin/env node
  2. if (!process.env.CI) require('dotenv-safe').load();
  3. const args = require('minimist')(process.argv.slice(2), {
  4. boolean: ['automaticRelease', 'notesOnly', 'stable']
  5. });
  6. const ciReleaseBuild = require('./ci-release-build');
  7. const { Octokit } = require('@octokit/rest');
  8. const { execSync } = require('node:child_process');
  9. const { GitProcess } = require('dugite');
  10. const path = require('node:path');
  11. const readline = require('node:readline');
  12. const releaseNotesGenerator = require('./notes/index.js');
  13. const { getCurrentBranch, ELECTRON_DIR } = require('../lib/utils.js');
  14. const bumpType = args._[0];
  15. const targetRepo = bumpType === 'nightly' ? 'nightlies' : 'electron';
  16. const octokit = new Octokit({
  17. auth: process.env.ELECTRON_GITHUB_TOKEN
  18. });
  19. require('colors');
  20. const pass = '✓'.green;
  21. const fail = '✗'.red;
  22. if (!bumpType && !args.notesOnly) {
  23. console.log('Usage: prepare-release [stable | minor | beta | alpha | nightly]' +
  24. ' (--stable) (--notesOnly) (--automaticRelease) (--branch)');
  25. process.exit(1);
  26. }
  27. async function getNewVersion (dryRun) {
  28. if (!dryRun) {
  29. console.log(`Bumping for new "${bumpType}" version.`);
  30. }
  31. const bumpScript = path.join(__dirname, 'version-bumper.js');
  32. const scriptArgs = ['node', bumpScript, `--bump=${bumpType}`];
  33. if (dryRun) scriptArgs.push('--dryRun');
  34. try {
  35. let bumpVersion = execSync(scriptArgs.join(' '), { encoding: 'UTF-8' });
  36. bumpVersion = bumpVersion.substr(bumpVersion.indexOf(':') + 1).trim();
  37. const newVersion = `v${bumpVersion}`;
  38. if (!dryRun) {
  39. console.log(`${pass} Successfully bumped version to ${newVersion}`);
  40. }
  41. return newVersion;
  42. } catch (err) {
  43. console.log(`${fail} Could not bump version, error was:`, err);
  44. throw err;
  45. }
  46. }
  47. async function getReleaseNotes (currentBranch, newVersion) {
  48. if (bumpType === 'nightly') {
  49. return { text: 'Nightlies do not get release notes, please compare tags for info.' };
  50. }
  51. console.log(`Generating release notes for ${currentBranch}.`);
  52. const releaseNotes = await releaseNotesGenerator(currentBranch, newVersion);
  53. if (releaseNotes.warning) {
  54. console.warn(releaseNotes.warning);
  55. }
  56. return releaseNotes;
  57. }
  58. async function createRelease (branchToTarget, isBeta) {
  59. const newVersion = await getNewVersion();
  60. const releaseNotes = await getReleaseNotes(branchToTarget, newVersion);
  61. await tagRelease(newVersion);
  62. console.log('Checking for existing draft release.');
  63. const releases = await octokit.repos.listReleases({
  64. owner: 'electron',
  65. repo: targetRepo
  66. }).catch(err => {
  67. console.log(`${fail} Could not get releases. Error was: `, err);
  68. });
  69. const drafts = releases.data.filter(release => release.draft &&
  70. release.tag_name === newVersion);
  71. if (drafts.length > 0) {
  72. console.log(`${fail} Aborting because draft release for
  73. ${drafts[0].tag_name} already exists.`);
  74. process.exit(1);
  75. }
  76. console.log(`${pass} A draft release does not exist; creating one.`);
  77. let releaseBody;
  78. let releaseIsPrelease = false;
  79. if (isBeta) {
  80. if (newVersion.indexOf('nightly') > 0) {
  81. releaseBody = 'Note: This is a nightly release. Please file new issues ' +
  82. 'for any bugs you find in it.\n \n This release is published to npm ' +
  83. 'under the electron-nightly package and can be installed via `npm install electron-nightly`, ' +
  84. `or \`npm install electron-nightly@${newVersion.substr(1)}\`.\n \n ${releaseNotes.text}`;
  85. } else if (newVersion.indexOf('alpha') > 0) {
  86. releaseBody = 'Note: This is an alpha release. Please file new issues ' +
  87. 'for any bugs you find in it.\n \n This release is published to npm ' +
  88. 'under the alpha tag and can be installed via `npm install electron@alpha`, ' +
  89. `or \`npm install electron@${newVersion.substr(1)}\`.\n \n ${releaseNotes.text}`;
  90. } else {
  91. releaseBody = 'Note: This is a beta release. Please file new issues ' +
  92. 'for any bugs you find in it.\n \n This release is published to npm ' +
  93. 'under the beta tag and can be installed via `npm install electron@beta`, ' +
  94. `or \`npm install electron@${newVersion.substr(1)}\`.\n \n ${releaseNotes.text}`;
  95. }
  96. releaseIsPrelease = true;
  97. } else {
  98. releaseBody = releaseNotes.text;
  99. }
  100. const release = await octokit.repos.createRelease({
  101. owner: 'electron',
  102. repo: targetRepo,
  103. tag_name: newVersion,
  104. draft: true,
  105. name: `electron ${newVersion}`,
  106. body: releaseBody,
  107. prerelease: releaseIsPrelease,
  108. target_commitish: newVersion.includes('nightly') ? 'main' : branchToTarget
  109. }).catch(err => {
  110. console.log(`${fail} Error creating new release: `, err);
  111. process.exit(1);
  112. });
  113. console.log(`Release has been created with id: ${release.data.id}.`);
  114. console.log(`${pass} Draft release for ${newVersion} successful.`);
  115. }
  116. async function pushRelease (branch) {
  117. const pushDetails = await GitProcess.exec(['push', 'origin', `HEAD:${branch}`, '--follow-tags'], ELECTRON_DIR);
  118. if (pushDetails.exitCode === 0) {
  119. console.log(`${pass} Successfully pushed the release. Wait for ` +
  120. 'release builds to finish before running "npm run release".');
  121. } else {
  122. console.log(`${fail} Error pushing the release: ${pushDetails.stderr}`);
  123. process.exit(1);
  124. }
  125. }
  126. async function runReleaseBuilds (branch) {
  127. await ciReleaseBuild(branch, {
  128. ghRelease: true
  129. });
  130. }
  131. async function tagRelease (version) {
  132. console.log(`Tagging release ${version}.`);
  133. const checkoutDetails = await GitProcess.exec(['tag', '-a', '-m', version, version], ELECTRON_DIR);
  134. if (checkoutDetails.exitCode === 0) {
  135. console.log(`${pass} Successfully tagged ${version}.`);
  136. } else {
  137. console.log(`${fail} Error tagging ${version}: ` +
  138. `${checkoutDetails.stderr}`);
  139. process.exit(1);
  140. }
  141. }
  142. async function verifyNewVersion () {
  143. const newVersion = await getNewVersion(true);
  144. let response;
  145. if (args.automaticRelease) {
  146. response = 'y';
  147. } else {
  148. response = await promptForVersion(newVersion);
  149. }
  150. if (response.match(/^y/i)) {
  151. console.log(`${pass} Starting release of ${newVersion}`);
  152. } else {
  153. console.log(`${fail} Aborting release of ${newVersion}`);
  154. process.exit();
  155. }
  156. }
  157. async function promptForVersion (version) {
  158. return new Promise(resolve => {
  159. const rl = readline.createInterface({
  160. input: process.stdin,
  161. output: process.stdout
  162. });
  163. rl.question(`Do you want to create the release ${version.green} (y/N)? `, (answer) => {
  164. rl.close();
  165. resolve(answer);
  166. });
  167. });
  168. }
  169. // function to determine if there have been commits to main since the last release
  170. async function changesToRelease () {
  171. const lastCommitWasRelease = /^Bump v[0-9]+.[0-9]+.[0-9]+(-beta.[0-9]+)?(-alpha.[0-9]+)?(-nightly.[0-9]+)?$/g;
  172. const lastCommit = await GitProcess.exec(['log', '-n', '1', '--pretty=format:\'%s\''], ELECTRON_DIR);
  173. return !lastCommitWasRelease.test(lastCommit.stdout);
  174. }
  175. async function prepareRelease (isBeta, notesOnly) {
  176. if (args.dryRun) {
  177. const newVersion = await getNewVersion(true);
  178. console.log(newVersion);
  179. } else {
  180. const currentBranch = (args.branch) ? args.branch : await getCurrentBranch(ELECTRON_DIR);
  181. if (notesOnly) {
  182. const newVersion = await getNewVersion(true);
  183. const releaseNotes = await getReleaseNotes(currentBranch, newVersion);
  184. console.log(`Draft release notes are: \n${releaseNotes.text}`);
  185. } else {
  186. const changes = await changesToRelease();
  187. if (changes) {
  188. await verifyNewVersion();
  189. await createRelease(currentBranch, isBeta);
  190. await pushRelease(currentBranch);
  191. await runReleaseBuilds(currentBranch);
  192. } else {
  193. console.log('There are no new changes to this branch since the last release, aborting release.');
  194. process.exit(1);
  195. }
  196. }
  197. }
  198. }
  199. prepareRelease(!args.stable, args.notesOnly)
  200. .catch((err) => {
  201. console.error(err);
  202. process.exit(1);
  203. });