Skip to content

Commit c08f469

Browse files
authored
[code-infra] Use util from code-infra to fetch changelogs (#47350)
1 parent cff8adc commit c08f469

File tree

1 file changed

+75
-135
lines changed

1 file changed

+75
-135
lines changed

scripts/releaseChangelog.mjs

Lines changed: 75 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
import childProcess from 'child_process';
2-
import { promisify } from 'util';
1+
import * as fs from 'node:fs/promises';
2+
import * as path from 'node:path';
33
import { Octokit } from '@octokit/rest';
4-
import chalk from 'chalk';
4+
import {
5+
fetchCommitsBetweenRefs,
6+
findLatestTaggedVersion,
7+
} from '@mui/internal-code-infra/changelog';
58
import yargs from 'yargs';
69

7-
const exec = promisify(childProcess.exec);
10+
/**
11+
* @TODO: Add it to @mui/internal-code-infra/changelog
12+
*
13+
* @param {string} login
14+
* @returns {boolean}
15+
*/
16+
function isBot(login) {
17+
return login.endsWith('[bot]') && !login.includes('copilot');
18+
}
819

920
/**
1021
* @param {string} commitMessage
@@ -27,168 +38,109 @@ function parseTags(commitMessage) {
2738
.join(',');
2839
}
2940

41+
// Match commit messages like:
42+
// "[docs] Fix small typo on Grid2 page (#44062)"
43+
const prLinkRegEx = /\(#[0-9]+\)$/;
44+
3045
/**
31-
* @param {Octokit.ReposCompareCommitsResponseCommitsItem} commitsItem
46+
*
47+
* @param {import('@mui/internal-code-infra/changelog').FetchedCommitDetails[]} commits
48+
* @returns {string[]}
3249
*/
33-
function filterCommit(commitsItem) {
34-
const commitMessage = commitsItem.commit.message;
35-
// TODO: Use labels
36-
return (
37-
// Filter renovate dependencies updates
38-
!commitMessage.startsWith('Bump') &&
39-
!commitMessage.startsWith('Lock file maintenance') &&
40-
// Filter website changes, no implications for library users
41-
!commitMessage.startsWith('[website]')
42-
);
43-
}
44-
45-
async function findLatestTaggedVersion() {
46-
const { stdout } = await exec(
47-
[
48-
'git',
49-
'describe',
50-
// Earlier tags used lightweight tags + commit.
51-
// We switched to annotated tags later.
52-
'--tags',
53-
'--abbrev=0',
54-
// only include "version-tags"
55-
'--match "v*"',
56-
].join(' '),
50+
function getAllContributors(commits) {
51+
const authors = Array.from(
52+
new Set(
53+
commits
54+
.filter((commit) => !!commit.author?.login)
55+
.map((commit) => {
56+
return commit.author.login;
57+
}),
58+
),
5759
);
5860

59-
return stdout.trim();
61+
return authors.sort((a, b) => a.localeCompare(b)).map((author) => `@${author}`);
6062
}
6163

62-
// Match commit messages like:
63-
// "[docs] Fix small typo on Grid2 page (#44062)"
64-
const prLinkRegEx = /\(#[0-9]+\)$/;
65-
6664
async function main(argv) {
67-
const { githubToken, lastRelease: lastReleaseInput, release, repo } = argv;
65+
const { lastRelease: previousReleaseParam, release } = argv;
6866

69-
if (!githubToken) {
70-
throw new TypeError(
71-
'Unable to authenticate. Make sure you either call the script with `--githubToken $token` or set `process.env.GITHUB_TOKEN`. The token needs `public_repo` permissions.',
72-
);
73-
}
74-
const octokit = new Octokit({
75-
auth: githubToken,
76-
request: {
77-
fetch,
78-
},
67+
const latestTaggedVersion = await findLatestTaggedVersion({
68+
cwd: process.cwd(),
69+
fetchAll: false,
7970
});
80-
81-
const latestTaggedVersion = await findLatestTaggedVersion();
82-
const lastRelease = lastReleaseInput !== undefined ? lastReleaseInput : latestTaggedVersion;
83-
if (lastRelease !== latestTaggedVersion) {
71+
const previousRelease =
72+
previousReleaseParam !== undefined ? previousReleaseParam : latestTaggedVersion;
73+
if (previousRelease !== latestTaggedVersion) {
8474
console.warn(
85-
`Creating changelog for ${lastRelease}..${release} when latest tagged version is '${latestTaggedVersion}'.`,
75+
`Creating changelog for ${previousRelease}..${release} while the latest tagged version is '${latestTaggedVersion}'.`,
8676
);
8777
}
8878

89-
/**
90-
* @type {AsyncIterableIterator<Octokit.Response<Octokit.ReposCompareCommitsResponse>>}
91-
*/
92-
const timeline = octokit.paginate.iterator(
93-
octokit.repos.compareCommits.endpoint.merge({
94-
owner: 'mui',
95-
repo,
96-
base: lastRelease,
97-
head: release,
98-
}),
99-
);
100-
101-
/**
102-
* @type {Octokit.ReposCompareCommitsResponseCommitsItem[]}
103-
*/
104-
const commitsItems = [];
105-
for await (const response of timeline) {
106-
const { data: compareCommits } = response;
107-
commitsItems.push(...compareCommits.commits.filter(filterCommit));
79+
if (process.env.GITHUB_TOKEN) {
80+
console.warn(
81+
`Using GITHUB_TOKEN from environment variables have been deprecated. Please remove it if set locally.`,
82+
);
10883
}
10984

110-
let warnedOnce = false;
111-
112-
const getAuthor = (commit) => {
113-
if (!commit.author) {
114-
if (!warnedOnce) {
115-
console.warn(
116-
`The author of the commit: ${commit.commit.tree.url} cannot be retrieved. Please add the github username manually.`,
117-
);
118-
}
119-
warnedOnce = true;
120-
return chalk.red("TODO INSERT AUTHOR'S USERNAME");
121-
}
122-
123-
const authorLogin = commit.author.login;
124-
125-
if (authorLogin === 'github-actions[bot]') {
126-
const authorFromMessage = /\(@(?<author>[a-zA-Z0-9-_]+)\) \(#[\d]+\)/.exec(
127-
commit.commit.message.split('\n')[0],
128-
);
129-
if (authorFromMessage.groups?.author) {
130-
return authorFromMessage.groups.author;
131-
}
132-
}
85+
const commitsItems = (
86+
await fetchCommitsBetweenRefs({
87+
lastRelease: previousRelease,
88+
release,
89+
repo: 'material-ui',
90+
octokit: process.env.GITHUB_TOKEN
91+
? new Octokit({ auth: process.env.GITHUB_TOKEN })
92+
: undefined,
93+
})
94+
).filter((commit) => !isBot(commit.author.login) && !commit.message.startsWith('[website]'));
13395

134-
return authorLogin;
135-
};
136-
137-
const authors = Array.from(
138-
new Set(
139-
commitsItems.map((commitsItem) => {
140-
return getAuthor(commitsItem);
141-
}),
142-
),
143-
);
144-
const contributorHandles = authors
145-
.sort((a, b) => a.localeCompare(b))
146-
.map((author) => `@${author}`)
147-
.join(', ');
96+
const contributorHandles = getAllContributors(commitsItems);
14897

14998
// We don't know when a particular commit was made from the API.
15099
// Only that the commits are ordered by date ASC
151-
const commitsItemsByDateDesc = commitsItems.slice().reverse();
100+
const commitsItemsByOrder = new Map(commitsItems.map((item, index) => [item, index]));
152101
// Sort by tags ASC, date desc
153102
// Will only consider exact matches of tags so `[Slider]` will not be grouped with `[Slider][Modal]`
154103
commitsItems.sort((a, b) => {
155-
const aTags = parseTags(a.commit.message);
156-
const bTags = parseTags(b.commit.message);
104+
const aTags = parseTags(a.message);
105+
const bTags = parseTags(b.message);
157106
if (aTags === bTags) {
158-
return commitsItemsByDateDesc.indexOf(a) - commitsItemsByDateDesc.indexOf(b);
107+
return commitsItemsByOrder.get(b) - commitsItemsByOrder.get(a);
159108
}
160109
return aTags.localeCompare(bTags);
161110
});
162111
const changes = commitsItems.map((commitsItem) => {
163-
let shortMessage = commitsItem.commit.message.split('\n')[0];
112+
let shortMessage = commitsItem.message.split('\n')[0];
164113

165114
// If the commit message doesn't have an associated PR, add the commit sha for reference.
166115
if (!prLinkRegEx.test(shortMessage)) {
167116
shortMessage += ` (${commitsItem.sha.substring(0, 7)})`;
168117
}
169118

170-
return `- ${shortMessage} @${getAuthor(commitsItem)}`;
119+
return `- ${shortMessage} @${commitsItem.author.login}`;
171120
});
172-
const nowFormatted = new Date().toLocaleDateString('en-US', {
121+
const generationDate = new Date().toLocaleDateString('en-US', {
173122
month: 'short',
174123
day: 'numeric',
175124
year: 'numeric',
176125
});
126+
const releaseName = /** @type {string} */ (
127+
JSON.parse(await fs.readFile(path.join(process.cwd(), 'package.json'), 'utf-8')).version
128+
);
177129

178130
const changelog = `
179-
## TODO RELEASE NAME
180-
<!-- generated comparing ${lastRelease}..${release} -->
181-
_${nowFormatted}_
131+
## ${releaseName}
132+
133+
<!-- generated comparing ${previousRelease}..${release} -->
134+
135+
_${generationDate}_
182136
183-
A big thanks to the ${
184-
authors.length
185-
} contributors who made this release possible. Here are some highlights ✨:
137+
A big thanks to the ${contributorHandles.length} contributors who made this release possible. Here are some highlights ✨:
186138
187139
TODO INSERT HIGHLIGHTS
188140
189141
${changes.join('\n')}
190142
191-
All contributors of this release in alphabetical order: ${contributorHandles}
143+
All contributors of this release in alphabetical order: ${contributorHandles.join(', ')}
192144
`;
193145

194146
// eslint-disable-next-line no-console -- output of this script
@@ -199,31 +151,19 @@ yargs(process.argv.slice(2))
199151
.command({
200152
command: '$0',
201153
description: 'Creates a changelog',
202-
builder: (command) => {
203-
return command
154+
builder: (command) =>
155+
command
204156
.option('lastRelease', {
205157
describe:
206158
'The release to compare against e.g. `v5.0.0-alpha.23`. Default: The latest tag on the current branch.',
207159
type: 'string',
208160
})
209-
.option('githubToken', {
210-
default: process.env.GITHUB_TOKEN,
211-
describe:
212-
'The personal access token to use for authenticating with GitHub. Needs public_repo permissions.',
213-
type: 'string',
214-
})
215161
.option('release', {
216162
// #target-branch-reference
217163
default: 'master',
218164
describe: 'Ref which we want to release',
219165
type: 'string',
220-
})
221-
.option('repo', {
222-
default: 'material-ui',
223-
describe: 'Repository to generate a changelog for',
224-
type: 'string',
225-
});
226-
},
166+
}),
227167
handler: main,
228168
})
229169
.help()

0 commit comments

Comments
 (0)