feat(react-native-github): automate publishing bumped packages via circleci (#35621)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/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: c5d0ed5b852b744a699ecb88861ea3e82200e1f3
This commit is contained in:
Ruslan Lesiutin 2022-12-14 02:17:16 -08:00 коммит произвёл Facebook GitHub Bot
Родитель 9f9111bd7b
Коммит 83afdafae4
5 изменённых файлов: 270 добавлений и 4 удалений

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

@ -34,6 +34,18 @@ references:
attach_workspace:
at: *hermes_workspace_root
main_only: &main_only
filters:
branches:
only: main
main_or_stable_only: &main_or_stable_only
filters:
branches:
only:
- main
- /0\.[0-9]+[\.[0-9]+]?-stable/
# -------------------------
# Dependency Anchors
# -------------------------
@ -1561,6 +1573,17 @@ jobs:
command: |
echo "Nightly build run"
find_and_publish_bumped_packages:
executor: reactnativeandroid
steps:
- checkout
- run:
name: Set NPM auth token
command: echo "//registry.npmjs.org/:_authToken=${CIRCLE_NPM_TOKEN}" > ~/.npmrc
- run:
name: Find and publish all bumped packages
command: node ./scripts/monorepo/find-and-publish-all-bumped-packages.js
# -------------------------
# PIPELINE PARAMETERS
@ -1749,11 +1772,8 @@ workflows:
unless: << pipeline.parameters.run_package_release_workflow_only >>
triggers:
- schedule:
<<: *main_only
cron: "0 20 * * *"
filters:
branches:
only:
- main
jobs:
- nightly_job
@ -1776,3 +1796,8 @@ workflows:
- build_hermesc_linux
- build_hermes_macos
- build_hermesc_windows
publish_bumped_packages:
jobs:
- find_and_publish_bumped_packages:
<<: *main_or_stable_only

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

@ -0,0 +1,35 @@
/**
* 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 {exec} = require('shelljs');
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('../monorepo/for-each-package', () => jest.fn());
describe('findAndPublishAllBumpedPackages', () => {
it('throws an error if updated version is not 0.x.y', () => {
const mockedPackageNewVersion = '1.0.0';
forEachPackage.mockImplementationOnce(callback => {
callback('absolute/path/to/package', 'to/package', {
version: mockedPackageNewVersion,
});
});
exec.mockImplementationOnce(() => ({
stdout: `- "version": "0.72.0"\n+ "version": "${mockedPackageNewVersion}"\n`,
}));
expect(() => findAndPublishAllBumpedPackages()).toThrow(
`Package version expected to be 0.x.y, but received ${mockedPackageNewVersion}`,
);
});
});

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

@ -0,0 +1,51 @@
/**
* 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 {readdirSync, readFileSync} = require('fs');
const forEachPackage = require('../monorepo/for-each-package');
jest.mock('fs', () => ({
readdirSync: jest.fn(),
readFileSync: jest.fn(),
}));
describe('forEachPackage', () => {
it('executes callback call with parameters', () => {
const callback = jest.fn();
const mockedPackageManifest = '{"name": "my-new-package"}';
const mockedParsedPackageManifest = JSON.parse(mockedPackageManifest);
const mockedPackageName = 'my-new-package';
readdirSync.mockImplementationOnce(() => [
{name: mockedPackageName, isDirectory: () => true},
]);
readFileSync.mockImplementationOnce(() => mockedPackageManifest);
forEachPackage(callback);
expect(callback).toHaveBeenCalledWith(
path.join(__dirname, '..', '..', 'packages', mockedPackageName),
path.join('packages', mockedPackageName),
mockedParsedPackageManifest,
);
});
it('filters react-native folder', () => {
const callback = jest.fn();
readdirSync.mockImplementationOnce(() => [
{name: 'react-native', isDirectory: () => true},
]);
forEachPackage(callback);
expect(callback).not.toHaveBeenCalled();
});
});

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

@ -0,0 +1,96 @@
/**
* 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 chalk = require('chalk');
const {exec} = require('shelljs');
const forEachPackage = require('./for-each-package');
const ROOT_LOCATION = path.join(__dirname, '..', '..');
const NPM_CONFIG_OTP = process.env.NPM_CONFIG_OTP;
const findAndPublishAllBumpedPackages = () => {
console.log('Traversing all packages inside /packages...');
forEachPackage(
(packageAbsolutePath, packageRelativePathFromRoot, packageManifest) => {
if (packageManifest.private) {
console.log(
`\u23ED Skipping private package ${chalk.dim(packageManifest.name)}`,
);
return;
}
const diff = exec(
`git log -p --format="" HEAD~1..HEAD ${packageRelativePathFromRoot}/package.json`,
{cwd: ROOT_LOCATION, silent: true},
).stdout;
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,
)}`,
);
return;
}
const [, previousVersion] = previousVersionPatternMatches;
const nextVersion = packageManifest.version;
console.log(
`\uD83D\uDCA1 ${chalk.yellow(
packageManifest.name,
)} was updated: ${chalk.red(previousVersion)} -> ${chalk.green(
nextVersion,
)}`,
);
if (!nextVersion.startsWith('0.')) {
throw new Error(
`Package version expected to be 0.x.y, but received ${nextVersion}`,
);
}
const npmOTPFlag = NPM_CONFIG_OTP ? `--otp ${NPM_CONFIG_OTP}` : '';
const {code, stderr} = exec(`npm publish ${npmOTPFlag}`, {
cwd: packageAbsolutePath,
silent: true,
});
if (code) {
console.log(
chalk.red(
`\u274c Failed to publish version ${nextVersion} of ${packageManifest.name}. Stderr:`,
),
);
console.log(stderr);
process.exit(1);
} else {
console.log(
`\u2705 Successfully published new version of ${chalk.green(
packageManifest.name,
)}`,
);
}
},
);
process.exit(0);
};
findAndPublishAllBumpedPackages();

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

@ -0,0 +1,59 @@
/**
* 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 {readdirSync, readFileSync} = require('fs');
const ROOT_LOCATION = path.join(__dirname, '..', '..');
const PACKAGES_LOCATION = path.join(ROOT_LOCATION, 'packages');
const PACKAGES_BLOCK_LIST = ['react-native'];
/**
* Function, which returns an array of all directories inside specified location
*
* @param {string} source Path to directory, where this should be executed
* @returns {string[]} List of directories names
*/
const getDirectories = source =>
readdirSync(source, {withFileTypes: true})
.filter(file => file.isDirectory())
.map(directory => directory.name);
/**
* @callback forEachPackageCallback
* @param {string} packageAbsolutePath
* @param {string} packageRelativePathFromRoot
* @param {Object} packageManifest
*/
/**
* Iterate through every package inside /packages (ignoring react-native) and call provided callback for each of them
*
* @param {forEachPackageCallback} callback The callback which will be called for each package
*/
const forEachPackage = callback => {
// We filter react-native package on purpose, so that no CI's script will be executed for this package in future
const packagesDirectories = getDirectories(PACKAGES_LOCATION).filter(
directoryName => !PACKAGES_BLOCK_LIST.includes(directoryName),
);
packagesDirectories.forEach(packageDirectory => {
const packageAbsolutePath = path.join(PACKAGES_LOCATION, packageDirectory);
const packageRelativePathFromRoot = path.join('packages', packageDirectory);
const packageManifest = JSON.parse(
readFileSync(path.join(packageAbsolutePath, 'package.json')),
);
callback(packageAbsolutePath, packageRelativePathFromRoot, packageManifest);
});
};
module.exports = forEachPackage;