diff --git a/scripts/__tests__/find-and-publish-all-bumped-packages-test.js b/scripts/__tests__/find-and-publish-all-bumped-packages-test.js index ee44b354b1e4c9..b4c3a9be55ff03 100644 --- a/scripts/__tests__/find-and-publish-all-bumped-packages-test.js +++ b/scripts/__tests__/find-and-publish-all-bumped-packages-test.js @@ -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'); @@ -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}`, ); diff --git a/scripts/monorepo/__tests__/bump-package-version-test.js b/scripts/monorepo/__tests__/bump-package-version-test.js new file mode 100644 index 00000000000000..ef8695e1ff51c1 --- /dev/null +++ b/scripts/monorepo/__tests__/bump-package-version-test.js @@ -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', + ); + }); +}); diff --git a/scripts/monorepo/bump-all-updated-packages/bump-package-version.js b/scripts/monorepo/bump-all-updated-packages/bump-package-version.js new file mode 100644 index 00000000000000..fe3fe367e479af --- /dev/null +++ b/scripts/monorepo/bump-all-updated-packages/bump-package-version.js @@ -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; diff --git a/scripts/monorepo/bump-all-updated-packages/index.js b/scripts/monorepo/bump-all-updated-packages/index.js new file mode 100644 index 00000000000000..d2f49e2877f932 --- /dev/null +++ b/scripts/monorepo/bump-all-updated-packages/index.js @@ -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(); diff --git a/scripts/monorepo/constants.js b/scripts/monorepo/constants.js new file mode 100644 index 00000000000000..fcbc5c51e3c16d --- /dev/null +++ b/scripts/monorepo/constants.js @@ -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}; diff --git a/scripts/monorepo/find-and-publish-all-bumped-packages.js b/scripts/monorepo/find-and-publish-all-bumped-packages.js index b2a75ab0225be5..61e328172918f0 100644 --- a/scripts/monorepo/find-and-publish-all-bumped-packages.js +++ b/scripts/monorepo/find-and-publish-all-bumped-packages.js @@ -11,6 +11,7 @@ const path = require('path'); const chalk = require('chalk'); const {exec} = require('shelljs'); +const {BUMP_COMMIT_MESSAGE} = require('./constants'); const forEachPackage = require('./for-each-package'); const ROOT_LOCATION = path.join(__dirname, '..', '..'); @@ -48,6 +49,19 @@ const findAndPublishAllBumpedPackages = () => { return; } + const commitMessage = exec( + `git log -n 1 --format=format:%B ${packageRelativePathFromRoot}/package.json`, + {cwd: ROOT_LOCATION, silent: true}, + ).stdout; + 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;