Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions .github/workflow-scripts/__tests__/generateChangelog-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/

const {
generateChangelog,
_computePreviousVersionFrom,
_generateChangelog,
_pushCommit,
_createPR,
} = require('../generateChangelog');

const silence = () => {};
const mockGetNpmPackageInfo = jest.fn();
const mockExecSync = jest.fn();
const mockRun = jest.fn();
const mockFetch = jest.fn();
const mockExit = jest.fn();

jest.mock('../utils.js', () => ({
log: silence,
run: mockRun,
getNpmPackageInfo: mockGetNpmPackageInfo,
}));

process.exit = mockExit;
global.fetch = mockFetch;

describe('Generate Changelog', () => {
beforeEach(jest.clearAllMocks);

describe('_computePreviousVersionFrom', () => {
it('returns rc.0 when rc is 1', async () => {
const currentVersion = '0.78.0-rc.1';
const expectedVersion = '0.78.0-rc.0';

const receivedVersion = await _computePreviousVersionFrom(currentVersion);

expect(receivedVersion).toEqual(expectedVersion);
});

it('returns previous rc version when rc is > 1', async () => {
const currentVersion = '0.78.0-rc.5';
const expectedVersion = '0.78.0-rc.4';

const receivedVersion = await _computePreviousVersionFrom(currentVersion);

expect(receivedVersion).toEqual(expectedVersion);
});

it('returns previous patch version when rc is 0', async () => {
const currentVersion = '0.78.0-rc.0';
const expectedVersion = '0.77.1';

mockGetNpmPackageInfo.mockReturnValueOnce(
Promise.resolve({version: '0.77.1'}),
);

const receivedVersion = await _computePreviousVersionFrom(currentVersion);

expect(receivedVersion).toEqual(expectedVersion);
});

it('returns patch 0 when patch is 1', async () => {
const currentVersion = '0.78.1';
const expectedVersion = '0.78.0';

const receivedVersion = await _computePreviousVersionFrom(currentVersion);

expect(receivedVersion).toEqual(expectedVersion);
});

it('returns previous patch when patch is > 1', async () => {
const currentVersion = '0.78.5';
const expectedVersion = '0.78.4';

const receivedVersion = await _computePreviousVersionFrom(currentVersion);

expect(receivedVersion).toEqual(expectedVersion);
});

it('returns null when patch is 0', async () => {
const currentVersion = '0.78.0';

const receivedVersion = await _computePreviousVersionFrom(currentVersion);

expect(receivedVersion).toBeNull();
});

it("throws an error when the version can't be parsed", async () => {
const currentVersion = '0.78.0-rc0';

await expect(
_computePreviousVersionFrom(currentVersion),
).rejects.toThrow();
});
});

describe('_generateChangelog', () => {
it('calls git in the right order', async () => {
const currentVersion = '0.79.0-rc5';
const previousVersion = '0.79.0-rc4';
const token = 'token';

expectedCommandArgs = [
'@rnx-kit/rn-changelog-generator',
'--base',
`v${previousVersion}`,
'--compare',
`v${currentVersion}`,
'--repo',
'.',
'--changelog',
'./CHANGELOG.md',
'--token',
`${token}`,
];

_generateChangelog(previousVersion, currentVersion, token);

expect(mockRun).toHaveBeenCalledTimes(4);
expect(mockRun).toHaveBeenNthCalledWith(1, 'git checkout main');
expect(mockRun).toHaveBeenNthCalledWith(2, 'git fetch');
expect(mockRun).toHaveBeenNthCalledWith(3, 'git pull origin main');
expect(mockRun).toHaveBeenNthCalledWith(
4,
`npx ${expectedCommandArgs.join(' ')}`,
);
});
});

describe('_pushCommit', () => {
it('calls git in the right order', async () => {
const currentVersion = '0.79.0-rc5';

_pushCommit(currentVersion);

expect(mockRun).toHaveBeenCalledTimes(4);
expect(mockRun).toHaveBeenNthCalledWith(
1,
`git checkout -b changelog/v${currentVersion}`,
);
expect(mockRun).toHaveBeenNthCalledWith(2, 'git add CHANGELOG.md');
expect(mockRun).toHaveBeenNthCalledWith(
3,
`git commit -m "[RN][Changelog] Add changelog for v${currentVersion}"`,
);
expect(mockRun).toHaveBeenNthCalledWith(
4,
`git push origin changelog/v${currentVersion}`,
);
});
});

describe('_createPR', () => {
it('throws error when status is not 201', async () => {
const currentVersion = '0.79.0-rc5';
const token = 'token';

mockFetch.mockReturnValueOnce(Promise.resolve({status: 401}));

const headers = {
Accept: 'Accept: application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
Authorization: `Bearer ${token}`,
};

const content = `
## Summary
Add Changelog for ${currentVersion}

## Changelog:
[Internal] - Add Changelog for ${currentVersion}

## Test Plan:
N/A`;

const body = {
title: `[RN][Changelog] Add changelog for v${currentVersion}`,
head: `changelog/v${currentVersion}`,
base: 'main',
body: content,
};

await expect(_createPR(currentVersion, token)).rejects.toThrow();

expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.github.com/repos/facebook/react-native/pulls',
{
method: 'POST',
headers: headers,
body: JSON.stringify(body),
},
);
});
it('Returns the pr url', async () => {
const currentVersion = '0.79.0-rc5';
const token = 'token';
const expectedPrURL =
'https://github.com/facebook/react-native/pulls/1234';

const returnedObject = {
status: 201,
json: () => Promise.resolve({html_url: expectedPrURL}),
};
mockFetch.mockReturnValueOnce(Promise.resolve(returnedObject));

const headers = {
Accept: 'Accept: application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
Authorization: `Bearer ${token}`,
};

const content = `
## Summary
Add Changelog for ${currentVersion}

## Changelog:
[Internal] - Add Changelog for ${currentVersion}

## Test Plan:
N/A`;

const body = {
title: `[RN][Changelog] Add changelog for v${currentVersion}`,
head: `changelog/v${currentVersion}`,
base: 'main',
body: content,
};

const receivedPrURL = await _createPR(currentVersion, token);

expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.github.com/repos/facebook/react-native/pulls',
{
method: 'POST',
headers: headers,
body: JSON.stringify(body),
},
);
expect(receivedPrURL).toEqual(expectedPrURL);
});
});
});
120 changes: 120 additions & 0 deletions .github/workflow-scripts/generateChangelog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/

const {log, getNpmPackageInfo, run} = require('./utils');

async function _computePreviousVersionFrom(version) {
log(`Computing previous version from: ${version}`);
const regex = /^0\.(\d+)\.(\d+)(-rc\.(\d+))?$/;
const match = version.match(regex);
if (!match) {
throw new Error(`Invalid version format: ${version}`);
}

const minor = match[1];
const patch = match[2];
const rc = match[4];

if (rc) {
if (Number(rc) > 0) {
return `0.${minor}.${patch}-rc.${Number(rc) - 1}`;
}
//fetch latest version on NPM
const latestPkg = await getNpmPackageInfo('react-native', 'latest');
return latestPkg.version;
} else {
if (Number(patch) === 0) {
// No need to generate the changelog for 0.X.0 as we already generated it from RCs
log(
`Skipping changelog generation for ${version} as we already have it from the RCs`,
);
return null;
}
return `0.${minor}.${Number(patch) - 1}`;
}
}

function _generateChangelog(previousVersion, version, token) {
log(`Generating changelog for ${version} from ${previousVersion}`);
run('git checkout main');
run('git fetch');
run('git pull origin main');
const generateChangelogComand = `npx @rnx-kit/rn-changelog-generator --base v${previousVersion} --compare v${version} --repo . --changelog ./CHANGELOG.md --token ${token}`;
run(generateChangelogComand);
}

function _pushCommit(version) {
log(`Pushing commit to changelog/v${version}`);
run(`git checkout -b changelog/v${version}`);
run('git add CHANGELOG.md');
run(`git commit -m "[RN][Changelog] Add changelog for v${version}"`);
run(`git push origin changelog/v${version}`);
}

async function _createPR(version, token) {
log('Creating changelog pr');
const url = 'https://api.github.com/repos/facebook/react-native/pulls';
const body = `
## Summary
Add Changelog for ${version}

## Changelog:
[Internal] - Add Changelog for ${version}

## Test Plan:
N/A`;

const response = await fetch(url, {
method: 'POST',
headers: {
Accept: 'Accept: application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
title: `[RN][Changelog] Add changelog for v${version}`,
head: `changelog/v${version}`,
base: 'main',
body: body,
}),
});

if (response.status !== 201) {
throw new Error(
`Failed to create PR: ${response.status} ${response.statusText}`,
);
}

const data = await response.json();
return data.html_url;
}

async function generateChangelog(version, token) {
if (version.startsWith('v')) {
version = version.substring(1);
}

const previousVersion = await _computePreviousVersionFrom(version);
if (previousVersion) {
log(`Previous version is ${previousVersion}`);
_generateChangelog(previousVersion, version, token);
_pushCommit(version);
const prURL = await _createPR(version, token);
log(`Created PR: ${prURL}`);
}
}

module.exports = {
generateChangelog,
// Exported only for testing purposes:
_computePreviousVersionFrom,
_generateChangelog,
_pushCommit,
_createPR,
};
Loading
Loading