Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 1 commit into from
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
feat(react-native-github): a script to automate patch version bumping…
… of packages (#35767)

Summary:
Pull Request resolved: #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: 1a4bbe33a996a93af37a6642ed4a11341066e6ee
  • Loading branch information
hoxyq authored and facebook-github-bot committed Jan 10, 2023
commit 104a05f024e23054ad916141af8eb06db468c47b
1 change: 0 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions scripts/__tests__/find-and-publish-all-bumped-packages-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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}`,
);
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) +
'\n',
'utf-8',
);
});
});
146 changes: 146 additions & 0 deletions scripts/monorepo/align-package-versions.js
Original file line number Diff line number Diff line change
@@ -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,
),
);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a super big fan of this double forEach which feels a bit like brute forcing checking everything against everything but honestly hey at least we'll make sure that everything is correctly aligned!

And I think this will work out of the box with -stable branches too so 👍


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();
52 changes: 52 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,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) =>
kelset marked this conversation as resolved.
Show resolved Hide resolved
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;
Loading