From 5b007573ac7eb39660273302d5a4679e3d2c44e3 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 9 Aug 2019 13:12:00 -0700 Subject: [PATCH] Release script supports publishing a subset of packages (#16338) Release script supports publishing a subset of packages (#16338) --- .../create-canary-commands/build-artifacts.js | 2 +- .../confirm-stable-version-numbers.js | 31 +++++++--- .../guess-stable-version-numbers.js | 23 +++++--- .../prepare-stable-commands/parse-params.js | 10 ++++ .../confirm-skipped-packages.js | 31 ++++++++++ .../confirm-version-and-tags.js | 1 - .../download-error-codes-from-ci.js | 6 +- .../release/publish-commands/parse-params.js | 15 ++++- .../print-follow-up-instructions.js | 13 +++- .../update-stable-version-numbers.js | 23 ++++---- .../validate-skip-packages.js | 59 +++++++++++++++++++ .../release/publish-commands/validate-tags.js | 11 +++- scripts/release/publish.js | 19 ++++++ scripts/release/utils.js | 12 ++++ 14 files changed, 219 insertions(+), 37 deletions(-) create mode 100644 scripts/release/publish-commands/confirm-skipped-packages.js create mode 100644 scripts/release/publish-commands/validate-skip-packages.js diff --git a/scripts/release/create-canary-commands/build-artifacts.js b/scripts/release/create-canary-commands/build-artifacts.js index f72cf59678..65867052a0 100644 --- a/scripts/release/create-canary-commands/build-artifacts.js +++ b/scripts/release/create-canary-commands/build-artifacts.js @@ -21,5 +21,5 @@ const run = async ({cwd, dry, tempDirectory}) => { }; module.exports = async params => { - return logPromise(run(params), 'Building artifacts', 420000); + return logPromise(run(params), 'Building artifacts', 600000); }; diff --git a/scripts/release/prepare-stable-commands/confirm-stable-version-numbers.js b/scripts/release/prepare-stable-commands/confirm-stable-version-numbers.js index d00bb38576..dd1229634b 100644 --- a/scripts/release/prepare-stable-commands/confirm-stable-version-numbers.js +++ b/scripts/release/prepare-stable-commands/confirm-stable-version-numbers.js @@ -5,8 +5,9 @@ const prompt = require('prompt-promise'); const semver = require('semver'); const theme = require('../theme'); +const {confirm} = require('../utils'); -const run = async (params, versionsMap) => { +const run = async ({skipPackages}, versionsMap) => { const groupedVersionsMap = new Map(); // Group packages with the same source versions. @@ -26,14 +27,26 @@ const run = async (params, versionsMap) => { for (let i = 0; i < entries.length; i++) { const [bestGuessVersion, packages] = entries[i]; const packageNames = packages.map(name => theme.package(name)).join(', '); - const defaultVersion = bestGuessVersion - ? theme.version(` (default ${bestGuessVersion})`) - : ''; - const version = - (await prompt( - theme`{spinnerSuccess ✓} Version for ${packageNames}${defaultVersion}: ` - )) || bestGuessVersion; - prompt.done(); + + let version = bestGuessVersion; + if ( + skipPackages.some(skipPackageName => + packageNames.includes(skipPackageName) + ) + ) { + await confirm( + theme`{spinnerSuccess ✓} Version for ${packageNames} will remain {version ${bestGuessVersion}}` + ); + } else { + const defaultVersion = bestGuessVersion + ? theme.version(` (default ${bestGuessVersion})`) + : ''; + version = + (await prompt( + theme`{spinnerSuccess ✓} Version for ${packageNames}${defaultVersion}: ` + )) || bestGuessVersion; + prompt.done(); + } // Verify a valid version has been supplied. try { diff --git a/scripts/release/prepare-stable-commands/guess-stable-version-numbers.js b/scripts/release/prepare-stable-commands/guess-stable-version-numbers.js index f5c78fa0d8..1e9b560faf 100644 --- a/scripts/release/prepare-stable-commands/guess-stable-version-numbers.js +++ b/scripts/release/prepare-stable-commands/guess-stable-version-numbers.js @@ -5,7 +5,7 @@ const semver = require('semver'); const {execRead, logPromise} = require('../utils'); -const run = async ({cwd, packages}, versionsMap) => { +const run = async ({cwd, packages, skipPackages}, versionsMap) => { const branch = await execRead('git branch | grep \\* | cut -d " " -f2', { cwd, }); @@ -17,16 +17,21 @@ const run = async ({cwd, packages}, versionsMap) => { // In case local package JSONs are outdated, // guess the next version based on the latest NPM release. const version = await execRead(`npm show ${packageName} version`); - const {major, minor, patch} = semver(version); - // Guess the next version by incrementing patch. - // The script will confirm this later. - // By default, new releases from masters should increment the minor version number, - // and patch releases should be done from branches. - if (branch === 'master') { - versionsMap.set(packageName, `${major}.${minor + 1}.0`); + if (skipPackages.includes(packageName)) { + versionsMap.set(packageName, version); } else { - versionsMap.set(packageName, `${major}.${minor}.${patch + 1}`); + const {major, minor, patch} = semver(version); + + // Guess the next version by incrementing patch. + // The script will confirm this later. + // By default, new releases from masters should increment the minor version number, + // and patch releases should be done from branches. + if (branch === 'master') { + versionsMap.set(packageName, `${major}.${minor + 1}.0`); + } else { + versionsMap.set(packageName, `${major}.${minor}.${patch + 1}`); + } } } catch (error) { // If the package has not yet been published, diff --git a/scripts/release/prepare-stable-commands/parse-params.js b/scripts/release/prepare-stable-commands/parse-params.js index c7d12c001e..fc2256d27f 100644 --- a/scripts/release/prepare-stable-commands/parse-params.js +++ b/scripts/release/prepare-stable-commands/parse-params.js @@ -3,6 +3,7 @@ 'use strict'; const commandLineArgs = require('command-line-args'); +const {splitCommaParams} = require('../utils'); const paramDefinitions = [ { @@ -12,6 +13,13 @@ const paramDefinitions = [ 'Skip NPM and use the build already present in "build/node_modules".', defaultValue: false, }, + { + name: 'skipPackages', + type: String, + multiple: true, + description: 'Packages to exclude from publishing', + defaultValue: [], + }, { name: 'skipTests', type: Boolean, @@ -28,5 +36,7 @@ const paramDefinitions = [ module.exports = () => { const params = commandLineArgs(paramDefinitions); + splitCommaParams(params.skipPackages); + return params; }; diff --git a/scripts/release/publish-commands/confirm-skipped-packages.js b/scripts/release/publish-commands/confirm-skipped-packages.js new file mode 100644 index 0000000000..3e7bc03d68 --- /dev/null +++ b/scripts/release/publish-commands/confirm-skipped-packages.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +'use strict'; + +const clear = require('clear'); +const {confirm} = require('../utils'); +const theme = require('../theme'); + +const run = async ({cwd, packages, skipPackages, tags}) => { + if (skipPackages.length === 0) { + return; + } + + clear(); + + console.log( + theme`{spinnerSuccess ✓} The following packages will not be published as part of this release` + ); + + skipPackages.forEach(packageName => { + console.log(theme`• {package ${packageName}}`); + }); + + await confirm('Do you want to proceed?'); + + clear(); +}; + +// Run this directly because it's fast, +// and logPromise would interfere with console prompting. +module.exports = run; diff --git a/scripts/release/publish-commands/confirm-version-and-tags.js b/scripts/release/publish-commands/confirm-version-and-tags.js index d5095b7ea1..73dadd5aaa 100644 --- a/scripts/release/publish-commands/confirm-version-and-tags.js +++ b/scripts/release/publish-commands/confirm-version-and-tags.js @@ -23,7 +23,6 @@ const run = async ({cwd, packages, tags}) => { ); } - // Cache all package JSONs for easy lookup below. for (let i = 0; i < packages.length; i++) { const packageName = packages[i]; const packageJSONPath = join( diff --git a/scripts/release/publish-commands/download-error-codes-from-ci.js b/scripts/release/publish-commands/download-error-codes-from-ci.js index 989a3a2944..bd5747f2df 100644 --- a/scripts/release/publish-commands/download-error-codes-from-ci.js +++ b/scripts/release/publish-commands/download-error-codes-from-ci.js @@ -4,10 +4,11 @@ const {exec} = require('child-process-promise'); const {readJsonSync} = require('fs-extra'); +const {join} = require('path'); const {getArtifactsList, logPromise} = require('../utils'); const theme = require('../theme'); -const run = async ({cwd, tags}) => { +const run = async ({cwd, packages, tags}) => { if (!tags.includes('latest')) { // Don't update error-codes for alphas. return; @@ -15,8 +16,9 @@ const run = async ({cwd, tags}) => { // All packages are built from a single source revision, // so it is safe to read build info from any one of them. + const arbitraryPackageName = packages[0]; const {buildNumber, environment} = readJsonSync( - `${cwd}/build/node_modules/react/build-info.json` + join(cwd, 'build', 'node_modules', arbitraryPackageName, 'build-info.json') ); // If this release was created on Circle CI, grab the updated error codes from there. diff --git a/scripts/release/publish-commands/parse-params.js b/scripts/release/publish-commands/parse-params.js index 635e22c812..b496d4f957 100644 --- a/scripts/release/publish-commands/parse-params.js +++ b/scripts/release/publish-commands/parse-params.js @@ -4,6 +4,7 @@ const commandLineArgs = require('command-line-args'); const commandLineUsage = require('command-line-usage'); +const {splitCommaParams} = require('../utils'); const paramDefinitions = [ { @@ -18,12 +19,21 @@ const paramDefinitions = [ multiple: true, description: 'NPM tags to point to the new release.', }, + { + name: 'skipPackages', + type: String, + multiple: true, + description: 'Packages to exclude from publishing', + defaultValue: [], + }, ]; module.exports = () => { const params = commandLineArgs(paramDefinitions); - if (!params.tags || params.tags.length === 0) { + const {skipPackages, tags} = params; + + if (!tags || tags.length === 0) { const usage = commandLineUsage([ { content: @@ -51,5 +61,8 @@ module.exports = () => { process.exit(1); } + splitCommaParams(skipPackages); + splitCommaParams(tags); + return params; }; diff --git a/scripts/release/publish-commands/print-follow-up-instructions.js b/scripts/release/publish-commands/print-follow-up-instructions.js index 41e1d4544b..5d2bc3451a 100644 --- a/scripts/release/publish-commands/print-follow-up-instructions.js +++ b/scripts/release/publish-commands/print-follow-up-instructions.js @@ -11,9 +11,10 @@ const {execRead} = require('../utils'); const run = async ({cwd, packages, tags}) => { // All packages are built from a single source revision, - // so it is safe to read the commit number from any one of them. + // so it is safe to read build info from any one of them. + const arbitraryPackageName = packages[0]; const {commit, environment} = readJsonSync( - `${cwd}/build/node_modules/react/build-info.json` + join(cwd, 'build', 'node_modules', arbitraryPackageName, 'build-info.json') ); // Tags are named after the react version. @@ -50,7 +51,13 @@ const run = async ({cwd, packages, tags}) => { const packageName = packages[i]; console.log(theme.path`• packages/%s/package.json`, packageName); } - console.log(theme.path`• packages/shared/ReactVersion.js`); + const status = await execRead( + 'git diff packages/shared/ReactVersion.js', + {cwd} + ); + if (status) { + console.log(theme.path`• packages/shared/ReactVersion.js`); + } console.log(); if (environment === 'ci') { diff --git a/scripts/release/publish-commands/update-stable-version-numbers.js b/scripts/release/publish-commands/update-stable-version-numbers.js index b94caa43c1..0a56d8e39c 100644 --- a/scripts/release/publish-commands/update-stable-version-numbers.js +++ b/scripts/release/publish-commands/update-stable-version-numbers.js @@ -6,7 +6,7 @@ const {readFileSync, writeFileSync} = require('fs'); const {readJson, writeJson} = require('fs-extra'); const {join} = require('path'); -const run = async ({cwd, packages, tags}) => { +const run = async ({cwd, packages, skipPackages, tags}) => { if (!tags.includes('latest')) { // Don't update version numbers for alphas. return; @@ -35,15 +35,18 @@ const run = async ({cwd, packages, tags}) => { } // Update the shared React version source file. - const sourceReactVersionPath = join(cwd, 'packages/shared/ReactVersion.js'); - const {version} = await readJson( - join(nodeModulesPath, 'react', 'package.json') - ); - const sourceReactVersion = readFileSync( - sourceReactVersionPath, - 'utf8' - ).replace(/module\.exports = '[^']+';/, `module.exports = '${version}';`); - writeFileSync(sourceReactVersionPath, sourceReactVersion); + // (Unless this release does not include an update to React) + if (!skipPackages.includes('react')) { + const sourceReactVersionPath = join(cwd, 'packages/shared/ReactVersion.js'); + const {version} = await readJson( + join(nodeModulesPath, 'react', 'package.json') + ); + const sourceReactVersion = readFileSync( + sourceReactVersionPath, + 'utf8' + ).replace(/module\.exports = '[^']+';/, `module.exports = '${version}';`); + writeFileSync(sourceReactVersionPath, sourceReactVersion); + } }; module.exports = run; diff --git a/scripts/release/publish-commands/validate-skip-packages.js b/scripts/release/publish-commands/validate-skip-packages.js new file mode 100644 index 0000000000..965c251bc7 --- /dev/null +++ b/scripts/release/publish-commands/validate-skip-packages.js @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +'use strict'; + +const {readJson} = require('fs-extra'); +const {join} = require('path'); +const theme = require('../theme'); +const {execRead} = require('../utils'); + +const readPackageJSON = async (cwd, name) => { + const packageJSONPath = join( + cwd, + 'build', + 'node_modules', + name, + 'package.json' + ); + return await readJson(packageJSONPath); +}; + +const run = async ({cwd, packages, skipPackages}) => { + if (skipPackages.length === 0) { + return; + } + + const validateDependencies = async (name, dependencies) => { + if (!dependencies) { + return; + } + + for (let dependency in dependencies) { + // Do we depend on a package thas has been skipped? + if (skipPackages.includes(dependency)) { + const version = dependencies[dependency]; + // Do we depend on a version of the package than has not been published to NPM? + const info = await execRead(`npm view ${dependency}@${version}`); + if (!info) { + console.log( + theme`{error Package} {package ${name}} {error depends on an unpublished skipped package}`, + theme`{package ${dependency}}@{version ${version}}` + ); + process.exit(1); + } + } + } + }; + + // Make sure none of the other packages depend on a skipped package, + // unless the dependency has already been published to NPM. + for (let i = 0; i < packages.length; i++) { + const name = packages[i]; + const {dependencies, peerDependencies} = await readPackageJSON(cwd, name); + + validateDependencies(name, dependencies); + validateDependencies(name, peerDependencies); + } +}; + +module.exports = run; diff --git a/scripts/release/publish-commands/validate-tags.js b/scripts/release/publish-commands/validate-tags.js index 0021511da3..70d9ff6521 100644 --- a/scripts/release/publish-commands/validate-tags.js +++ b/scripts/release/publish-commands/validate-tags.js @@ -8,11 +8,13 @@ const theme = require('../theme'); const run = async ({cwd, packages, tags}) => { // Prevent a canary release from ever being published as @latest + // All canaries share a version number, so it's okay to check any of them. + const arbitraryPackageName = packages[0]; const packageJSONPath = join( cwd, 'build', 'node_modules', - 'react', + arbitraryPackageName, 'package.json' ); const {version} = await readJson(packageJSONPath); @@ -23,6 +25,13 @@ const run = async ({cwd, packages, tags}) => { ); process.exit(1); } + } else { + if (tags.includes('canary')) { + console.log( + theme`{error Stable release} {version ${version}} {error cannot be tagged as} {tag canary}` + ); + process.exit(1); + } } }; diff --git a/scripts/release/publish.js b/scripts/release/publish.js index 4923204a13..0e7224a3e2 100755 --- a/scripts/release/publish.js +++ b/scripts/release/publish.js @@ -4,8 +4,10 @@ const {join} = require('path'); const {getPublicPackages, handleError} = require('./utils'); +const theme = require('./theme'); const checkNPMPermissions = require('./publish-commands/check-npm-permissions'); +const confirmSkippedPackages = require('./publish-commands/confirm-skipped-packages'); const confirmVersionAndTags = require('./publish-commands/confirm-version-and-tags'); const downloadErrorCodesFromCI = require('./publish-commands/download-error-codes-from-ci'); const parseParams = require('./publish-commands/parse-params'); @@ -14,6 +16,7 @@ const promptForOTP = require('./publish-commands/prompt-for-otp'); const publishToNPM = require('./publish-commands/publish-to-npm'); const updateStableVersionNumbers = require('./publish-commands/update-stable-version-numbers'); const validateTags = require('./publish-commands/validate-tags'); +const validateSkipPackages = require('./publish-commands/validate-skip-packages'); const run = async () => { try { @@ -21,8 +24,24 @@ const run = async () => { params.cwd = join(__dirname, '..', '..'); params.packages = await getPublicPackages(); + // Pre-filter any skipped packages to simplify the following commands. + // As part of doing this we can also validate that none of the skipped packages were misspelled. + params.skipPackages.forEach(packageName => { + const index = params.packages.indexOf(packageName); + if (index < 0) { + console.log( + theme`Invalid skip package {package ${packageName}} specified.` + ); + process.exit(1); + } else { + params.packages.splice(index, 1); + } + }); + await validateTags(params); + await confirmSkippedPackages(params); await confirmVersionAndTags(params); + await validateSkipPackages(params); await checkNPMPermissions(params); const otp = await promptForOTP(params); await publishToNPM(params, otp); diff --git a/scripts/release/utils.js b/scripts/release/utils.js index 139695facb..f616ef792e 100644 --- a/scripts/release/utils.js +++ b/scripts/release/utils.js @@ -157,6 +157,17 @@ const printDiff = (path, beforeContents, afterContents) => { return patch; }; +// Convert an array param (expected format "--foo bar baz") +// to also accept comma input (e.g. "--foo bar,baz") +const splitCommaParams = array => { + for (let i = array.length - 1; i >= 0; i--) { + const param = array[i]; + if (param.includes(',')) { + array.splice(i, 1, ...param.split(',')); + } + } +}; + // This method is used by both local Node release scripts and Circle CI bash scripts. // It updates version numbers in package JSONs (both the version field and dependencies), // As well as the embedded renderer version in "packages/shared/ReactVersion". @@ -228,6 +239,7 @@ module.exports = { handleError, logPromise, printDiff, + splitCommaParams, theme, updateVersionsForCanary, };