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:
Родитель
9f9111bd7b
Коммит
83afdafae4
|
@ -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;
|
Загрузка…
Ссылка в новой задаче