Skip to content

Commit

Permalink
feat(react-native-github): automate publishing bumped packages via ci…
Browse files Browse the repository at this point in the history
…rcleci (#35621)

Summary:
Pull Request resolved: #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
  • Loading branch information
hoxyq authored and facebook-github-bot committed Dec 14, 2022
1 parent 9f9111b commit 83afdaf
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 4 deletions.
33 changes: 29 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
# -------------------------
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
35 changes: 35 additions & 0 deletions scripts/__tests__/find-and-publish-all-bumped-packages-test.js
Original file line number Diff line number Diff line change
@@ -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}`,
);
});
});
51 changes: 51 additions & 0 deletions scripts/__tests__/for-each-package-test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
96 changes: 96 additions & 0 deletions scripts/monorepo/find-and-publish-all-bumped-packages.js
Original file line number Diff line number Diff line change
@@ -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();
59 changes: 59 additions & 0 deletions scripts/monorepo/for-each-package.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 83afdaf

Please sign in to comment.