Skip to content

Commit e891c12

Browse files
hoxyqRuslan Lesiutin
authored andcommitted
feat(react-native-github): automate publishing bumped packages via circleci (facebook#35621)
Summary: Pull Request resolved: facebook#35621 Changelog: [Internal] 1. Added `for-each-package.js` script. This can be used to iterate through all of the packages inside `/packages` with the access to package manifest. This soon can be used as a replacement for `yarn workspaces --info` 2. Added `find-and-publish-all-bumped-packages.js` script. This script iterates through all the packages and detects if the version was changed via `git log -p` (same as `git diff`). If so, it tries to publish it to npm. 3. Added corresponding job and workflow to CircleCI config, which will use this script Reviewed By: cortinico Differential Revision: D41972733 fbshipit-source-id: d9de31365d5a68381d3326674ca12608472a6f96
1 parent 0fdbef7 commit e891c12

File tree

5 files changed

+270
-4
lines changed

5 files changed

+270
-4
lines changed

.circleci/config.yml

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ references:
3434
attach_workspace:
3535
at: *hermes_workspace_root
3636

37+
main_only: &main_only
38+
filters:
39+
branches:
40+
only: main
41+
main_or_stable_only: &main_or_stable_only
42+
filters:
43+
branches:
44+
only:
45+
- main
46+
- /0\.[0-9]+[\.[0-9]+]?-stable/
47+
48+
3749
# -------------------------
3850
# Dependency Anchors
3951
# -------------------------
@@ -1567,6 +1579,17 @@ jobs:
15671579
command: |
15681580
echo "Nightly build run"
15691581
1582+
find_and_publish_bumped_packages:
1583+
executor: reactnativeandroid
1584+
steps:
1585+
- checkout
1586+
- run:
1587+
name: Set NPM auth token
1588+
command: echo "//registry.npmjs.org/:_authToken=${CIRCLE_NPM_TOKEN}" > ~/.npmrc
1589+
- run:
1590+
name: Find and publish all bumped packages
1591+
command: node ./scripts/monorepo/find-and-publish-all-bumped-packages.js
1592+
15701593

15711594
# -------------------------
15721595
# PIPELINE PARAMETERS
@@ -1755,11 +1778,8 @@ workflows:
17551778
unless: << pipeline.parameters.run_package_release_workflow_only >>
17561779
triggers:
17571780
- schedule:
1781+
<<: *main_only
17581782
cron: "0 20 * * *"
1759-
filters:
1760-
branches:
1761-
only:
1762-
- main
17631783
jobs:
17641784
- nightly_job
17651785

@@ -1782,3 +1802,8 @@ workflows:
17821802
- build_hermesc_linux
17831803
- build_hermes_macos
17841804
- build_hermesc_windows
1805+
1806+
publish_bumped_packages:
1807+
jobs:
1808+
- find_and_publish_bumped_packages:
1809+
<<: *main_or_stable_only
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const {exec} = require('shelljs');
11+
12+
const forEachPackage = require('../monorepo/for-each-package');
13+
const findAndPublishAllBumpedPackages = require('../monorepo/find-and-publish-all-bumped-packages');
14+
15+
jest.mock('shelljs', () => ({exec: jest.fn()}));
16+
jest.mock('../monorepo/for-each-package', () => jest.fn());
17+
18+
describe('findAndPublishAllBumpedPackages', () => {
19+
it('throws an error if updated version is not 0.x.y', () => {
20+
const mockedPackageNewVersion = '1.0.0';
21+
22+
forEachPackage.mockImplementationOnce(callback => {
23+
callback('absolute/path/to/package', 'to/package', {
24+
version: mockedPackageNewVersion,
25+
});
26+
});
27+
exec.mockImplementationOnce(() => ({
28+
stdout: `- "version": "0.72.0"\n+ "version": "${mockedPackageNewVersion}"\n`,
29+
}));
30+
31+
expect(() => findAndPublishAllBumpedPackages()).toThrow(
32+
`Package version expected to be 0.x.y, but received ${mockedPackageNewVersion}`,
33+
);
34+
});
35+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const path = require('path');
11+
const {readdirSync, readFileSync} = require('fs');
12+
13+
const forEachPackage = require('../monorepo/for-each-package');
14+
15+
jest.mock('fs', () => ({
16+
readdirSync: jest.fn(),
17+
readFileSync: jest.fn(),
18+
}));
19+
20+
describe('forEachPackage', () => {
21+
it('executes callback call with parameters', () => {
22+
const callback = jest.fn();
23+
const mockedPackageManifest = '{"name": "my-new-package"}';
24+
const mockedParsedPackageManifest = JSON.parse(mockedPackageManifest);
25+
const mockedPackageName = 'my-new-package';
26+
27+
readdirSync.mockImplementationOnce(() => [
28+
{name: mockedPackageName, isDirectory: () => true},
29+
]);
30+
readFileSync.mockImplementationOnce(() => mockedPackageManifest);
31+
32+
forEachPackage(callback);
33+
34+
expect(callback).toHaveBeenCalledWith(
35+
path.join(__dirname, '..', '..', 'packages', mockedPackageName),
36+
path.join('packages', mockedPackageName),
37+
mockedParsedPackageManifest,
38+
);
39+
});
40+
41+
it('filters react-native folder', () => {
42+
const callback = jest.fn();
43+
readdirSync.mockImplementationOnce(() => [
44+
{name: 'react-native', isDirectory: () => true},
45+
]);
46+
47+
forEachPackage(callback);
48+
49+
expect(callback).not.toHaveBeenCalled();
50+
});
51+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const path = require('path');
11+
const chalk = require('chalk');
12+
const {exec} = require('shelljs');
13+
14+
const forEachPackage = require('./for-each-package');
15+
16+
const ROOT_LOCATION = path.join(__dirname, '..', '..');
17+
const NPM_CONFIG_OTP = process.env.NPM_CONFIG_OTP;
18+
19+
const findAndPublishAllBumpedPackages = () => {
20+
console.log('Traversing all packages inside /packages...');
21+
22+
forEachPackage(
23+
(packageAbsolutePath, packageRelativePathFromRoot, packageManifest) => {
24+
if (packageManifest.private) {
25+
console.log(
26+
`\u23ED Skipping private package ${chalk.dim(packageManifest.name)}`,
27+
);
28+
29+
return;
30+
}
31+
32+
const diff = exec(
33+
`git log -p --format="" HEAD~1..HEAD ${packageRelativePathFromRoot}/package.json`,
34+
{cwd: ROOT_LOCATION, silent: true},
35+
).stdout;
36+
37+
const previousVersionPatternMatches = diff.match(
38+
/- {2}"version": "([0-9]+.[0-9]+.[0-9]+)"/,
39+
);
40+
41+
if (!previousVersionPatternMatches) {
42+
console.log(
43+
`\uD83D\uDD0E No version bump for ${chalk.green(
44+
packageManifest.name,
45+
)}`,
46+
);
47+
48+
return;
49+
}
50+
51+
const [, previousVersion] = previousVersionPatternMatches;
52+
const nextVersion = packageManifest.version;
53+
54+
console.log(
55+
`\uD83D\uDCA1 ${chalk.yellow(
56+
packageManifest.name,
57+
)} was updated: ${chalk.red(previousVersion)} -> ${chalk.green(
58+
nextVersion,
59+
)}`,
60+
);
61+
62+
if (!nextVersion.startsWith('0.')) {
63+
throw new Error(
64+
`Package version expected to be 0.x.y, but received ${nextVersion}`,
65+
);
66+
}
67+
68+
const npmOTPFlag = NPM_CONFIG_OTP ? `--otp ${NPM_CONFIG_OTP}` : '';
69+
70+
const {code, stderr} = exec(`npm publish ${npmOTPFlag}`, {
71+
cwd: packageAbsolutePath,
72+
silent: true,
73+
});
74+
if (code) {
75+
console.log(
76+
chalk.red(
77+
`\u274c Failed to publish version ${nextVersion} of ${packageManifest.name}. Stderr:`,
78+
),
79+
);
80+
console.log(stderr);
81+
82+
process.exit(1);
83+
} else {
84+
console.log(
85+
`\u2705 Successfully published new version of ${chalk.green(
86+
packageManifest.name,
87+
)}`,
88+
);
89+
}
90+
},
91+
);
92+
93+
process.exit(0);
94+
};
95+
96+
findAndPublishAllBumpedPackages();
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const path = require('path');
11+
const {readdirSync, readFileSync} = require('fs');
12+
13+
const ROOT_LOCATION = path.join(__dirname, '..', '..');
14+
const PACKAGES_LOCATION = path.join(ROOT_LOCATION, 'packages');
15+
16+
const PACKAGES_BLOCK_LIST = ['react-native'];
17+
18+
/**
19+
* Function, which returns an array of all directories inside specified location
20+
*
21+
* @param {string} source Path to directory, where this should be executed
22+
* @returns {string[]} List of directories names
23+
*/
24+
const getDirectories = source =>
25+
readdirSync(source, {withFileTypes: true})
26+
.filter(file => file.isDirectory())
27+
.map(directory => directory.name);
28+
29+
/**
30+
* @callback forEachPackageCallback
31+
* @param {string} packageAbsolutePath
32+
* @param {string} packageRelativePathFromRoot
33+
* @param {Object} packageManifest
34+
*/
35+
36+
/**
37+
* Iterate through every package inside /packages (ignoring react-native) and call provided callback for each of them
38+
*
39+
* @param {forEachPackageCallback} callback The callback which will be called for each package
40+
*/
41+
const forEachPackage = callback => {
42+
// We filter react-native package on purpose, so that no CI's script will be executed for this package in future
43+
const packagesDirectories = getDirectories(PACKAGES_LOCATION).filter(
44+
directoryName => !PACKAGES_BLOCK_LIST.includes(directoryName),
45+
);
46+
47+
packagesDirectories.forEach(packageDirectory => {
48+
const packageAbsolutePath = path.join(PACKAGES_LOCATION, packageDirectory);
49+
const packageRelativePathFromRoot = path.join('packages', packageDirectory);
50+
51+
const packageManifest = JSON.parse(
52+
readFileSync(path.join(packageAbsolutePath, 'package.json')),
53+
);
54+
55+
callback(packageAbsolutePath, packageRelativePathFromRoot, packageManifest);
56+
});
57+
};
58+
59+
module.exports = forEachPackage;

0 commit comments

Comments
 (0)