Skip to content

Commit 2c343d1

Browse files
cipolleschifacebook-github-bot
authored andcommitted
Generate the changelog automatically (#50323)
Summary: This change implements the automatic generation of the Changelog in CI while doing a release. The output is a PR opened by the react-native-bot that can be manipulated and imported by Meta engineers ## Changelog: [Internal] - Generate the changelog automatically Pull Request resolved: #50323 Test Plan: Tested as a separated workflow first: <img width="1624" alt="Screenshot 2025-03-27 at 17 44 47" src="https://github.com/user-attachments/assets/b8877cdb-f63b-4d82-b340-54f612ac0cd4" /> this generated the PR: #50328 I also added jest tests: <img width="516" alt="Screenshot 2025-03-27 at 17 45 39" src="https://github.com/user-attachments/assets/7ebbc310-e41e-48fc-997e-21366c7306cf" /> Reviewed By: cortinico Differential Revision: D71986909 Pulled By: cipolleschi fbshipit-source-id: 10ffaf342bb0642a6992a107185b6704815b16e3
1 parent 6dd5a83 commit 2c343d1

File tree

4 files changed

+404
-0
lines changed

4 files changed

+404
-0
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const {
11+
generateChangelog,
12+
_computePreviousVersionFrom,
13+
_generateChangelog,
14+
_pushCommit,
15+
_createPR,
16+
} = require('../generateChangelog');
17+
18+
const silence = () => {};
19+
const mockGetNpmPackageInfo = jest.fn();
20+
const mockExecSync = jest.fn();
21+
const mockRun = jest.fn();
22+
const mockFetch = jest.fn();
23+
const mockExit = jest.fn();
24+
25+
jest.mock('../utils.js', () => ({
26+
log: silence,
27+
run: mockRun,
28+
getNpmPackageInfo: mockGetNpmPackageInfo,
29+
}));
30+
31+
process.exit = mockExit;
32+
global.fetch = mockFetch;
33+
34+
describe('Generate Changelog', () => {
35+
beforeEach(jest.clearAllMocks);
36+
37+
describe('_computePreviousVersionFrom', () => {
38+
it('returns rc.0 when rc is 1', async () => {
39+
const currentVersion = '0.78.0-rc.1';
40+
const expectedVersion = '0.78.0-rc.0';
41+
42+
const receivedVersion = await _computePreviousVersionFrom(currentVersion);
43+
44+
expect(receivedVersion).toEqual(expectedVersion);
45+
});
46+
47+
it('returns previous rc version when rc is > 1', async () => {
48+
const currentVersion = '0.78.0-rc.5';
49+
const expectedVersion = '0.78.0-rc.4';
50+
51+
const receivedVersion = await _computePreviousVersionFrom(currentVersion);
52+
53+
expect(receivedVersion).toEqual(expectedVersion);
54+
});
55+
56+
it('returns previous patch version when rc is 0', async () => {
57+
const currentVersion = '0.78.0-rc.0';
58+
const expectedVersion = '0.77.1';
59+
60+
mockGetNpmPackageInfo.mockReturnValueOnce(
61+
Promise.resolve({version: '0.77.1'}),
62+
);
63+
64+
const receivedVersion = await _computePreviousVersionFrom(currentVersion);
65+
66+
expect(receivedVersion).toEqual(expectedVersion);
67+
});
68+
69+
it('returns patch 0 when patch is 1', async () => {
70+
const currentVersion = '0.78.1';
71+
const expectedVersion = '0.78.0';
72+
73+
const receivedVersion = await _computePreviousVersionFrom(currentVersion);
74+
75+
expect(receivedVersion).toEqual(expectedVersion);
76+
});
77+
78+
it('returns previous patch when patch is > 1', async () => {
79+
const currentVersion = '0.78.5';
80+
const expectedVersion = '0.78.4';
81+
82+
const receivedVersion = await _computePreviousVersionFrom(currentVersion);
83+
84+
expect(receivedVersion).toEqual(expectedVersion);
85+
});
86+
87+
it('returns null when patch is 0', async () => {
88+
const currentVersion = '0.78.0';
89+
90+
const receivedVersion = await _computePreviousVersionFrom(currentVersion);
91+
92+
expect(receivedVersion).toBeNull();
93+
});
94+
95+
it("throws an error when the version can't be parsed", async () => {
96+
const currentVersion = '0.78.0-rc0';
97+
98+
await expect(
99+
_computePreviousVersionFrom(currentVersion),
100+
).rejects.toThrow();
101+
});
102+
});
103+
104+
describe('_generateChangelog', () => {
105+
it('calls git in the right order', async () => {
106+
const currentVersion = '0.79.0-rc5';
107+
const previousVersion = '0.79.0-rc4';
108+
const token = 'token';
109+
110+
expectedCommandArgs = [
111+
'@rnx-kit/rn-changelog-generator',
112+
'--base',
113+
`v${previousVersion}`,
114+
'--compare',
115+
`v${currentVersion}`,
116+
'--repo',
117+
'.',
118+
'--changelog',
119+
'./CHANGELOG.md',
120+
'--token',
121+
`${token}`,
122+
];
123+
124+
_generateChangelog(previousVersion, currentVersion, token);
125+
126+
expect(mockRun).toHaveBeenCalledTimes(4);
127+
expect(mockRun).toHaveBeenNthCalledWith(1, 'git checkout main');
128+
expect(mockRun).toHaveBeenNthCalledWith(2, 'git fetch');
129+
expect(mockRun).toHaveBeenNthCalledWith(3, 'git pull origin main');
130+
expect(mockRun).toHaveBeenNthCalledWith(
131+
4,
132+
`npx ${expectedCommandArgs.join(' ')}`,
133+
);
134+
});
135+
});
136+
137+
describe('_pushCommit', () => {
138+
it('calls git in the right order', async () => {
139+
const currentVersion = '0.79.0-rc5';
140+
141+
_pushCommit(currentVersion);
142+
143+
expect(mockRun).toHaveBeenCalledTimes(4);
144+
expect(mockRun).toHaveBeenNthCalledWith(
145+
1,
146+
`git checkout -b changelog/v${currentVersion}`,
147+
);
148+
expect(mockRun).toHaveBeenNthCalledWith(2, 'git add CHANGELOG.md');
149+
expect(mockRun).toHaveBeenNthCalledWith(
150+
3,
151+
`git commit -m "[RN][Changelog] Add changelog for v${currentVersion}"`,
152+
);
153+
expect(mockRun).toHaveBeenNthCalledWith(
154+
4,
155+
`git push origin changelog/v${currentVersion}`,
156+
);
157+
});
158+
});
159+
160+
describe('_createPR', () => {
161+
it('throws error when status is not 201', async () => {
162+
const currentVersion = '0.79.0-rc5';
163+
const token = 'token';
164+
165+
mockFetch.mockReturnValueOnce(Promise.resolve({status: 401}));
166+
167+
const headers = {
168+
Accept: 'Accept: application/vnd.github+json',
169+
'X-GitHub-Api-Version': '2022-11-28',
170+
Authorization: `Bearer ${token}`,
171+
};
172+
173+
const content = `
174+
## Summary
175+
Add Changelog for ${currentVersion}
176+
177+
## Changelog:
178+
[Internal] - Add Changelog for ${currentVersion}
179+
180+
## Test Plan:
181+
N/A`;
182+
183+
const body = {
184+
title: `[RN][Changelog] Add changelog for v${currentVersion}`,
185+
head: `changelog/v${currentVersion}`,
186+
base: 'main',
187+
body: content,
188+
};
189+
190+
await expect(_createPR(currentVersion, token)).rejects.toThrow();
191+
192+
expect(mockFetch).toHaveBeenCalledTimes(1);
193+
expect(mockFetch).toHaveBeenCalledWith(
194+
'https://api.github.com/repos/facebook/react-native/pulls',
195+
{
196+
method: 'POST',
197+
headers: headers,
198+
body: JSON.stringify(body),
199+
},
200+
);
201+
});
202+
it('Returns the pr url', async () => {
203+
const currentVersion = '0.79.0-rc5';
204+
const token = 'token';
205+
const expectedPrURL =
206+
'https://github.com/facebook/react-native/pulls/1234';
207+
208+
const returnedObject = {
209+
status: 201,
210+
json: () => Promise.resolve({html_url: expectedPrURL}),
211+
};
212+
mockFetch.mockReturnValueOnce(Promise.resolve(returnedObject));
213+
214+
const headers = {
215+
Accept: 'Accept: application/vnd.github+json',
216+
'X-GitHub-Api-Version': '2022-11-28',
217+
Authorization: `Bearer ${token}`,
218+
};
219+
220+
const content = `
221+
## Summary
222+
Add Changelog for ${currentVersion}
223+
224+
## Changelog:
225+
[Internal] - Add Changelog for ${currentVersion}
226+
227+
## Test Plan:
228+
N/A`;
229+
230+
const body = {
231+
title: `[RN][Changelog] Add changelog for v${currentVersion}`,
232+
head: `changelog/v${currentVersion}`,
233+
base: 'main',
234+
body: content,
235+
};
236+
237+
const receivedPrURL = await _createPR(currentVersion, token);
238+
239+
expect(mockFetch).toHaveBeenCalledTimes(1);
240+
expect(mockFetch).toHaveBeenCalledWith(
241+
'https://api.github.com/repos/facebook/react-native/pulls',
242+
{
243+
method: 'POST',
244+
headers: headers,
245+
body: JSON.stringify(body),
246+
},
247+
);
248+
expect(receivedPrURL).toEqual(expectedPrURL);
249+
});
250+
});
251+
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const {log, getNpmPackageInfo, run} = require('./utils');
11+
12+
async function _computePreviousVersionFrom(version) {
13+
log(`Computing previous version from: ${version}`);
14+
const regex = /^0\.(\d+)\.(\d+)(-rc\.(\d+))?$/;
15+
const match = version.match(regex);
16+
if (!match) {
17+
throw new Error(`Invalid version format: ${version}`);
18+
}
19+
20+
const minor = match[1];
21+
const patch = match[2];
22+
const rc = match[4];
23+
24+
if (rc) {
25+
if (Number(rc) > 0) {
26+
return `0.${minor}.${patch}-rc.${Number(rc) - 1}`;
27+
}
28+
//fetch latest version on NPM
29+
const latestPkg = await getNpmPackageInfo('react-native', 'latest');
30+
return latestPkg.version;
31+
} else {
32+
if (Number(patch) === 0) {
33+
// No need to generate the changelog for 0.X.0 as we already generated it from RCs
34+
log(
35+
`Skipping changelog generation for ${version} as we already have it from the RCs`,
36+
);
37+
return null;
38+
}
39+
return `0.${minor}.${Number(patch) - 1}`;
40+
}
41+
}
42+
43+
function _generateChangelog(previousVersion, version, token) {
44+
log(`Generating changelog for ${version} from ${previousVersion}`);
45+
run('git checkout main');
46+
run('git fetch');
47+
run('git pull origin main');
48+
const generateChangelogComand = `npx @rnx-kit/rn-changelog-generator --base v${previousVersion} --compare v${version} --repo . --changelog ./CHANGELOG.md --token ${token}`;
49+
run(generateChangelogComand);
50+
}
51+
52+
function _pushCommit(version) {
53+
log(`Pushing commit to changelog/v${version}`);
54+
run(`git checkout -b changelog/v${version}`);
55+
run('git add CHANGELOG.md');
56+
run(`git commit -m "[RN][Changelog] Add changelog for v${version}"`);
57+
run(`git push origin changelog/v${version}`);
58+
}
59+
60+
async function _createPR(version, token) {
61+
log('Creating changelog pr');
62+
const url = 'https://api.github.com/repos/facebook/react-native/pulls';
63+
const body = `
64+
## Summary
65+
Add Changelog for ${version}
66+
67+
## Changelog:
68+
[Internal] - Add Changelog for ${version}
69+
70+
## Test Plan:
71+
N/A`;
72+
73+
const response = await fetch(url, {
74+
method: 'POST',
75+
headers: {
76+
Accept: 'Accept: application/vnd.github+json',
77+
'X-GitHub-Api-Version': '2022-11-28',
78+
Authorization: `Bearer ${token}`,
79+
},
80+
body: JSON.stringify({
81+
title: `[RN][Changelog] Add changelog for v${version}`,
82+
head: `changelog/v${version}`,
83+
base: 'main',
84+
body: body,
85+
}),
86+
});
87+
88+
if (response.status !== 201) {
89+
throw new Error(
90+
`Failed to create PR: ${response.status} ${response.statusText}`,
91+
);
92+
}
93+
94+
const data = await response.json();
95+
return data.html_url;
96+
}
97+
98+
async function generateChangelog(version, token) {
99+
if (version.startsWith('v')) {
100+
version = version.substring(1);
101+
}
102+
103+
const previousVersion = await _computePreviousVersionFrom(version);
104+
if (previousVersion) {
105+
log(`Previous version is ${previousVersion}`);
106+
_generateChangelog(previousVersion, version, token);
107+
_pushCommit(version);
108+
const prURL = await _createPR(version, token);
109+
log(`Created PR: ${prURL}`);
110+
}
111+
}
112+
113+
module.exports = {
114+
generateChangelog,
115+
// Exported only for testing purposes:
116+
_computePreviousVersionFrom,
117+
_generateChangelog,
118+
_pushCommit,
119+
_createPR,
120+
};

0 commit comments

Comments
 (0)