prepare-release.js 8.0 KB

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