Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;