Skip to content

Commit a0550a6

Browse files
committed
[RN][Releases] Generate the changelog automatically
1 parent 68741d7 commit a0550a6

File tree

3 files changed

+378
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)