Skip to content

Commit

Permalink
feat(react-native-github): a script to automate patch version bumping…
Browse files Browse the repository at this point in the history
… of packages (facebook#35767)

Summary:
Pull Request resolved: facebook#35767

Changelog: [Internal]

Introducing a script, which can be used to identify all packages inside `/packages`, which contain any changes after the last time its version was changed

How it works step by step:

```
check that no git changes are present

for each package:
    if package is private -> skip

    grep id of the last commit that changed package
    grep id of the last commit that changed version of the package

    if these ids are different:
        bump package patch version
        for each package:
            check if this package is used and update its version

run yarn if required
commit changes if required
```

Can be executed only in git environment and by running: `node ./scripts/bump-all-updated-packages`

Reviewed By: cortinico

Differential Revision: D42295344

fbshipit-source-id: 326b1b28f20f21252154bf9f3ebf42d71de9112b
  • Loading branch information
hoxyq authored and facebook-github-bot committed Jan 6, 2023
1 parent 66927ec commit fca5d04
Show file tree
Hide file tree
Showing 6 changed files with 387 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

const {exec} = require('shelljs');

const {BUMP_COMMIT_MESSAGE} = require('../monorepo/constants');
const forEachPackage = require('../monorepo/for-each-package');
const findAndPublishAllBumpedPackages = require('../monorepo/find-and-publish-all-bumped-packages');

Expand All @@ -24,10 +25,15 @@ describe('findAndPublishAllBumpedPackages', () => {
version: mockedPackageNewVersion,
});
});

exec.mockImplementationOnce(() => ({
stdout: `- "version": "0.72.0"\n+ "version": "${mockedPackageNewVersion}"\n`,
}));

exec.mockImplementationOnce(() => ({
stdout: BUMP_COMMIT_MESSAGE,
}));

expect(() => findAndPublishAllBumpedPackages()).toThrow(
`Package version expected to be 0.x.y, but received ${mockedPackageNewVersion}`,
);
Expand Down
39 changes: 39 additions & 0 deletions scripts/monorepo/__tests__/bump-package-version-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* 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 path = require('path');
const {writeFileSync} = require('fs');

const bumpPackageVersion = require('../bump-all-updated-packages/bump-package-version');

jest.mock('fs', () => ({
writeFileSync: jest.fn(),
readFileSync: jest.fn(() => '{}'),
}));

jest.mock('../for-each-package', () => callback => {});

describe('bumpPackageVersionTest', () => {
it('updates patch version of the package', () => {
const mockedPackageLocation = '~/packages/assets';
const mockedPackageManifest = {
name: '@react-native/test',
version: '1.2.3',
};

bumpPackageVersion(mockedPackageLocation, mockedPackageManifest);

expect(writeFileSync).toHaveBeenCalledWith(
path.join(mockedPackageLocation, 'package.json'),
JSON.stringify({...mockedPackageManifest, version: '1.2.4'}, null, 2) +
'\r\n',
'utf-8',
);
});
});
128 changes: 128 additions & 0 deletions scripts/monorepo/bump-all-updated-packages/bump-package-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* 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 {writeFileSync, readFileSync} = require('fs');
const path = require('path');

const forEachPackage = require('../for-each-package');

const ROOT_LOCATION = path.join(__dirname, '..', '..', '..');
const TEMPLATE_LOCATION = path.join(ROOT_LOCATION, 'template');
const REPO_CONFIG_LOCATION = path.join(ROOT_LOCATION, 'repo-config');

const readJSONFile = packageAbsolutePath =>
JSON.parse(readFileSync(packageAbsolutePath));

const getIncrementedVersion = (version, increment) =>
version
.split('.')
.map((token, index) => {
const indexOfVersionToIncrement = increment === 'minor' ? 1 : 2;

return index === indexOfVersionToIncrement
? parseInt(token, 10) + 1
: token;
})
.join('.');

const updatePackageVersionInPackageManifest = (
packageAbsolutePath,
updatedPackageName,
updatedPackageVersion,
) => {
const packageManifestPath = path.join(packageAbsolutePath, 'package.json');
const packageManifest = readJSONFile(packageManifestPath);

const dependencyVersion = packageManifest.dependencies?.[updatedPackageName];
if (dependencyVersion && dependencyVersion !== '*') {
const updatedDependencyVersion = dependencyVersion.startsWith('^')
? `^${updatedPackageVersion}`
: updatedPackageVersion;
const updatedPackageManifest = {
...packageManifest,
dependencies: {
...packageManifest.dependencies,
[updatedPackageName]: updatedDependencyVersion,
},
};

writeFileSync(
packageManifestPath,
JSON.stringify(updatedPackageManifest, null, 2) + '\r\n',
'utf-8',
);
}

const devDependencyVersion =
packageManifest.devDependencies?.[updatedPackageName];
if (devDependencyVersion && devDependencyVersion !== '*') {
const updatedDependencyVersion = devDependencyVersion.startsWith('^')
? `^${updatedPackageVersion}`
: updatedPackageVersion;
const updatedPackageManifest = {
...packageManifest,
devDependencies: {
...packageManifest.devDependencies,
[updatedPackageName]: updatedDependencyVersion,
},
};

writeFileSync(
packageManifestPath,
JSON.stringify(updatedPackageManifest, null, 2) + '\r\n',
'utf-8',
);
}
};

const bumpPackageVersion = (
packageAbsolutePath,
packageManifest,
increment = 'patch',
) => {
const updatedVersion = getIncrementedVersion(
packageManifest.version,
increment,
);

// Not using simple `npm version patch` because it updates dependencies and yarn.lock file
writeFileSync(
path.join(packageAbsolutePath, 'package.json'),
JSON.stringify({...packageManifest, version: updatedVersion}, null, 2) +
'\r\n',
'utf-8',
);

updatePackageVersionInPackageManifest(
ROOT_LOCATION,
packageManifest.name,
updatedVersion,
);
updatePackageVersionInPackageManifest(
TEMPLATE_LOCATION,
packageManifest.name,
updatedVersion,
);
updatePackageVersionInPackageManifest(
REPO_CONFIG_LOCATION,
packageManifest.name,
updatedVersion,
);
forEachPackage(pathToPackage =>
updatePackageVersionInPackageManifest(
pathToPackage,
packageManifest.name,
updatedVersion,
),
);

return updatedVersion;
};

module.exports = bumpPackageVersion;
187 changes: 187 additions & 0 deletions scripts/monorepo/bump-all-updated-packages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* 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 chalk = require('chalk');
const inquirer = require('inquirer');
const path = require('path');
const {echo, exec, exit} = require('shelljs');

const {BUMP_COMMIT_MESSAGE} = require('../constants');
const forEachPackage = require('../for-each-package');
const bumpPackageVersion = require('./bump-package-version');

const ROOT_LOCATION = path.join(__dirname, '..', '..', '..');

const buildExecutor =
(packageAbsolutePath, packageRelativePathFromRoot, packageManifest) =>
async () => {
const {name: packageName} = packageManifest;
if (packageManifest.private) {
echo(`\u23ED Skipping private package ${chalk.dim(packageName)}`);

return;
}

const hashOfLastCommitInsidePackage = exec(
`git log -n 1 --format=format:%H -- ${packageRelativePathFromRoot}`,
{cwd: ROOT_LOCATION, silent: true},
).stdout.trim();

const hashOfLastCommitThatChangedVersion = exec(
`git log -G\\"version\\": --format=format:%H -n 1 -- ${packageRelativePathFromRoot}/package.json`,
{cwd: ROOT_LOCATION, silent: true},
).stdout.trim();

if (hashOfLastCommitInsidePackage === hashOfLastCommitThatChangedVersion) {
echo(
`\uD83D\uDD0E No changes for package ${chalk.green(
packageName,
)} since last version bump`,
);

return;
}

echo(`\uD83D\uDCA1 Found changes for ${chalk.yellow(packageName)}:`);
exec(
`git log --pretty=oneline ${hashOfLastCommitThatChangedVersion}..${hashOfLastCommitInsidePackage} ${packageRelativePathFromRoot}`,
{
cwd: ROOT_LOCATION,
},
);
echo();

await inquirer
.prompt([
{
type: 'list',
name: 'shouldBumpPackage',
message: `Do you want to bump ${packageName}?`,
choices: ['Yes', 'No'],
filter: val => val === 'Yes',
},
])
.then(({shouldBumpPackage}) => {
if (!shouldBumpPackage) {
echo(`Skipping bump for ${packageName}`);
return;
}

return inquirer
.prompt([
{
type: 'list',
name: 'increment',
message: 'Which version you want to increment?',
choices: ['patch', 'minor'],
},
])
.then(({increment}) => {
const updatedVersion = bumpPackageVersion(
packageAbsolutePath,
packageManifest,
increment,
);
echo(
`\u2705 Successfully bumped ${chalk.green(
packageName,
)} to ${chalk.green(updatedVersion)}`,
);
});
});
};

const buildAllExecutors = () => {
const executors = [];

forEachPackage((...params) => {
executors.push(buildExecutor(...params));
});

return executors;
};

const checkIfThereIsSomethingToCommit = () => {
const {stdout: thereIsSomethingToCommit} = exec('git status --porcelain', {
cwd: ROOT_LOCATION,
silent: true,
});

return Boolean(thereIsSomethingToCommit);
};

const main = async () => {
if (checkIfThereIsSomethingToCommit()) {
echo(
chalk.red(
'Found uncommitted changes. Please commit or stash them before running this script',
),
);
exit(1);
}

const executors = buildAllExecutors();
for (const executor of executors) {
await executor()
.catch(() => exit(1))
.then(() => echo());
}

await inquirer
.prompt([
{
type: 'list',
name: 'shouldRunYarn',
message: 'Do you want to run yarn?',
choices: ['Yes', 'No'],
filter: val => val === 'Yes',
},
])
.then(({shouldRunYarn}) => {
if (!shouldRunYarn) {
echo(`Skipping ${chalk.dim('yarn install')}`);
return;
}

const {code} = exec('yarn', {cwd: ROOT_LOCATION});

if (code) {
echo(chalk.red('Failed to run yarn'));
exit(code);
}
})
.then(() => echo());

if (checkIfThereIsSomethingToCommit()) {
await inquirer
.prompt([
{
type: 'list',
name: 'shouldSubmitCommit',
message: 'Do you want to submit a commit with these changes?',
choices: ['Yes', 'No'],
filter: val => val === 'Yes',
},
])
.then(({shouldSubmitCommit}) => {
if (!shouldSubmitCommit) {
echo('Not submitting a commit, but keeping all changes');
return;
}

exec(`git commit -a -m "${BUMP_COMMIT_MESSAGE}"`, {cwd: ROOT_LOCATION});
})
.then(() => echo());
}

echo(chalk.green('Successfully finished the process of bumping packages'));
exit(0);
};

main();
13 changes: 13 additions & 0 deletions scripts/monorepo/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* 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.
*
* @flow strict-local
* @format
*/

const BUMP_COMMIT_MESSAGE = '[ci][monorepo] bump package versions';

module.exports = {BUMP_COMMIT_MESSAGE};
Loading

0 comments on commit fca5d04

Please sign in to comment.