From fd9e295befcd8781190ec26a6a2fc4ef39fb1c15 Mon Sep 17 00:00:00 2001 From: Luna Wei Date: Fri, 2 Jun 2023 17:40:52 -0700 Subject: [PATCH] Publish nightly monorepo packages (#37556) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/37556 Changelog: [Internal] Before making this change, we need to publish `nightly` versions of all existing monorepo dependencies. Reviewed By: hoxyq Differential Revision: D46117197 fbshipit-source-id: bcf6364e068579e63ca19e8161dcd32de4353e56 --- scripts/__tests__/publish-npm-test.js | 3 +- ...h-nightly-for-each-changed-package-test.js | 101 +++++++++++++ ...ublish-nightly-for-each-changed-package.js | 135 ++++++++++++++++++ scripts/npm-utils.js | 31 +++- scripts/publish-npm.js | 8 ++ scripts/scm-utils.js | 14 ++ 6 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 scripts/monorepo/__tests__/publish-nightly-for-each-changed-package-test.js create mode 100644 scripts/monorepo/publish-nightly-for-each-changed-package.js diff --git a/scripts/__tests__/publish-npm-test.js b/scripts/__tests__/publish-npm-test.js index 2b4825bdf42..32bc613ff98 100644 --- a/scripts/__tests__/publish-npm-test.js +++ b/scripts/__tests__/publish-npm-test.js @@ -32,7 +32,8 @@ jest .mock('./../release-utils', () => ({ generateAndroidArtifacts: jest.fn(), publishAndroidArtifactsToMaven: publishAndroidArtifactsToMavenMock, - })); + })) + .mock('../monorepo/publish-nightly-for-each-changed-package'); const date = new Date('2023-04-20T23:52:39.543Z'); diff --git a/scripts/monorepo/__tests__/publish-nightly-for-each-changed-package-test.js b/scripts/monorepo/__tests__/publish-nightly-for-each-changed-package-test.js new file mode 100644 index 00000000000..b7d420809bf --- /dev/null +++ b/scripts/monorepo/__tests__/publish-nightly-for-each-changed-package-test.js @@ -0,0 +1,101 @@ +/** + * 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 publishNightlyForEachChangedPackage = require('../publish-nightly-for-each-changed-package'); + +const mockPackages = [ + { + packageManifest: {name: 'packageA', version: 'local-version'}, + packageAbsolutePath: '/some/place/packageA', + packageRelativePathFromRoot: './place/packageA', + }, +]; + +const execMock = jest.fn(); +const writeFileSyncMock = jest.fn(); +const diffPackagesMock = jest.fn(); +const publishPackageMock = jest.fn(); + +jest + .mock('shelljs', () => ({ + exec: execMock, + rm: jest.fn(), + })) + .mock('fs', () => ({ + writeFileSync: writeFileSyncMock, + })) + .mock('../for-each-package', () => callback => { + mockPackages.forEach( + ({packageManifest, packageAbsolutePath, packageRelativePathFromRoot}) => + callback( + packageAbsolutePath, + packageRelativePathFromRoot, + packageManifest, + ), + ); + }) + .mock('../../scm-utils', () => ({ + restore: jest.fn(), + })) + .mock('../../npm-utils', () => ({ + getPackageVersionStrByTag: () => 'published-nightly-version', + diffPackages: diffPackagesMock, + publishPackage: publishPackageMock, + pack: jest.fn(), + })); + +describe('publishNightlyForEachChangedPackage', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('publishes because there are changes', () => { + const nightlyVersion = '0.73.0-nightly-202108-shortcommit'; + publishPackageMock.mockImplementationOnce(() => ({code: 0})); + diffPackagesMock.mockImplementationOnce(() => 'some-file-name.js\n'); + + publishNightlyForEachChangedPackage(nightlyVersion); + + // ensure we set the version of the last published nightly (for diffing) + expect(writeFileSyncMock.mock.calls[0][1]).toBe( + '{\n "name": "packageA",\n "version": "published-nightly-version"\n}\n', + ); + + expect(diffPackagesMock).toBeCalledWith( + 'packageA@nightly', + 'packageA-published-nightly-version.tgz', + { + cwd: '/some/place/packageA', + }, + ); + + // when determining that we DO want to publish, ensure we update the version to the provded nightly version we want to use + expect(writeFileSyncMock.mock.calls[1][1]).toBe( + `{\n "name": "packageA",\n "version": "${nightlyVersion}"\n}\n`, + ); + + expect(publishPackageMock).toBeCalled(); + }); + + it('doesnt publish because no changes', () => { + const nightlyVersion = '0.73.0-nightly-202108-shortcommit'; + diffPackagesMock.mockImplementationOnce(() => '\n'); + + publishNightlyForEachChangedPackage(nightlyVersion); + + expect(writeFileSyncMock.mock.calls[0][1]).toBe( + '{\n "name": "packageA",\n "version": "published-nightly-version"\n}\n', + ); + + // in this test, we expect there to be no differences between last published nightly and local + // so we never update the version and we don't publish + expect(writeFileSyncMock.mock.calls.length).toBe(1); + expect(publishPackageMock).not.toBeCalled(); + }); +}); diff --git a/scripts/monorepo/publish-nightly-for-each-changed-package.js b/scripts/monorepo/publish-nightly-for-each-changed-package.js new file mode 100644 index 00000000000..1c04d722180 --- /dev/null +++ b/scripts/monorepo/publish-nightly-for-each-changed-package.js @@ -0,0 +1,135 @@ +/** + * 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 + * @oncall react_native + */ + +const forEachPackage = require('./for-each-package'); +const {rm} = require('shelljs'); +const { + getPackageVersionStrByTag, + diffPackages, + publishPackage, + pack, +} = require('../npm-utils'); +const {restore} = require('../scm-utils'); +const path = require('path'); +const {writeFileSync} = require('fs'); + +function hasChanges( + currentNightlyVersion, + packageManifest, + packageAbsolutePath, +) { + // Set local package to same nightly version so diff is comparable + writeFileSync( + path.join(packageAbsolutePath, 'package.json'), + JSON.stringify( + {...packageManifest, version: currentNightlyVersion}, + null, + 2, + ) + '\n', + 'utf-8', + ); + + // prepare local package + pack(packageAbsolutePath); + + // ex. react-native-codegen-0.73.0-nightly-20230530-730ca3540.tgz + const localTarballName = `${packageManifest.name + .replace('@', '') + .replace('/', '-')}-${currentNightlyVersion}.tgz`; + + // npm diff --diff=@nightly --diff=. --diff-name-only + const diff = diffPackages( + `${packageManifest.name}@nightly`, + localTarballName, + { + cwd: packageAbsolutePath, + }, + ); + + // delete tarball and restore package.json changes + rm({cwd: packageAbsolutePath}, localTarballName); + restore(packageAbsolutePath); + + return diff.trim().length !== 0; +} + +/** + * Publish nightlies for monorepo packages that changed since last publish. + * This is called by `react-native`'s nightly job. + * + * Note: This does not publish `package/react-native`'s nightly. That is handled in `publish-npm`. + */ +function publishNightlyForEachChangedPackage(nightlyVersion) { + forEachPackage( + (packageAbsolutePath, packageRelativePathFromRoot, packageManifest) => { + if (packageManifest.private) { + console.log(`\u23ED Skipping private package ${packageManifest.name}`); + + return; + } + + let lastPublishedNightlyVersion; + let shouldPublishNightly = false; + console.log( + `\n---- Attempting to publish nightly for ${packageManifest.name}`, + ); + try { + lastPublishedNightlyVersion = getPackageVersionStrByTag( + packageManifest.name, + 'nightly', + ); + shouldPublishNightly = hasChanges( + lastPublishedNightlyVersion, + packageManifest, + packageAbsolutePath, + ); + } catch (e) { + console.error( + `Unable to verify if ${packageManifest.name} has changes due to error:`, + ); + console.error(e.message); + console.log(`\u23ED Skipping package ${packageManifest.name}`); + return; + } + + if (!shouldPublishNightly) { + console.log( + `Detected no changes in ${packageManifest.name}@nightly since last publish. Skipping.`, + ); + return; + } + + // Set the local package to the updated nightly version + writeFileSync( + path.join(packageAbsolutePath, 'package.json'), + JSON.stringify({...packageManifest, version: nightlyVersion}, null, 2) + + '\n', + 'utf-8', + ); + + const result = publishPackage(packageAbsolutePath, { + tag: 'nightly', + otp: process.env.NPM_CONFIG_OTP, + }); + if (result.code !== 0) { + console.log( + `\u274c Failed to publish version ${nightlyVersion} of ${packageManifest.name}. npm publish exited with code ${result.code}:`, + ); + console.error(result.stderr); + } else { + console.log( + `\u2705 Successfully published new version of ${packageManifest.name}`, + ); + } + }, + ); +} + +module.exports = publishNightlyForEachChangedPackage; diff --git a/scripts/npm-utils.js b/scripts/npm-utils.js index 15752a340eb..5586d7e3372 100644 --- a/scripts/npm-utils.js +++ b/scripts/npm-utils.js @@ -12,10 +12,10 @@ const {exec} = require('shelljs'); function getPackageVersionStrByTag(packageName, tag) { - const result = exec(`npm view ${packageName}@${tag} version`); + const result = exec(`npm view ${packageName}@${tag} version`, {silent: true}); if (result.code) { - throw `Failed to get ${tag} version from npm\n${result.stderr}`; + throw new Error(`Failed to get ${tag} version from npm\n${result.stderr}`); } return result.stdout.trim(); } @@ -31,7 +31,34 @@ function publishPackage(packagePath, packageOptions, execOptions) { return exec(`npm publish${tagFlag}${otpFlag}`, options); } +function diffPackages(packageSpecA, packageSpecB, options) { + const result = exec( + `npm diff --diff=${packageSpecA} --diff=${packageSpecB} --diff-name-only`, + options, + ); + + if (result.code) { + throw new Error( + `Failed to diff ${packageSpecA} and ${packageSpecB}\n${result.stderr}`, + ); + } + + return result.stdout; +} + +function pack(packagePath) { + const result = exec('npm pack', { + cwd: packagePath, + }); + + if (result.code !== 0) { + throw new Error(result.stderr); + } +} + module.exports = { getPackageVersionStrByTag, publishPackage, + diffPackages, + pack, }; diff --git a/scripts/publish-npm.js b/scripts/publish-npm.js index 275407ad621..653a3782c56 100755 --- a/scripts/publish-npm.js +++ b/scripts/publish-npm.js @@ -17,6 +17,7 @@ const { getCurrentCommit, isTaggedLatest, } = require('./scm-utils'); +const publishNightlyForEachChangedPackage = require('./monorepo/publish-nightly-for-each-changed-package'); const { generateAndroidArtifacts, publishAndroidArtifactsToMaven, @@ -136,6 +137,7 @@ function publishNpm(buildType) { // Set version number in various files (package.json, gradle.properties etc) // For non-nightly, non-dry-run, CircleCI job `prepare_package_for_release` does this if (buildType === 'nightly' || buildType === 'dry-run') { + // Sets the version for package/react-native if ( exec( `node scripts/set-rn-version.js --to-version ${version} --build-type ${buildType}`, @@ -146,6 +148,11 @@ function publishNpm(buildType) { } } + // set and publish the relevant monorepo packages + if (buildType === 'nightly') { + publishNightlyForEachChangedPackage(version); + } + generateAndroidArtifacts(version); // Write version number to the build folder @@ -166,6 +173,7 @@ function publishNpm(buildType) { tag, otp: process.env.NPM_CONFIG_OTP, }); + if (result.code) { echo('Failed to publish package to npm'); return exit(1); diff --git a/scripts/scm-utils.js b/scripts/scm-utils.js index 11810e6470c..eab057bd5f4 100644 --- a/scripts/scm-utils.js +++ b/scripts/scm-utils.js @@ -85,6 +85,19 @@ function revertFiles(filePaths, tmpFolder) { } } +// git restore for local path +function restore(repoPath) { + const result = exec('git restore .', { + cwd: repoPath, + }); + + if (result.code !== 0) { + throw new Error(result.stderr); + } + + return; +} + module.exports = { exitIfNotOnGit, getCurrentCommit, @@ -92,4 +105,5 @@ module.exports = { isTaggedLatest, revertFiles, saveFiles, + restore, };