index.ts 6.6 KB

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