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
};