prepare-release.js 8.0 KB

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