From fe0306d6372da06a992dbba87aaecbd3293927bb Mon Sep 17 00:00:00 2001 From: Luna Wei Date: Mon, 8 Jan 2024 08:38:53 -0800 Subject: [PATCH] Support specifying dist-tags for monorepo package bumps (#42146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Currently our CI will auto-tag any `npm publish` as `latest` for the monorepo packages. This is because [we do not specify a tag](https://github.com/facebook/react-native/blob/main/scripts/monorepo/find-and-publish-all-bumped-packages.js#L104), so npm will [default to `latest`](https://docs.npmjs.com/cli/v10/commands/npm-dist-tag#description). We encountered a similar issue for `react-native` awhile ago and fixed that with [always specifying a tag](https://github.com/facebook/react-native/blob/main/scripts/npm-utils.js#L84), with the explicit opt-in for `latest`. yarn and npm will resolve `*` dependencies using `latest`. This will be a problem for any React Native version that uses `*` deps. We have actively tried to remove these `*` versions but older patches may still contain them. When we do a monorepo package bump, it may be for 0.71 and for a user who is initializing a 0.72 version project (that still has * deps), they will receive monorepo packages of version `0.71.x`, which is not compatible. (React Native monorepo packages do not faithfully follow semver) This change allows us to specify what tags to use and suggest tags based on what branch you are on and asks for confirmation ``` > branch 0.73-stable ? Select suggested npm tags. (Press to select, to toggle all, to invert selection) ❯◉ "0.73-stable" ◉ "latest" ? Confirm these tags for *ALL* packages being bumped: "0.73-stable","latest" (Y/n) > branch 0.72-stable ? Select suggested npm tags. (Press to select, to toggle all, to invert selection) ❯◉ "0.72-stable" ◯ "latest" ? Confirm these tags for *ALL* packages being bumped: "0.72-stable" (Y/n) > branch main ? Select suggested npm tags. (Press to select, to toggle all, to invert selection) ❯◉ "nightly" ? Confirm these tags for *ALL* packages being bumped: "nightly" (Y/n) ``` ## Changelog: [INTERNAL] [CHANGED] - Support dist-tags in publishing monorepo packages to avoid default "latest" tag. Pull Request resolved: https://github.com/facebook/react-native/pull/42146 Test Plan: `yarn test scripts/` Reviewed By: NickGerleman Differential Revision: D52551769 Pulled By: lunaleaps fbshipit-source-id: 52f923464387cffdc6ca22c6f0a45425965a3680 --- scripts/__tests__/npm-utils-test.js | 15 ++++- ...nd-and-publish-all-bumped-packages-test.js | 16 ++++- .../__tests__/get-and-update-packages-test.js | 4 +- .../bump-all-updated-packages/index.js | 59 ++++++++++++++++++- .../find-and-publish-all-bumped-packages.js | 26 +++++++- scripts/monorepo/get-and-update-packages.js | 2 +- scripts/npm-utils.js | 6 +- scripts/publish-npm.js | 2 +- 8 files changed, 116 insertions(+), 14 deletions(-) diff --git a/scripts/__tests__/npm-utils-test.js b/scripts/__tests__/npm-utils-test.js index f6da57d4961b23..adb3de08b69fe9 100644 --- a/scripts/__tests__/npm-utils-test.js +++ b/scripts/__tests__/npm-utils-test.js @@ -95,7 +95,7 @@ describe('npm-utils', () => { it('should run publish command', () => { publishPackage( 'path/to/my-package', - {tag: 'latest', otp: 'otp'}, + {tags: ['latest'], otp: 'otp'}, {silent: true, cwd: 'i/expect/this/to/be/overriden'}, ); expect(execMock).toHaveBeenCalledWith( @@ -105,12 +105,23 @@ describe('npm-utils', () => { }); it('should run publish command when no execOptions', () => { - publishPackage('path/to/my-package', {tag: 'latest', otp: 'otp'}); + publishPackage('path/to/my-package', {tags: ['latest'], otp: 'otp'}); expect(execMock).toHaveBeenCalledWith( 'npm publish --tag latest --otp otp', {cwd: 'path/to/my-package'}, ); }); + + it('should handle multiple tags', () => { + publishPackage('path/to/my-package', { + tags: ['next', '0.72-stable'], + otp: 'otp', + }); + expect(execMock).toHaveBeenCalledWith( + 'npm publish --tag next --tag 0.72-stable --otp otp', + {cwd: 'path/to/my-package'}, + ); + }); }); describe('getNpmInfo', () => { diff --git a/scripts/monorepo/__tests__/find-and-publish-all-bumped-packages-test.js b/scripts/monorepo/__tests__/find-and-publish-all-bumped-packages-test.js index 4890fb378a7d73..15c4c683d416eb 100644 --- a/scripts/monorepo/__tests__/find-and-publish-all-bumped-packages-test.js +++ b/scripts/monorepo/__tests__/find-and-publish-all-bumped-packages-test.js @@ -8,7 +8,10 @@ */ const {PUBLISH_PACKAGES_TAG} = require('../constants'); -const findAndPublishAllBumpedPackages = require('../find-and-publish-all-bumped-packages'); +const { + findAndPublishAllBumpedPackages, + getTagsFromCommitMessage, +} = require('../find-and-publish-all-bumped-packages'); const forEachPackage = require('../for-each-package'); const {spawnSync} = require('child_process'); @@ -42,3 +45,14 @@ describe('findAndPublishAllBumpedPackages', () => { ); }); }); + +describe('getTagsFromCommitMessage', () => { + it('should parse tags out', () => { + const commitMsg = `This may be any commit message before it like tag a \n\n${PUBLISH_PACKAGES_TAG}&tagA&tagB&tagA\n`; + expect(getTagsFromCommitMessage(commitMsg)).toEqual([ + 'tagA', + 'tagB', + 'tagA', + ]); + }); +}); diff --git a/scripts/monorepo/__tests__/get-and-update-packages-test.js b/scripts/monorepo/__tests__/get-and-update-packages-test.js index 6ccc21df78b8ec..44a51f5ad3cc26 100644 --- a/scripts/monorepo/__tests__/get-and-update-packages-test.js +++ b/scripts/monorepo/__tests__/get-and-update-packages-test.js @@ -88,7 +88,7 @@ describe('getAndUpdatePackages', () => { forEachPackageThatShouldBePublished(package => { expect(publishPackageMock).toHaveBeenCalledWith( package.packageAbsolutePath, - {otp: undefined, tag: 'nightly'}, + {otp: undefined, tags: ['nightly']}, ); }); @@ -154,7 +154,7 @@ describe('getAndUpdatePackages', () => { forEachPackageThatShouldBePublished(package => { expect(publishPackageMock).toHaveBeenCalledWith( package.packageAbsolutePath, - {otp: undefined, tag: 'prealpha'}, + {otp: undefined, tags: ['prealpha']}, ); }); diff --git a/scripts/monorepo/bump-all-updated-packages/index.js b/scripts/monorepo/bump-all-updated-packages/index.js index a04e043b5dd252..8ba2b1d486179c 100644 --- a/scripts/monorepo/bump-all-updated-packages/index.js +++ b/scripts/monorepo/bump-all-updated-packages/index.js @@ -7,6 +7,9 @@ * @format */ +const {getPackageVersionStrByTag} = require('../../npm-utils'); +const {getBranchName} = require('../../scm-utils'); +const {isReleaseBranch, parseVersion} = require('../../version-utils'); const alignPackageVersions = require('../align-package-versions'); const checkForGitChanges = require('../check-for-git-changes'); const { @@ -148,6 +151,58 @@ const main = async () => { alignPackageVersions(); echo(chalk.green('Done!\n')); + // Figure out the npm dist-tags we want for all monorepo packages we're bumping + const branchName = getBranchName(); + const choices = []; + + if (branchName === 'main') { + choices.push({name: '"nightly"', value: 'nightly', checked: true}); + } else if (isReleaseBranch(branchName)) { + choices.push({ + name: `"${branchName}"`, + value: branchName, + checked: true, + }); + + const latestVersion = getPackageVersionStrByTag('react-native', 'latest'); + const {major, minor} = parseVersion(latestVersion, 'release'); + choices.push({ + name: '"latest"', + value: 'latest', + checked: `${major}.${minor}-stable` === branchName, + }); + } else { + echo( + 'You should be running `yarn bump-all-updated-packages` only from release or main branch', + ); + exit(1); + } + + const {tags} = await inquirer.prompt([ + { + type: 'checkbox', + name: 'tags', + message: 'Select suggested npm tags.', + choices, + required: true, + }, + ]); + + const {confirm} = await inquirer.prompt({ + type: 'confirm', + name: 'confirm', + message: `Confirm these tags for *ALL* packages being bumped: ${tags + .map(t => `"${t}"`) + .join()}`, + }); + + if (!confirm) { + echo('Exiting without commiting...'); + exit(0); + } + + const tagString = '&' + tags.join('&'); + await inquirer .prompt([ { @@ -179,7 +234,7 @@ const main = async () => { } case COMMIT_WITH_GENERIC_MESSAGE_CHOICE: { - exec(`git commit -am "${GENERIC_COMMIT_MESSAGE}"`, { + exec(`git commit -am "${GENERIC_COMMIT_MESSAGE}${tagString}"`, { cwd: ROOT_LOCATION, silent: true, }); @@ -197,7 +252,7 @@ const main = async () => { silent: true, }).stdout.trim(); const commitMessageWithTag = - enteredCommitMessage + `\n\n${PUBLISH_PACKAGES_TAG}`; + enteredCommitMessage + `\n\n${PUBLISH_PACKAGES_TAG}${tagString}`; exec(`git commit --amend -m "${commitMessageWithTag}"`, { cwd: ROOT_LOCATION, diff --git a/scripts/monorepo/find-and-publish-all-bumped-packages.js b/scripts/monorepo/find-and-publish-all-bumped-packages.js index 17ffb58ed7f60b..5cec500e462410 100644 --- a/scripts/monorepo/find-and-publish-all-bumped-packages.js +++ b/scripts/monorepo/find-and-publish-all-bumped-packages.js @@ -16,6 +16,16 @@ const path = require('path'); const ROOT_LOCATION = path.join(__dirname, '..', '..'); const NPM_CONFIG_OTP = process.env.NPM_CONFIG_OTP; +function getTagsFromCommitMessage(msg) { + // ex message we're trying to parse tags out of + // `_some_message_here_${PUBLISH_PACKAGES_TAG}&tagA&tagB\n`; + return msg + .substring(msg.indexOf(PUBLISH_PACKAGES_TAG)) + .trim() + .split('&') + .slice(1); +} + const findAndPublishAllBumpedPackages = () => { console.log('Traversing all packages inside /packages...'); @@ -101,7 +111,12 @@ const findAndPublishAllBumpedPackages = () => { ); } - const result = publishPackage(packageAbsolutePath, {otp: NPM_CONFIG_OTP}); + const tags = getTagsFromCommitMessage(commitMessage); + + const result = publishPackage(packageAbsolutePath, { + tags, + otp: NPM_CONFIG_OTP, + }); if (result.code !== 0) { console.log( `\u274c Failed to publish version ${nextVersion} of ${packageManifest.name}. npm publish exited with code ${result.code}:`, @@ -120,4 +135,11 @@ const findAndPublishAllBumpedPackages = () => { process.exit(0); }; -findAndPublishAllBumpedPackages(); +if (require.main === module) { + findAndPublishAllBumpedPackages(); +} + +module.exports = { + findAndPublishAllBumpedPackages, + getTagsFromCommitMessage, +}; diff --git a/scripts/monorepo/get-and-update-packages.js b/scripts/monorepo/get-and-update-packages.js index 62446e984b3301..67b74e5383ef93 100644 --- a/scripts/monorepo/get-and-update-packages.js +++ b/scripts/monorepo/get-and-update-packages.js @@ -135,7 +135,7 @@ function publishPackages( const version = packageMetadata.packageJson.version; const result = publishPackage(packageAbsolutePath, { - tag: tag, + tags: [tag], otp: process.env.NPM_CONFIG_OTP, }); diff --git a/scripts/npm-utils.js b/scripts/npm-utils.js index 879ae65d11b52c..95e76a28134126 100644 --- a/scripts/npm-utils.js +++ b/scripts/npm-utils.js @@ -91,14 +91,14 @@ function getNpmInfo(buildType) { } function publishPackage(packagePath, packageOptions, execOptions) { - const {tag, otp} = packageOptions; - const tagFlag = tag ? ` --tag ${tag}` : ''; + const {otp, tags} = packageOptions; + const tagsFlag = tags ? tags.map(t => ` --tag ${t}`).join('') : ''; const otpFlag = otp ? ` --otp ${otp}` : ''; const options = execOptions ? {...execOptions, cwd: packagePath} : {cwd: packagePath}; - return exec(`npm publish${tagFlag}${otpFlag}`, options); + return exec(`npm publish${tagsFlag}${otpFlag}`, options); } function diffPackages(packageSpecA, packageSpecB, options) { diff --git a/scripts/publish-npm.js b/scripts/publish-npm.js index e8e710b2caf7b4..9051f207a9021d 100755 --- a/scripts/publish-npm.js +++ b/scripts/publish-npm.js @@ -96,7 +96,7 @@ function publishNpm(buildType) { const packagePath = path.join(__dirname, '..', 'packages', 'react-native'); const result = publishPackage(packagePath, { - tag, + tags: [tag], otp: process.env.NPM_CONFIG_OTP, });