prepare-release.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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 pkg = require('../package.json')
  15. const readline = require('readline')
  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. let bumpScript = path.join(__dirname, 'bump-version.py')
  33. let 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. let 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. let gitArgs = ['rev-parse', '--abbrev-ref', 'HEAD']
  53. let branchDetails = await GitProcess.exec(gitArgs, gitDir)
  54. if (branchDetails.exitCode === 0) {
  55. let currentBranch = branchDetails.stdout.trim()
  56. console.log(`${pass} Successfully determined current git branch is ` +
  57. `${currentBranch}`)
  58. return currentBranch
  59. } else {
  60. let 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) {
  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. let githubOpts = {
  72. owner: 'electron',
  73. repo: targetRepo,
  74. base: `v${pkg.version}`,
  75. head: currentBranch
  76. }
  77. let releaseNotes
  78. if (args.automaticRelease) {
  79. releaseNotes = '## Bug Fixes/Changes \n\n'
  80. } else {
  81. releaseNotes = '(placeholder)\n'
  82. }
  83. console.log(`Checking for commits from ${pkg.version} to ${currentBranch}`)
  84. let commitComparison = await github.repos.compareCommits(githubOpts)
  85. .catch(err => {
  86. console.log(`${fail} Error checking for commits from ${pkg.version} to ` +
  87. `${currentBranch}`, err)
  88. process.exit(1)
  89. })
  90. if (commitComparison.data.commits.length === 0) {
  91. console.log(`${pass} There are no commits from ${pkg.version} to ` +
  92. `${currentBranch}, skipping release.`)
  93. process.exit(0)
  94. }
  95. let prCount = 0
  96. const mergeRE = /Merge pull request #(\d+) from .*\n/
  97. const newlineRE = /(.*)\n*.*/
  98. const prRE = /(.* )\(#(\d+)\)(?:.*)/
  99. commitComparison.data.commits.forEach(commitEntry => {
  100. let commitMessage = commitEntry.commit.message
  101. if (commitMessage.indexOf('#') > -1) {
  102. let prMatch = commitMessage.match(mergeRE)
  103. let prNumber
  104. if (prMatch) {
  105. commitMessage = commitMessage.replace(mergeRE, '').replace('\n', '')
  106. let newlineMatch = commitMessage.match(newlineRE)
  107. if (newlineMatch) {
  108. commitMessage = newlineMatch[1]
  109. }
  110. prNumber = prMatch[1]
  111. } else {
  112. prMatch = commitMessage.match(prRE)
  113. if (prMatch) {
  114. commitMessage = prMatch[1].trim()
  115. prNumber = prMatch[2]
  116. }
  117. }
  118. if (prMatch) {
  119. if (commitMessage.substr(commitMessage.length - 1, commitMessage.length) !== '.') {
  120. commitMessage += '.'
  121. }
  122. releaseNotes += `* ${commitMessage} #${prNumber} \n\n`
  123. prCount++
  124. }
  125. }
  126. })
  127. console.log(`${pass} Done generating release notes for ${currentBranch}. Found ${prCount} PRs.`)
  128. return releaseNotes
  129. }
  130. async function createRelease (branchToTarget, isBeta) {
  131. console.log('creating release for', branchToTarget, `isBeta=${isBeta}`)
  132. let releaseNotes = await getReleaseNotes(branchToTarget)
  133. let newVersion = await getNewVersion()
  134. await tagRelease(newVersion)
  135. const githubOpts = {
  136. owner: 'electron',
  137. repo: targetRepo
  138. }
  139. console.log(`Checking for existing draft release.`)
  140. let releases = await github.repos.getReleases(githubOpts)
  141. .catch(err => {
  142. console.log(`${fail} Could not get releases. Error was: `, err)
  143. })
  144. let drafts = releases.data.filter(release => release.draft &&
  145. release.tag_name === newVersion)
  146. if (drafts.length > 0) {
  147. console.log(`${fail} Aborting because draft release for
  148. ${drafts[0].tag_name} already exists.`)
  149. process.exit(1)
  150. }
  151. console.log(`${pass} A draft release does not exist; creating one.`)
  152. githubOpts.draft = true
  153. githubOpts.name = `electron ${newVersion}`
  154. if (isBeta) {
  155. if (newVersion.indexOf('nightly') > 0) {
  156. githubOpts.body = `Note: This is a nightly release. Please file new issues ` +
  157. `for any bugs you find in it.\n \n This release is published to npm ` +
  158. `under the nightly tag and can be installed via npm install electron@nightly, ` +
  159. `or npm i electron@${newVersion.substr(1)}.\n \n ${releaseNotes}`
  160. } else {
  161. githubOpts.body = `Note: This is a beta release. Please file new issues ` +
  162. `for any bugs you find in it.\n \n This release is published to npm ` +
  163. `under the beta tag and can be installed via npm install electron@beta, ` +
  164. `or npm i electron@${newVersion.substr(1)}.\n \n ${releaseNotes}`
  165. }
  166. githubOpts.name = `${githubOpts.name}`
  167. githubOpts.prerelease = true
  168. } else {
  169. githubOpts.body = releaseNotes
  170. }
  171. githubOpts.tag_name = newVersion
  172. githubOpts.target_commitish = newVersion.indexOf('nightly') !== -1 ? 'master' : branchToTarget
  173. const release = await github.repos.createRelease(githubOpts)
  174. .catch(err => {
  175. console.log(`${fail} Error creating new release: `, err)
  176. process.exit(1)
  177. })
  178. console.log(`Release has been created with id: ${release.data.id}.`)
  179. console.log(`${pass} Draft release for ${newVersion} successful.`)
  180. }
  181. async function pushRelease (branch) {
  182. let pushDetails = await GitProcess.exec(['push', 'origin', `HEAD:${branch}`, '--follow-tags'], gitDir)
  183. if (pushDetails.exitCode === 0) {
  184. console.log(`${pass} Successfully pushed the release. Wait for ` +
  185. `release builds to finish before running "npm run release".`)
  186. } else {
  187. console.log(`${fail} Error pushing the release: ` +
  188. `${pushDetails.stderr}`)
  189. process.exit(1)
  190. }
  191. }
  192. async function runReleaseBuilds (branch) {
  193. await ciReleaseBuild(branch, {
  194. ghRelease: true,
  195. automaticRelease: args.automaticRelease
  196. })
  197. }
  198. async function tagRelease (version) {
  199. console.log(`Tagging release ${version}.`)
  200. let checkoutDetails = await GitProcess.exec([ 'tag', '-a', '-m', version, version ], gitDir)
  201. if (checkoutDetails.exitCode === 0) {
  202. console.log(`${pass} Successfully tagged ${version}.`)
  203. } else {
  204. console.log(`${fail} Error tagging ${version}: ` +
  205. `${checkoutDetails.stderr}`)
  206. process.exit(1)
  207. }
  208. }
  209. async function verifyNewVersion () {
  210. let newVersion = await getNewVersion(true)
  211. let response
  212. if (args.automaticRelease) {
  213. response = 'y'
  214. } else {
  215. response = await promptForVersion(newVersion)
  216. }
  217. if (response.match(/^y/i)) {
  218. console.log(`${pass} Starting release of ${newVersion}`)
  219. } else {
  220. console.log(`${fail} Aborting release of ${newVersion}`)
  221. process.exit()
  222. }
  223. }
  224. async function promptForVersion (version) {
  225. return new Promise((resolve, reject) => {
  226. const rl = readline.createInterface({
  227. input: process.stdin,
  228. output: process.stdout
  229. })
  230. rl.question(`Do you want to create the release ${version.green} (y/N)? `, (answer) => {
  231. rl.close()
  232. resolve(answer)
  233. })
  234. })
  235. }
  236. // function to determine if there have been commits to master since the last release
  237. async function changesToRelease () {
  238. let lastCommitWasRelease = new RegExp(`^Bump v[0-9.]*(-beta[0-9.]*)?(-nightly[0-9.]*)?$`, 'g')
  239. let lastCommit = await GitProcess.exec(['log', '-n', '1', `--pretty=format:'%s'`], gitDir)
  240. return !lastCommitWasRelease.test(lastCommit.stdout)
  241. }
  242. async function prepareRelease (isBeta, notesOnly) {
  243. if (args.dryRun) {
  244. let newVersion = await getNewVersion(true)
  245. console.log(newVersion)
  246. } else {
  247. const currentBranch = (args.branch) ? args.branch : await getCurrentBranch(gitDir)
  248. if (notesOnly) {
  249. let releaseNotes = await getReleaseNotes(currentBranch)
  250. console.log(`Draft release notes are: \n${releaseNotes}`)
  251. } else {
  252. const changes = await changesToRelease(currentBranch)
  253. if (changes) {
  254. await verifyNewVersion()
  255. await createRelease(currentBranch, isBeta)
  256. await pushRelease(currentBranch)
  257. await runReleaseBuilds(currentBranch)
  258. } else {
  259. console.log(`There are no new changes to this branch since the last release, aborting release.`)
  260. process.exit(1)
  261. }
  262. }
  263. }
  264. }
  265. prepareRelease(!args.stable, args.notesOnly)