index.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. #!/usr/bin/env node
  2. const { GitProcess } = require('dugite')
  3. const minimist = require('minimist')
  4. const path = require('path')
  5. const semver = require('semver')
  6. const notesGenerator = require('./notes.js')
  7. const gitDir = path.resolve(__dirname, '..', '..')
  8. const semverify = version => version.replace(/^origin\//, '').replace('x', '0').replace(/-/g, '.')
  9. const runGit = async (args) => {
  10. const response = await GitProcess.exec(args, gitDir)
  11. if (response.exitCode !== 0) {
  12. throw new Error(response.stderr.trim())
  13. }
  14. return response.stdout.trim()
  15. }
  16. const tagIsSupported = tag => tag && !tag.includes('nightly') && !tag.includes('unsupported')
  17. const tagIsBeta = tag => tag.includes('beta')
  18. const tagIsStable = tag => tagIsSupported(tag) && !tagIsBeta(tag)
  19. const getTagsOf = async (point) => {
  20. return (await runGit(['tag', '--merged', point]))
  21. .split('\n')
  22. .map(tag => tag.trim())
  23. .filter(tag => semver.valid(tag))
  24. .sort(semver.compare)
  25. }
  26. const getTagsOnBranch = async (point) => {
  27. const masterTags = await getTagsOf('master')
  28. if (point === 'master') {
  29. return masterTags
  30. }
  31. const masterTagsSet = new Set(masterTags)
  32. return (await getTagsOf(point)).filter(tag => !masterTagsSet.has(tag))
  33. }
  34. const getBranchOf = async (point) => {
  35. const branches = (await runGit(['branch', '-a', '--contains', point]))
  36. .split('\n')
  37. .map(branch => branch.trim())
  38. .filter(branch => !!branch)
  39. const current = branches.find(branch => branch.startsWith('* '))
  40. return current ? current.slice(2) : branches.shift()
  41. }
  42. const getAllBranches = async () => {
  43. return (await runGit(['branch', '--remote']))
  44. .split('\n')
  45. .map(branch => branch.trim())
  46. .filter(branch => !!branch)
  47. .filter(branch => branch !== 'origin/HEAD -> origin/master')
  48. .sort()
  49. }
  50. const getStabilizationBranches = async () => {
  51. return (await getAllBranches())
  52. .filter(branch => /^origin\/\d+-\d+-x$/.test(branch))
  53. }
  54. const getPreviousStabilizationBranch = async (current) => {
  55. const stabilizationBranches = (await getStabilizationBranches())
  56. .filter(branch => branch !== current && branch !== `origin/${current}`)
  57. if (!semver.valid(current)) {
  58. // since we don't seem to be on a stabilization branch right now,
  59. // pick a placeholder name that will yield the newest branch
  60. // as a comparison point.
  61. current = 'v999.999.999'
  62. }
  63. let newestMatch = null
  64. for (const branch of stabilizationBranches) {
  65. if (semver.gte(semverify(branch), semverify(current))) {
  66. continue
  67. }
  68. if (newestMatch && semver.lte(semverify(branch), semverify(newestMatch))) {
  69. continue
  70. }
  71. newestMatch = branch
  72. }
  73. return newestMatch
  74. }
  75. const getPreviousPoint = async (point) => {
  76. const currentBranch = await getBranchOf(point)
  77. const currentTag = (await getTagsOf(point)).filter(tag => tagIsSupported(tag)).pop()
  78. const currentIsStable = tagIsStable(currentTag)
  79. try {
  80. // First see if there's an earlier tag on the same branch
  81. // that can serve as a reference point.
  82. let tags = (await getTagsOnBranch(`${point}^`)).filter(tag => tagIsSupported(tag))
  83. if (currentIsStable) {
  84. tags = tags.filter(tag => tagIsStable(tag))
  85. }
  86. if (tags.length) {
  87. return tags.pop()
  88. }
  89. } catch (error) {
  90. console.log('error', error)
  91. }
  92. // Otherwise, use the newest stable release that preceeds this branch.
  93. // To reach that you may have to walk past >1 branch, e.g. to get past
  94. // 2-1-x which never had a stable release.
  95. let branch = currentBranch
  96. while (branch) {
  97. const prevBranch = await getPreviousStabilizationBranch(branch)
  98. const tags = (await getTagsOnBranch(prevBranch)).filter(tag => tagIsStable(tag))
  99. if (tags.length) {
  100. return tags.pop()
  101. }
  102. branch = prevBranch
  103. }
  104. }
  105. async function getReleaseNotes (range, newVersion, explicitLinks) {
  106. const rangeList = range.split('..') || ['HEAD']
  107. const to = rangeList.pop()
  108. const from = rangeList.pop() || (await getPreviousPoint(to))
  109. if (!newVersion) {
  110. newVersion = to
  111. }
  112. console.log(`Generating release notes between ${from} and ${to} for version ${newVersion}`)
  113. const notes = await notesGenerator.get(from, to, newVersion)
  114. const ret = {
  115. text: notesGenerator.render(notes, explicitLinks)
  116. }
  117. if (notes.unknown.length) {
  118. ret.warning = `You have ${notes.unknown.length} unknown release notes. Please fix them before releasing.`
  119. }
  120. return ret
  121. }
  122. async function main () {
  123. const opts = minimist(process.argv.slice(2), {
  124. boolean: [ 'explicit-links', 'help' ],
  125. string: [ 'version' ]
  126. })
  127. opts.range = opts._.shift()
  128. if (opts.help || !opts.range) {
  129. const name = path.basename(process.argv[1])
  130. console.log(`
  131. easy usage: ${name} version
  132. full usage: ${name} [begin..]end [--version version] [--explicit-links]
  133. * 'begin' and 'end' are two git references -- tags, branches, etc --
  134. from which the release notes are generated.
  135. * if omitted, 'begin' defaults to the previous tag in end's branch.
  136. * if omitted, 'version' defaults to 'end'. Specifying a version is
  137. useful if you're making notes on a new version that isn't tagged yet.
  138. * 'explicit-links' makes every note's issue, commit, or pull an MD link
  139. For example, these invocations are equivalent:
  140. ${process.argv[1]} v4.0.1
  141. ${process.argv[1]} v4.0.0..v4.0.1 --version v4.0.1
  142. `)
  143. return 0
  144. }
  145. const notes = await getReleaseNotes(opts.range, opts.version, opts['explicit-links'])
  146. console.log(notes.text)
  147. if (notes.warning) {
  148. throw new Error(notes.warning)
  149. }
  150. }
  151. if (process.mainModule === module) {
  152. main().catch((err) => {
  153. console.error('Error Occurred:', err)
  154. process.exit(1)
  155. })
  156. }
  157. module.exports = getReleaseNotes