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

Release script supports publishing a subset of packages (#16338)
This commit is contained in:
Brian Vaughn 2019-08-09 13:12:00 -07:00 committed by GitHub
parent 0bd0c5269f
commit 5b007573ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 219 additions and 37 deletions

View File

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

View File

@ -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 {

View File

@ -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,

View File

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

View File

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

View File

@ -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(

View File

@ -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.

View File

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

View File

@ -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') {

View File

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

View File

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

View File

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

View File

@ -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);

View File

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