From a5c3f441f4a61c303ee84e16528deee0819cdb0e Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Tue, 13 Dec 2022 05:46:02 -0800 Subject: [PATCH] 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 Differential Revision: D41972733 fbshipit-source-id: cc7866f01b57603ffd81dc38ca5bba94b36ea6c3 --- .circleci/config.yml | 27 +++++- .../find-and-publish-all-bumped-packages.js | 93 +++++++++++++++++++ scripts/monorepo/for-each-package.js | 59 ++++++++++++ 3 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 scripts/monorepo/find-and-publish-all-bumped-packages.js create mode 100644 scripts/monorepo/for-each-package.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 541e48a9c92053..38db40f7d69242 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,6 +34,11 @@ references: attach_workspace: at: *hermes_workspace_root + main_only: &main_only + filters: + branches: + only: main + # ------------------------- # Dependency Anchors # ------------------------- @@ -1561,6 +1566,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 +1765,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 +1789,9 @@ workflows: - build_hermesc_linux - build_hermes_macos - build_hermesc_windows + + publish_bumped_packages: + unless: << pipeline.parameters.run_package_release_workflow_only >> + jobs: + - find_and_publish_bumped_packages: + <<: *main_only diff --git a/scripts/monorepo/find-and-publish-all-bumped-packages.js b/scripts/monorepo/find-and-publish-all-bumped-packages.js new file mode 100644 index 00000000000000..c0f178569ab85f --- /dev/null +++ b/scripts/monorepo/find-and-publish-all-bumped-packages.js @@ -0,0 +1,93 @@ +/** + * 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 nextVersionPatternMatches = diff.match( + /\+ {2}"version": "([0-9]+.[0-9]+.[0-9]+)"/, + ); + const [, previousVersion] = previousVersionPatternMatches; + const [, nextVersion] = nextVersionPatternMatches; + + console.log( + `\uD83D\uDCA1 ${chalk.yellow( + packageManifest.name, + )} was updated: ${chalk.red(previousVersion)} -> ${chalk.green( + 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(); diff --git a/scripts/monorepo/for-each-package.js b/scripts/monorepo/for-each-package.js new file mode 100644 index 00000000000000..4616559d22aaf6 --- /dev/null +++ b/scripts/monorepo/for-each-package.js @@ -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(packageRelativePathFromRoot, 'package.json')), + ); + + callback(packageAbsolutePath, packageRelativePathFromRoot, packageManifest); + }); +}; + +module.exports = forEachPackage;