index.js 6.3 KB

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