@samitouri / QOS-React-2 / commits / 5b007573ac

Release script supports publishing a subset of packages (#16338)

Release script supports publishing a subset of packages (#16338)

Brian Vaughn committed Aug 9, 2019 at 13:12 UTC 5b007573ac7eb39660273302d5a4679e3d2c44e3
14 files changed +220 -38
scripts/release/create-canary-commands/build-artifacts.js
+1 -1
@@ -21,5 +21,5 @@ const run = async ({cwd, dry, tempDirectory}) => {
21 };
22
23 module.exports = async params => {
24 - return logPromise(run(params), 'Building artifacts', 420000);
24 + return logPromise(run(params), 'Building artifacts', 600000);
25 };
scripts/release/prepare-stable-commands/confirm-stable-version-numbers.js
+22 -9
@@ -5,8 +5,9 @@
5 const prompt = require('prompt-promise');
6 const semver = require('semver');
7 const theme = require('../theme');
8 +const {confirm} = require('../utils');
9
9 -const run = async (params, versionsMap) => {
10 +const run = async ({skipPackages}, versionsMap) => {
11 const groupedVersionsMap = new Map();
12
13 // Group packages with the same source versions.
@@ -26,14 +27,26 @@ const run = async (params, versionsMap) => {
27 for (let i = 0; i < entries.length; i++) {
28 const [bestGuessVersion, packages] = entries[i];
29 const packageNames = packages.map(name => theme.package(name)).join(', ');
29 - const defaultVersion = bestGuessVersion
30 - ? theme.version(` (default ${bestGuessVersion})`)
31 - : '';
32 - const version =
33 - (await prompt(
34 - theme`{spinnerSuccess ✓} Version for ${packageNames}${defaultVersion}: `
35 - )) || bestGuessVersion;
36 - prompt.done();
30 +
31 + let version = bestGuessVersion;
32 + if (
33 + skipPackages.some(skipPackageName =>
34 + packageNames.includes(skipPackageName)
35 + )
36 + ) {
37 + await confirm(
38 + theme`{spinnerSuccess ✓} Version for ${packageNames} will remain {version ${bestGuessVersion}}`
39 + );
40 + } else {
41 + const defaultVersion = bestGuessVersion
42 + ? theme.version(` (default ${bestGuessVersion})`)
43 + : '';
44 + version =
45 + (await prompt(
46 + theme`{spinnerSuccess ✓} Version for ${packageNames}${defaultVersion}: `
47 + )) || bestGuessVersion;
48 + prompt.done();
49 + }
50
51 // Verify a valid version has been supplied.
52 try {
scripts/release/prepare-stable-commands/guess-stable-version-numbers.js
+15 -10
@@ -5,7 +5,7 @@
5 const semver = require('semver');
6 const {execRead, logPromise} = require('../utils');
7
8 -const run = async ({cwd, packages}, versionsMap) => {
8 +const run = async ({cwd, packages, skipPackages}, versionsMap) => {
9 const branch = await execRead('git branch | grep \\* | cut -d " " -f2', {
10 cwd,
11 });
@@ -17,16 +17,21 @@ const run = async ({cwd, packages}, versionsMap) => {
17 // In case local package JSONs are outdated,
18 // guess the next version based on the latest NPM release.
19 const version = await execRead(`npm show ${packageName} version`);
20 - const {major, minor, patch} = semver(version);
21 -
22 - // Guess the next version by incrementing patch.
23 - // The script will confirm this later.
24 - // By default, new releases from masters should increment the minor version number,
25 - // and patch releases should be done from branches.
26 - if (branch === 'master') {
27 - versionsMap.set(packageName, `${major}.${minor + 1}.0`);
20 +
21 + if (skipPackages.includes(packageName)) {
22 + versionsMap.set(packageName, version);
23 } else {
29 - versionsMap.set(packageName, `${major}.${minor}.${patch + 1}`);
24 + const {major, minor, patch} = semver(version);
25 +
26 + // Guess the next version by incrementing patch.
27 + // The script will confirm this later.
28 + // By default, new releases from masters should increment the minor version number,
29 + // and patch releases should be done from branches.
30 + if (branch === 'master') {
31 + versionsMap.set(packageName, `${major}.${minor + 1}.0`);
32 + } else {
33 + versionsMap.set(packageName, `${major}.${minor}.${patch + 1}`);
34 + }
35 }
36 } catch (error) {
37 // If the package has not yet been published,
scripts/release/prepare-stable-commands/parse-params.js
+10
@@ -3,6 +3,7 @@
3 'use strict';
4
5 const commandLineArgs = require('command-line-args');
6 +const {splitCommaParams} = require('../utils');
7
8 const paramDefinitions = [
9 {
@@ -12,6 +13,13 @@ const paramDefinitions = [
13 'Skip NPM and use the build already present in "build/node_modules".',
14 defaultValue: false,
15 },
16 + {
17 + name: 'skipPackages',
18 + type: String,
19 + multiple: true,
20 + description: 'Packages to exclude from publishing',
21 + defaultValue: [],
22 + },
23 {
24 name: 'skipTests',
25 type: Boolean,
@@ -28,5 +36,7 @@ const paramDefinitions = [
36 module.exports = () => {
37 const params = commandLineArgs(paramDefinitions);
38
39 + splitCommaParams(params.skipPackages);
40 +
41 return params;
42 };
scripts/release/publish-commands/confirm-skipped-packages.js new
+31
@@ -0,0 +1,31 @@
1 +#!/usr/bin/env node
2 +
3 +'use strict';
4 +
5 +const clear = require('clear');
6 +const {confirm} = require('../utils');
7 +const theme = require('../theme');
8 +
9 +const run = async ({cwd, packages, skipPackages, tags}) => {
10 + if (skipPackages.length === 0) {
11 + return;
12 + }
13 +
14 + clear();
15 +
16 + console.log(
17 + theme`{spinnerSuccess ✓} The following packages will not be published as part of this release`
18 + );
19 +
20 + skipPackages.forEach(packageName => {
21 + console.log(theme`• {package ${packageName}}`);
22 + });
23 +
24 + await confirm('Do you want to proceed?');
25 +
26 + clear();
27 +};
28 +
29 +// Run this directly because it's fast,
30 +// and logPromise would interfere with console prompting.
31 +module.exports = run;
scripts/release/publish-commands/confirm-version-and-tags.js
-1
@@ -23,7 +23,6 @@ const run = async ({cwd, packages, tags}) => {
23 );
24 }
25
26 - // Cache all package JSONs for easy lookup below.
26 for (let i = 0; i < packages.length; i++) {
27 const packageName = packages[i];
28 const packageJSONPath = join(
scripts/release/publish-commands/download-error-codes-from-ci.js
+4 -2
@@ -4,10 +4,11 @@
4
5 const {exec} = require('child-process-promise');
6 const {readJsonSync} = require('fs-extra');
7 +const {join} = require('path');
8 const {getArtifactsList, logPromise} = require('../utils');
9 const theme = require('../theme');
10
10 -const run = async ({cwd, tags}) => {
11 +const run = async ({cwd, packages, tags}) => {
12 if (!tags.includes('latest')) {
13 // Don't update error-codes for alphas.
14 return;
@@ -15,8 +16,9 @@ const run = async ({cwd, tags}) => {
16
17 // All packages are built from a single source revision,
18 // so it is safe to read build info from any one of them.
19 + const arbitraryPackageName = packages[0];
20 const {buildNumber, environment} = readJsonSync(
19 - `${cwd}/build/node_modules/react/build-info.json`
21 + join(cwd, 'build', 'node_modules', arbitraryPackageName, 'build-info.json')
22 );
23
24 // If this release was created on Circle CI, grab the updated error codes from there.
scripts/release/publish-commands/parse-params.js
+14 -1
@@ -4,6 +4,7 @@
4
5 const commandLineArgs = require('command-line-args');
6 const commandLineUsage = require('command-line-usage');
7 +const {splitCommaParams} = require('../utils');
8
9 const paramDefinitions = [
10 {
@@ -18,12 +19,21 @@ const paramDefinitions = [
19 multiple: true,
20 description: 'NPM tags to point to the new release.',
21 },
22 + {
23 + name: 'skipPackages',
24 + type: String,
25 + multiple: true,
26 + description: 'Packages to exclude from publishing',
27 + defaultValue: [],
28 + },
29 ];
30
31 module.exports = () => {
32 const params = commandLineArgs(paramDefinitions);
33
26 - if (!params.tags || params.tags.length === 0) {
34 + const {skipPackages, tags} = params;
35 +
36 + if (!tags || tags.length === 0) {
37 const usage = commandLineUsage([
38 {
39 content:
@@ -51,5 +61,8 @@ module.exports = () => {
61 process.exit(1);
62 }
63
64 + splitCommaParams(skipPackages);
65 + splitCommaParams(tags);
66 +
67 return params;
68 };
scripts/release/publish-commands/print-follow-up-instructions.js
+10 -3
@@ -11,9 +11,10 @@ const {execRead} = require('../utils');
11
12 const run = async ({cwd, packages, tags}) => {
13 // All packages are built from a single source revision,
14 - // so it is safe to read the commit number from any one of them.
14 + // so it is safe to read build info from any one of them.
15 + const arbitraryPackageName = packages[0];
16 const {commit, environment} = readJsonSync(
16 - `${cwd}/build/node_modules/react/build-info.json`
17 + join(cwd, 'build', 'node_modules', arbitraryPackageName, 'build-info.json')
18 );
19
20 // Tags are named after the react version.
@@ -50,7 +51,13 @@ const run = async ({cwd, packages, tags}) => {
51 const packageName = packages[i];
52 console.log(theme.path`• packages/%s/package.json`, packageName);
53 }
53 - console.log(theme.path`• packages/shared/ReactVersion.js`);
54 + const status = await execRead(
55 + 'git diff packages/shared/ReactVersion.js',
56 + {cwd}
57 + );
58 + if (status) {
59 + console.log(theme.path`• packages/shared/ReactVersion.js`);
60 + }
61
62 console.log();
63 if (environment === 'ci') {
scripts/release/publish-commands/update-stable-version-numbers.js
+13 -10
@@ -6,7 +6,7 @@ const {readFileSync, writeFileSync} = require('fs');
6 const {readJson, writeJson} = require('fs-extra');
7 const {join} = require('path');
8
9 -const run = async ({cwd, packages, tags}) => {
9 +const run = async ({cwd, packages, skipPackages, tags}) => {
10 if (!tags.includes('latest')) {
11 // Don't update version numbers for alphas.
12 return;
@@ -35,15 +35,18 @@ const run = async ({cwd, packages, tags}) => {
35 }
36
37 // Update the shared React version source file.
38 - const sourceReactVersionPath = join(cwd, 'packages/shared/ReactVersion.js');
39 - const {version} = await readJson(
40 - join(nodeModulesPath, 'react', 'package.json')
41 - );
42 - const sourceReactVersion = readFileSync(
43 - sourceReactVersionPath,
44 - 'utf8'
45 - ).replace(/module\.exports = '[^']+';/, `module.exports = '${version}';`);
46 - writeFileSync(sourceReactVersionPath, sourceReactVersion);
38 + // (Unless this release does not include an update to React)
39 + if (!skipPackages.includes('react')) {
40 + const sourceReactVersionPath = join(cwd, 'packages/shared/ReactVersion.js');
41 + const {version} = await readJson(
42 + join(nodeModulesPath, 'react', 'package.json')
43 + );
44 + const sourceReactVersion = readFileSync(
45 + sourceReactVersionPath,
46 + 'utf8'
47 + ).replace(/module\.exports = '[^']+';/, `module.exports = '${version}';`);
48 + writeFileSync(sourceReactVersionPath, sourceReactVersion);
49 + }
50 };
51
52 module.exports = run;
scripts/release/publish-commands/validate-skip-packages.js new
+59
@@ -0,0 +1,59 @@
1 +#!/usr/bin/env node
2 +
3 +'use strict';
4 +
5 +const {readJson} = require('fs-extra');
6 +const {join} = require('path');
7 +const theme = require('../theme');
8 +const {execRead} = require('../utils');
9 +
10 +const readPackageJSON = async (cwd, name) => {
11 + const packageJSONPath = join(
12 + cwd,
13 + 'build',
14 + 'node_modules',
15 + name,
16 + 'package.json'
17 + );
18 + return await readJson(packageJSONPath);
19 +};
20 +
21 +const run = async ({cwd, packages, skipPackages}) => {
22 + if (skipPackages.length === 0) {
23 + return;
24 + }
25 +
26 + const validateDependencies = async (name, dependencies) => {
27 + if (!dependencies) {
28 + return;
29 + }
30 +
31 + for (let dependency in dependencies) {
32 + // Do we depend on a package thas has been skipped?
33 + if (skipPackages.includes(dependency)) {
34 + const version = dependencies[dependency];
35 + // Do we depend on a version of the package than has not been published to NPM?
36 + const info = await execRead(`npm view ${dependency}@${version}`);
37 + if (!info) {
38 + console.log(
39 + theme`{error Package} {package ${name}} {error depends on an unpublished skipped package}`,
40 + theme`{package ${dependency}}@{version ${version}}`
41 + );
42 + process.exit(1);
43 + }
44 + }
45 + }
46 + };
47 +
48 + // Make sure none of the other packages depend on a skipped package,
49 + // unless the dependency has already been published to NPM.
50 + for (let i = 0; i < packages.length; i++) {
51 + const name = packages[i];
52 + const {dependencies, peerDependencies} = await readPackageJSON(cwd, name);
53 +
54 + validateDependencies(name, dependencies);
55 + validateDependencies(name, peerDependencies);
56 + }
57 +};
58 +
59 +module.exports = run;
scripts/release/publish-commands/validate-tags.js
+10 -1
@@ -8,11 +8,13 @@ const theme = require('../theme');
8
9 const run = async ({cwd, packages, tags}) => {
10 // Prevent a canary release from ever being published as @latest
11 + // All canaries share a version number, so it's okay to check any of them.
12 + const arbitraryPackageName = packages[0];
13 const packageJSONPath = join(
14 cwd,
15 'build',
16 'node_modules',
15 - 'react',
17 + arbitraryPackageName,
18 'package.json'
19 );
20 const {version} = await readJson(packageJSONPath);
@@ -23,6 +25,13 @@ const run = async ({cwd, packages, tags}) => {
25 );
26 process.exit(1);
27 }
28 + } else {
29 + if (tags.includes('canary')) {
30 + console.log(
31 + theme`{error Stable release} {version ${version}} {error cannot be tagged as} {tag canary}`
32 + );
33 + process.exit(1);
34 + }
35 }
36 };
37
scripts/release/publish.js
+19
@@ -4,8 +4,10 @@
4
5 const {join} = require('path');
6 const {getPublicPackages, handleError} = require('./utils');
7 +const theme = require('./theme');
8
9 const checkNPMPermissions = require('./publish-commands/check-npm-permissions');
10 +const confirmSkippedPackages = require('./publish-commands/confirm-skipped-packages');
11 const confirmVersionAndTags = require('./publish-commands/confirm-version-and-tags');
12 const downloadErrorCodesFromCI = require('./publish-commands/download-error-codes-from-ci');
13 const parseParams = require('./publish-commands/parse-params');
@@ -14,6 +16,7 @@ const promptForOTP = require('./publish-commands/prompt-for-otp');
16 const publishToNPM = require('./publish-commands/publish-to-npm');
17 const updateStableVersionNumbers = require('./publish-commands/update-stable-version-numbers');
18 const validateTags = require('./publish-commands/validate-tags');
19 +const validateSkipPackages = require('./publish-commands/validate-skip-packages');
20
21 const run = async () => {
22 try {
@@ -21,8 +24,24 @@ const run = async () => {
24 params.cwd = join(__dirname, '..', '..');
25 params.packages = await getPublicPackages();
26
27 + // Pre-filter any skipped packages to simplify the following commands.
28 + // As part of doing this we can also validate that none of the skipped packages were misspelled.
29 + params.skipPackages.forEach(packageName => {
30 + const index = params.packages.indexOf(packageName);
31 + if (index < 0) {
32 + console.log(
33 + theme`Invalid skip package {package ${packageName}} specified.`
34 + );
35 + process.exit(1);
36 + } else {
37 + params.packages.splice(index, 1);
38 + }
39 + });
40 +
41 await validateTags(params);
42 + await confirmSkippedPackages(params);
43 await confirmVersionAndTags(params);
44 + await validateSkipPackages(params);
45 await checkNPMPermissions(params);
46 const otp = await promptForOTP(params);
47 await publishToNPM(params, otp);
scripts/release/utils.js
+12
@@ -157,6 +157,17 @@ const printDiff = (path, beforeContents, afterContents) => {
157 return patch;
158 };
159
160 +// Convert an array param (expected format "--foo bar baz")
161 +// to also accept comma input (e.g. "--foo bar,baz")
162 +const splitCommaParams = array => {
163 + for (let i = array.length - 1; i >= 0; i--) {
164 + const param = array[i];
165 + if (param.includes(',')) {
166 + array.splice(i, 1, ...param.split(','));
167 + }
168 + }
169 +};
170 +
171 // This method is used by both local Node release scripts and Circle CI bash scripts.
172 // It updates version numbers in package JSONs (both the version field and dependencies),
173 // As well as the embedded renderer version in "packages/shared/ReactVersion".
@@ -228,6 +239,7 @@ module.exports = {
239 handleError,
240 logPromise,
241 printDiff,
242 + splitCommaParams,
243 theme,
244 updateVersionsForCanary,
245 };