prepare-release.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. #!/usr/bin/env node
  2. require('colors')
  3. const args = require('minimist')(process.argv.slice(2), {
  4. boolean: ['automaticRelease', 'notesOnly', 'stable']
  5. })
  6. const assert = require('assert')
  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 pkg = require('../package.json')
  15. const readline = require('readline')
  16. const versionType = args._[0]
  17. // TODO (future) automatically determine version based on conventional commits
  18. // via conventional-recommended-bump
  19. assert(process.env.ELECTRON_GITHUB_TOKEN, 'ELECTRON_GITHUB_TOKEN not found in environment')
  20. if (!versionType && !args.notesOnly) {
  21. console.log(`Usage: prepare-release versionType [major | minor | patch | beta | specificVersion]` +
  22. ` (--stable) (--notesOnly) (--automaticRelease) (--branch) (--version)`)
  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. function getNewVersion (dryRun) {
  29. console.log(`Bumping for new "${versionType}" version.`)
  30. let bumpScript = path.join(__dirname, 'bump-version.py')
  31. let scriptArgs = [bumpScript]
  32. if (versionType === 'specificVersion' && args.version) {
  33. scriptArgs.push(`--version ${args.version}`)
  34. } else {
  35. scriptArgs.push(`--bump ${versionType}`)
  36. }
  37. if (args.stable) {
  38. scriptArgs.push('--stable')
  39. }
  40. if (dryRun) {
  41. scriptArgs.push('--dry-run')
  42. }
  43. try {
  44. let bumpVersion = execSync(scriptArgs.join(' '), {encoding: 'UTF-8'})
  45. bumpVersion = bumpVersion.substr(bumpVersion.indexOf(':') + 1).trim()
  46. let newVersion = `v${bumpVersion}`
  47. if (!dryRun) {
  48. console.log(`${pass} Successfully bumped version to ${newVersion}`)
  49. }
  50. return newVersion
  51. } catch (err) {
  52. console.log(`${fail} Could not bump version, error was:`, err)
  53. }
  54. }
  55. async function getCurrentBranch (gitDir) {
  56. console.log(`Determining current git branch`)
  57. let gitArgs = ['rev-parse', '--abbrev-ref', 'HEAD']
  58. let branchDetails = await GitProcess.exec(gitArgs, gitDir)
  59. if (branchDetails.exitCode === 0) {
  60. let currentBranch = branchDetails.stdout.trim()
  61. console.log(`${pass} Successfully determined current git branch is ` +
  62. `${currentBranch}`)
  63. return currentBranch
  64. } else {
  65. let error = GitProcess.parseError(branchDetails.stderr)
  66. console.log(`${fail} Could not get details for the current branch,
  67. error was ${branchDetails.stderr}`, error)
  68. process.exit(1)
  69. }
  70. }
  71. async function getReleaseNotes (currentBranch) {
  72. console.log(`Generating release notes for ${currentBranch}.`)
  73. let githubOpts = {
  74. owner: 'electron',
  75. repo: 'electron',
  76. base: `v${pkg.version}`,
  77. head: currentBranch
  78. }
  79. let releaseNotes
  80. if (args.automaticRelease) {
  81. releaseNotes = '## Bug Fixes/Changes \n\n'
  82. } else {
  83. releaseNotes = '(placeholder)\n'
  84. }
  85. console.log(`Checking for commits from ${pkg.version} to ${currentBranch}`)
  86. let commitComparison = await github.repos.compareCommits(githubOpts)
  87. .catch(err => {
  88. console.log(`${fail} Error checking for commits from ${pkg.version} to ` +
  89. `${currentBranch}`, err)
  90. process.exit(1)
  91. })
  92. if (commitComparison.data.commits.length === 0) {
  93. console.log(`${pass} There are no commits from ${pkg.version} to ` +
  94. `${currentBranch}, skipping release.`)
  95. process.exit(0)
  96. }
  97. let prCount = 0
  98. const mergeRE = /Merge pull request #(\d+) from .*\n/
  99. const newlineRE = /(.*)\n*.*/
  100. const prRE = /(.* )\(#(\d+)\)(?:.*)/
  101. commitComparison.data.commits.forEach(commitEntry => {
  102. let commitMessage = commitEntry.commit.message
  103. if (commitMessage.indexOf('#') > -1) {
  104. let prMatch = commitMessage.match(mergeRE)
  105. let prNumber
  106. if (prMatch) {
  107. commitMessage = commitMessage.replace(mergeRE, '').replace('\n', '')
  108. let newlineMatch = commitMessage.match(newlineRE)
  109. if (newlineMatch) {
  110. commitMessage = newlineMatch[1]
  111. }
  112. prNumber = prMatch[1]
  113. } else {
  114. prMatch = commitMessage.match(prRE)
  115. if (prMatch) {
  116. commitMessage = prMatch[1].trim()
  117. prNumber = prMatch[2]
  118. }
  119. }
  120. if (prMatch) {
  121. if (commitMessage.substr(commitMessage.length - 1, commitMessage.length) !== '.') {
  122. commitMessage += '.'
  123. }
  124. releaseNotes += `* ${commitMessage} #${prNumber} \n\n`
  125. prCount++
  126. }
  127. }
  128. })
  129. console.log(`${pass} Done generating release notes for ${currentBranch}. Found ${prCount} PRs.`)
  130. return releaseNotes
  131. }
  132. async function createRelease (branchToTarget, isBeta) {
  133. let releaseNotes = await getReleaseNotes(branchToTarget)
  134. let newVersion = getNewVersion()
  135. await tagRelease(newVersion)
  136. const githubOpts = {
  137. owner: 'electron',
  138. repo: 'electron'
  139. }
  140. console.log(`Checking for existing draft release.`)
  141. let releases = await github.repos.getReleases(githubOpts)
  142. .catch(err => {
  143. console.log('$fail} Could not get releases. Error was', err)
  144. })
  145. let drafts = releases.data.filter(release => release.draft &&
  146. release.tag_name === newVersion)
  147. if (drafts.length > 0) {
  148. console.log(`${fail} Aborting because draft release for
  149. ${drafts[0].tag_name} already exists.`)
  150. process.exit(1)
  151. }
  152. console.log(`${pass} A draft release does not exist; creating one.`)
  153. githubOpts.draft = true
  154. githubOpts.name = `electron ${newVersion}`
  155. if (isBeta) {
  156. githubOpts.body = `Note: This is a beta release. Please file new issues ` +
  157. `for any bugs you find in it.\n \n This release is published to npm ` +
  158. `under the beta tag and can be installed via npm install electron@beta, ` +
  159. `or npm i electron@${newVersion.substr(1)}.\n \n ${releaseNotes}`
  160. githubOpts.name = `${githubOpts.name}`
  161. githubOpts.prerelease = true
  162. } else {
  163. githubOpts.body = releaseNotes
  164. }
  165. githubOpts.tag_name = newVersion
  166. githubOpts.target_commitish = branchToTarget
  167. await github.repos.createRelease(githubOpts)
  168. .catch(err => {
  169. console.log(`${fail} Error creating new release: `, err)
  170. process.exit(1)
  171. })
  172. console.log(`${pass} Draft release for ${newVersion} has been created.`)
  173. }
  174. async function pushRelease (branch) {
  175. let pushDetails = await GitProcess.exec(['push', 'origin', `HEAD:${branch}`, '--follow-tags'], gitDir)
  176. if (pushDetails.exitCode === 0) {
  177. console.log(`${pass} Successfully pushed the release. Wait for ` +
  178. `release builds to finish before running "npm run release".`)
  179. } else {
  180. console.log(`${fail} Error pushing the release: ` +
  181. `${pushDetails.stderr}`)
  182. process.exit(1)
  183. }
  184. }
  185. async function runReleaseBuilds (branch) {
  186. await ciReleaseBuild(branch, {
  187. ghRelease: true,
  188. automaticRelease: args.automaticRelease
  189. })
  190. }
  191. async function tagRelease (version) {
  192. console.log(`Tagging release ${version}.`)
  193. let checkoutDetails = await GitProcess.exec([ 'tag', '-a', '-m', version, version ], gitDir)
  194. if (checkoutDetails.exitCode === 0) {
  195. console.log(`${pass} Successfully tagged ${version}.`)
  196. } else {
  197. console.log(`${fail} Error tagging ${version}: ` +
  198. `${checkoutDetails.stderr}`)
  199. process.exit(1)
  200. }
  201. }
  202. async function verifyNewVersion () {
  203. let newVersion = getNewVersion(true)
  204. let response
  205. if (args.automaticRelease) {
  206. response = 'y'
  207. } else {
  208. response = await promptForVersion(newVersion)
  209. }
  210. if (response.match(/^y/i)) {
  211. console.log(`${pass} Starting release of ${newVersion}`)
  212. } else {
  213. console.log(`${fail} Aborting release of ${newVersion}`)
  214. process.exit()
  215. }
  216. }
  217. async function promptForVersion (version) {
  218. return new Promise((resolve, reject) => {
  219. const rl = readline.createInterface({
  220. input: process.stdin,
  221. output: process.stdout
  222. })
  223. rl.question(`Do you want to create the release ${version.green} (y/N)? `, (answer) => {
  224. rl.close()
  225. resolve(answer)
  226. })
  227. })
  228. }
  229. async function prepareRelease (isBeta, notesOnly) {
  230. if (args.automaticRelease && (pkg.version.indexOf('beta') === -1 ||
  231. versionType !== 'beta')) {
  232. console.log(`${fail} Automatic release is only supported for beta releases`)
  233. process.exit(1)
  234. }
  235. let currentBranch
  236. if (args.branch) {
  237. currentBranch = args.branch
  238. } else {
  239. currentBranch = await getCurrentBranch(gitDir)
  240. }
  241. if (notesOnly) {
  242. let releaseNotes = await getReleaseNotes(currentBranch)
  243. console.log(`Draft release notes are: \n${releaseNotes}`)
  244. } else {
  245. await verifyNewVersion()
  246. await createRelease(currentBranch, isBeta)
  247. await pushRelease(currentBranch)
  248. await runReleaseBuilds(currentBranch)
  249. }
  250. }
  251. prepareRelease(!args.stable, args.notesOnly)