feat(react-native-github): a script to automate patch version bumping of packages (#35767)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/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

commit changes if required
```

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

 ---

Also adding a separate script `align-package-versions.js`, which can be used to update versions of packages inside consumer packages

```
check that no git changes are present

for each package x:
   for each package y:
       if y has x as dependency:
           validate that y uses the latest version of x

if some changes were made:
   run yarn
```

 ---

Q: Why `run_yarn` step was removed from CircleCI flow?
A: For *-stable branches, there are no yarn workspaces and all packages are specified as direct dependencies, so if we update `react-native/assets-registry` to the next version, we won't be able to run `yarn` for react-native root package, because updated version is not yet published to npm

To avoid this, we first need publish new versions and then update them in consumer packages

 ---
The final flow:
1. Developer uses `node ./scripts/bump-all-updated-packages` to bump versions of all updated packages.
2. Commit created from step 1 being merged or directly pushed to `main` or `*-stable` branches
3. A workflow from CircleCI publishes all updated versions to npm
4. Developer can use `align-package-versions.js` script to create required changes to align all packages versions

Reviewed By: cortinico

Differential Revision: D42295344

fbshipit-source-id: 54b667adb3ee5f28d19ee9c7991570451549aac2
This commit is contained in:
Ruslan Lesiutin 2023-01-10 13:23:06 -08:00 коммит произвёл Facebook GitHub Bot
Родитель a80cf96fc8
Коммит ec28c5bbaa
10 изменённых файлов: 517 добавлений и 33 удалений

Просмотреть файл

@ -1576,7 +1576,6 @@ jobs:
executor: reactnativeandroid
steps:
- checkout
- run_yarn
- run:
name: Set NPM auth token
command: echo "//registry.npmjs.org/:_authToken=${CIRCLE_NPM_TOKEN}" > ~/.npmrc

Просмотреть файл

@ -98,7 +98,9 @@
"test-e2e-local-clean": "node ./scripts/test-e2e-local-clean.js",
"test-ios": "./scripts/objc-test.sh test",
"test-typescript": "dtslint types",
"test-typescript-offline": "dtslint --localTs node_modules/typescript/lib types"
"test-typescript-offline": "dtslint --localTs node_modules/typescript/lib types",
"bump-all-updated-packages": "node ./scripts/monorepo/bump-all-updated-packages",
"align-package-versions": "node ./scripts/monorepo/align-package-versions.js"
},
"workspaces": [
"packages/*",

Просмотреть файл

@ -7,12 +7,13 @@
* @format
*/
const {exec} = require('shelljs');
const {spawnSync} = require('child_process');
const {BUMP_COMMIT_MESSAGE} = require('../monorepo/constants');
const forEachPackage = require('../monorepo/for-each-package');
const findAndPublishAllBumpedPackages = require('../monorepo/find-and-publish-all-bumped-packages');
jest.mock('shelljs', () => ({exec: jest.fn()}));
jest.mock('child_process', () => ({spawnSync: jest.fn()}));
jest.mock('../monorepo/for-each-package', () => jest.fn());
describe('findAndPublishAllBumpedPackages', () => {
@ -24,10 +25,15 @@ describe('findAndPublishAllBumpedPackages', () => {
version: mockedPackageNewVersion,
});
});
exec.mockImplementationOnce(() => ({
spawnSync.mockImplementationOnce(() => ({
stdout: `- "version": "0.72.0"\n+ "version": "${mockedPackageNewVersion}"\n`,
}));
spawnSync.mockImplementationOnce(() => ({
stdout: BUMP_COMMIT_MESSAGE,
}));
expect(() => findAndPublishAllBumpedPackages()).toThrow(
`Package version expected to be 0.x.y, but received ${mockedPackageNewVersion}`,
);

Просмотреть файл

@ -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) +
'\n',
'utf-8',
);
});
});

Просмотреть файл

@ -0,0 +1,146 @@
/**
* 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 {spawnSync} = require('child_process');
const {writeFileSync, readFileSync} = require('fs');
const path = require('path');
const checkForGitChanges = require('./check-for-git-changes');
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 = pathToFile => JSON.parse(readFileSync(pathToFile));
const checkIfShouldUpdateDependencyPackageVersion = (
consumerPackageAbsolutePath,
updatedPackageName,
updatedPackageVersion,
) => {
const consumerPackageManifestPath = path.join(
consumerPackageAbsolutePath,
'package.json',
);
const consumerPackageManifest = readJSONFile(consumerPackageManifestPath);
const dependencyVersion =
consumerPackageManifest.dependencies?.[updatedPackageName];
if (dependencyVersion && dependencyVersion !== '*') {
const updatedDependencyVersion = dependencyVersion.startsWith('^')
? `^${updatedPackageVersion}`
: updatedPackageVersion;
if (updatedDependencyVersion !== dependencyVersion) {
console.log(
`\uD83D\uDCA1 ${consumerPackageManifest.name} was updated: now using version ${updatedPackageVersion} of ${updatedPackageName}`,
);
const updatedPackageManifest = {
...consumerPackageManifest,
dependencies: {
...consumerPackageManifest.dependencies,
[updatedPackageName]: updatedDependencyVersion,
},
};
writeFileSync(
consumerPackageManifestPath,
JSON.stringify(updatedPackageManifest, null, 2) + '\n',
'utf-8',
);
}
}
const devDependencyVersion =
consumerPackageManifest.devDependencies?.[updatedPackageName];
if (devDependencyVersion && devDependencyVersion !== '*') {
const updatedDependencyVersion = devDependencyVersion.startsWith('^')
? `^${updatedPackageVersion}`
: updatedPackageVersion;
if (updatedDependencyVersion !== devDependencyVersion) {
console.log(
`\uD83D\uDCA1 ${consumerPackageManifest.name} was updated: now using version ${updatedPackageVersion} of ${updatedPackageName}`,
);
const updatedPackageManifest = {
...consumerPackageManifest,
devDependencies: {
...consumerPackageManifest.devDependencies,
[updatedPackageName]: updatedDependencyVersion,
},
};
writeFileSync(
consumerPackageManifestPath,
JSON.stringify(updatedPackageManifest, null, 2) + '\n',
'utf-8',
);
}
}
};
const alignPackageVersions = () => {
if (checkForGitChanges()) {
console.log(
'\u274c Found uncommitted changes. Please commit or stash them before running this script',
);
process.exit(1);
}
forEachPackage((packageAbsolutePath, _, packageManifest) => {
checkIfShouldUpdateDependencyPackageVersion(
ROOT_LOCATION,
packageManifest.name,
packageManifest.version,
);
checkIfShouldUpdateDependencyPackageVersion(
TEMPLATE_LOCATION,
packageManifest.name,
packageManifest.version,
);
checkIfShouldUpdateDependencyPackageVersion(
REPO_CONFIG_LOCATION,
packageManifest.name,
packageManifest.version,
);
forEachPackage(pathToPackage =>
checkIfShouldUpdateDependencyPackageVersion(
pathToPackage,
packageManifest.name,
packageManifest.version,
),
);
});
if (!checkForGitChanges()) {
console.log(
'\u2705 There were no changes. Every consumer package uses the actual version of dependency package.',
);
return;
}
console.log('Running yarn to update lock file...');
spawnSync('yarn', ['install'], {
cwd: ROOT_LOCATION,
shell: true,
stdio: 'inherit',
encoding: 'utf-8',
});
};
alignPackageVersions();

Просмотреть файл

@ -0,0 +1,52 @@
/**
* 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} = require('fs');
const path = require('path');
const getIncrementedVersion = (version, increment) =>
version
.split('.')
.map((token, index) => {
const indexOfVersionToIncrement = increment === 'minor' ? 1 : 2;
if (index === indexOfVersionToIncrement) {
return parseInt(token, 10) + 1;
}
if (index > indexOfVersionToIncrement) {
return 0;
}
return token;
})
.join('.');
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) +
'\n',
'utf-8',
);
return updatedVersion;
};
module.exports = bumpPackageVersion;

Просмотреть файл

@ -0,0 +1,154 @@
/**
* 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 checkForGitChanges = require('../check-for-git-changes');
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 main = async () => {
if (checkForGitChanges()) {
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());
}
if (checkForGitChanges()) {
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();

Просмотреть файл

@ -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 {spawnSync} = require('child_process');
const path = require('path');
const ROOT_LOCATION = path.join(__dirname, '..', '..');
const checkForGitChanges = () => {
const {stdout: thereIsSomethingToCommit, stderr} = spawnSync(
'git',
['status', '--porcelain'],
{
cwd: ROOT_LOCATION,
shell: true,
stdio: 'pipe',
encoding: 'utf-8',
},
);
if (stderr) {
console.log(
'\u274c An error occured while running `git status --porcelain`:',
);
console.log(stderr);
process.exit(1);
}
return Boolean(thereIsSomethingToCommit);
};
module.exports = checkForGitChanges;

Просмотреть файл

@ -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};

Просмотреть файл

@ -8,9 +8,9 @@
*/
const path = require('path');
const chalk = require('chalk');
const {exec} = require('shelljs');
const {spawnSync} = require('child_process');
const {BUMP_COMMIT_MESSAGE} = require('./constants');
const forEachPackage = require('./for-each-package');
const ROOT_LOCATION = path.join(__dirname, '..', '..');
@ -22,41 +22,77 @@ const findAndPublishAllBumpedPackages = () => {
forEachPackage(
(packageAbsolutePath, packageRelativePathFromRoot, packageManifest) => {
if (packageManifest.private) {
console.log(
`\u23ED Skipping private package ${chalk.dim(packageManifest.name)}`,
);
console.log(`\u23ED Skipping private package ${packageManifest.name}`);
return;
}
const diff = exec(
`git log -p --format="" HEAD~1..HEAD ${packageRelativePathFromRoot}/package.json`,
{cwd: ROOT_LOCATION, silent: true},
).stdout;
const {stdout: diff, stderr: commitDiffStderr} = spawnSync(
'git',
[
'log',
'-p',
'--format=""',
'HEAD~1..HEAD',
`${packageRelativePathFromRoot}/package.json`,
],
{cwd: ROOT_LOCATION, shell: true, stdio: 'pipe', encoding: 'utf-8'},
);
if (commitDiffStderr) {
console.log(
`\u274c Failed to get latest committed changes for ${packageManifest.name}:`,
);
console.log(commitDiffStderr);
process.exit(1);
}
const previousVersionPatternMatches = diff.match(
/- {2}"version": "([0-9]+.[0-9]+.[0-9]+)"/,
);
if (!previousVersionPatternMatches) {
console.log(
`\uD83D\uDD0E No version bump for ${chalk.green(
packageManifest.name,
)}`,
);
console.log(`\uD83D\uDD0E No version bump for ${packageManifest.name}`);
return;
}
const {stdout: commitMessage, stderr: commitMessageStderr} = spawnSync(
'git',
[
'log',
'-n',
'1',
'--format=format:%B',
`${packageRelativePathFromRoot}/package.json`,
],
{cwd: ROOT_LOCATION, shell: true, stdio: 'pipe', encoding: 'utf-8'},
);
if (commitMessageStderr) {
console.log(
`\u274c Failed to get latest commit message for ${packageManifest.name}:`,
);
console.log(commitMessageStderr);
process.exit(1);
}
const hasSpecificCommitMessage =
commitMessage.startsWith(BUMP_COMMIT_MESSAGE);
if (!hasSpecificCommitMessage) {
throw new Error(
`Package ${packageManifest.name} was updated, but not through CI script`,
);
}
const [, previousVersion] = previousVersionPatternMatches;
const nextVersion = packageManifest.version;
console.log(
`\uD83D\uDCA1 ${chalk.yellow(
packageManifest.name,
)} was updated: ${chalk.red(previousVersion)} -> ${chalk.green(
nextVersion,
)}`,
`\uD83D\uDCA1 ${packageManifest.name} was updated: ${previousVersion} -> ${nextVersion}`,
);
if (!nextVersion.startsWith('0.')) {
@ -67,24 +103,22 @@ const findAndPublishAllBumpedPackages = () => {
const npmOTPFlag = NPM_CONFIG_OTP ? `--otp ${NPM_CONFIG_OTP}` : '';
const {code, stderr} = exec(`npm publish ${npmOTPFlag}`, {
const {stderr} = spawnSync('npm', ['publish', `${npmOTPFlag}`], {
cwd: packageAbsolutePath,
silent: true,
shell: true,
stdio: 'pipe',
encoding: 'utf-8',
});
if (code) {
if (stderr) {
console.log(
chalk.red(
`\u274c Failed to publish version ${nextVersion} of ${packageManifest.name}. Stderr:`,
),
`\u274c Failed to publish version ${nextVersion} of ${packageManifest.name}:`,
);
console.log(stderr);
process.exit(1);
} else {
console.log(
`\u2705 Successfully published new version of ${chalk.green(
packageManifest.name,
)}`,
`\u2705 Successfully published new version of ${packageManifest.name}`,
);
}
},