DevTools release script updates: (#22170)

This commit is contained in:
Brian Vaughn
2021-08-24 18:59:34 -04:00
committed by GitHub
parent 582858083e
commit b9964684bd
8 changed files with 797 additions and 554 deletions

View File

@@ -1 +1,2 @@
.progress-estimator
.build-metadata
.progress-estimator

View File

@@ -0,0 +1,30 @@
# Releasing DevTools
To release DevTools, do the following steps (in order):
1. [Prepare a release](#prepare-a-release)
2. [Build and test a release](#build-and-test-a-release)
3. [Publish a release](#publish-a-release)
Each of the scripts can be run with a `--dry` flag to test without committing or publishing any changes.
### Prepare a release
To increment version numbers and update the [CHANGELOG](https://github.com/facebook/react/blob/main/packages/react-devtools/CHANGELOG.md), run the `prepare-release` script:
```sh
./prepare-release.js
```
You'll need to follow the instructions at the end of the script to push the committed changes to the main fork on GitHub.
### Build and test a release
To build and test a release, run the `build-and-test` script:
```sh
./build-and-test.js
```
### Publish a release
To publish a release to NPM, run the `publish-release` script:
```sh
./publish-release.js
```
You'll need to follow the instructions at the end of the script to upload the extension to Chrome, Edge, and Firefox stores.

View File

@@ -0,0 +1,221 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {exec} = require('child-process-promise');
const inquirer = require('inquirer');
const {homedir} = require('os');
const {join, relative} = require('path');
const {DRY_RUN, ROOT_PATH} = require('./configuration');
const {
clear,
confirm,
confirmContinue,
execRead,
logger,
saveBuildMetadata,
} = require('./utils');
// This is the primary control function for this script.
async function main() {
clear();
await confirm('Have you run the prepare-release script?', () => {
const prepareReleaseScriptPath = join(__dirname, 'prepare-release.js');
const pathToPrint = relative(process.cwd(), prepareReleaseScriptPath);
console.log('Begin by running the prepare-release script:');
console.log(chalk.bold.green(' ' + pathToPrint));
});
const archivePath = await archiveGitRevision();
const buildID = await downloadLatestReactBuild();
await buildAndTestInlinePackage();
await buildAndTestStandalonePackage();
await buildAndTestExtensions();
saveBuildMetadata({archivePath, buildID});
printFinalInstructions();
}
async function archiveGitRevision() {
const desktopPath = join(homedir(), 'Desktop');
const archivePath = join(desktopPath, 'DevTools.tgz');
console.log(`Creating git archive at ${chalk.dim(archivePath)}`);
console.log('');
if (!DRY_RUN) {
await exec(`git archive main | gzip > ${archivePath}`, {cwd: ROOT_PATH});
}
return archivePath;
}
async function buildAndTestExtensions() {
const extensionsPackagePath = join(
ROOT_PATH,
'packages',
'react-devtools-extensions'
);
const buildExtensionsPromise = exec('yarn build', {
cwd: extensionsPackagePath,
});
await logger(
buildExtensionsPromise,
`Building browser extensions ${chalk.dim('(this may take a minute)')}`,
{
estimate: 60000,
}
);
console.log('');
console.log(`Extensions have been build for Chrome, Edge, and Firefox.`);
console.log('');
console.log('Smoke test each extension before continuing:');
console.log(` ${chalk.bold.green('cd ' + extensionsPackagePath)}`);
console.log('');
console.log(` ${chalk.dim('# Test Chrome extension')}`);
console.log(` ${chalk.bold.green('yarn test:chrome')}`);
console.log('');
console.log(` ${chalk.dim('# Test Edge extension')}`);
console.log(` ${chalk.bold.green('yarn test:edge')}`);
console.log('');
console.log(` ${chalk.dim('# Firefox Chrome extension')}`);
console.log(` ${chalk.bold.green('yarn test:firefox')}`);
await confirmContinue();
}
async function buildAndTestStandalonePackage() {
const corePackagePath = join(ROOT_PATH, 'packages', 'react-devtools-core');
const buildCorePromise = exec('yarn build', {cwd: corePackagePath});
await logger(
buildCorePromise,
`Building ${chalk.bold('react-devtools-core')} package.`,
{
estimate: 25000,
}
);
const standalonePackagePath = join(ROOT_PATH, 'packages', 'react-devtools');
const safariFixturePath = join(
ROOT_PATH,
'fixtures',
'devtools',
'standalone',
'index.html'
);
console.log('');
console.log(
`Test the ${chalk.bold('react-devtools-core')} target before continuing:`
);
console.log(` ${chalk.bold.green('cd ' + standalonePackagePath)}`);
console.log(` ${chalk.bold.green('yarn start')}`);
console.log('');
console.log(
'The following fixture can be useful for testing Safari integration:'
);
console.log(` ${chalk.dim(safariFixturePath)}`);
await confirmContinue();
}
async function buildAndTestInlinePackage() {
const inlinePackagePath = join(
ROOT_PATH,
'packages',
'react-devtools-inline'
);
const buildPromise = exec('yarn build', {cwd: inlinePackagePath});
await logger(
buildPromise,
`Building ${chalk.bold('react-devtools-inline')} package.`,
{
estimate: 10000,
}
);
const shellPackagePath = join(ROOT_PATH, 'packages', 'react-devtools-shell');
console.log('');
console.log(`Built ${chalk.bold('react-devtools-inline')} target.`);
console.log('');
console.log('Test this build before continuing:');
console.log(` ${chalk.bold.green('cd ' + shellPackagePath)}`);
console.log(` ${chalk.bold.green('yarn start')}`);
await confirmContinue();
}
async function downloadLatestReactBuild() {
const releaseScriptPath = join(ROOT_PATH, 'scripts', 'release');
const installPromise = exec('yarn install', {cwd: releaseScriptPath});
await logger(
installPromise,
`Installing release script dependencies. ${chalk.dim(
'(this may take a minute if CI is still running)'
)}`,
{
estimate: 5000,
}
);
console.log('');
const {commit} = await inquirer.prompt([
{
type: 'input',
name: 'commit',
message: 'Which React version (commit) should be used?',
default: 'main',
},
]);
console.log('');
const downloadScriptPath = join(
releaseScriptPath,
'download-experimental-build.js'
);
const downloadPromise = execRead(
`"${downloadScriptPath}" --commit=${commit}`
);
const output = await logger(
downloadPromise,
'Downloading React artifacts from CI.',
{estimate: 15000}
);
const match = output.match('--build=([0-9]+)');
if (match.length === 0) {
console.error(chalk.red(`No build ID found in "${output}"`));
process.exit(1);
}
const buildID = match[1];
console.log('');
console.log(`Downloaded artiacts for CI build ${chalk.bold(buildID)}.`);
return buildID;
}
function printFinalInstructions() {
const publishReleaseScriptPath = join(__dirname, 'publish-release.js');
const pathToPrint = relative(process.cwd(), publishReleaseScriptPath);
console.log('');
console.log('Continue by running the publish-release script:');
console.log(chalk.bold.green(' ' + pathToPrint));
}
main();

View File

@@ -0,0 +1,46 @@
'use strict';
const {join} = require('path');
const PACKAGE_PATHS = [
'packages/react-devtools/package.json',
'packages/react-devtools-core/package.json',
'packages/react-devtools-inline/package.json',
'packages/react-devtools-scheduling-profiler/package.json',
];
const MANIFEST_PATHS = [
'packages/react-devtools-extensions/chrome/manifest.json',
'packages/react-devtools-extensions/edge/manifest.json',
'packages/react-devtools-extensions/firefox/manifest.json',
];
const NPM_PACKAGES = [
'react-devtools',
'react-devtools-core',
'react-devtools-inline',
];
const CHANGELOG_PATH = 'packages/react-devtools/CHANGELOG.md';
const PULL_REQUEST_BASE_URL = 'https://github.com/facebook/react/pull/';
const RELEASE_SCRIPT_TOKEN = '<!-- RELEASE_SCRIPT_TOKEN -->';
const ROOT_PATH = join(__dirname, '..', '..');
const DRY_RUN = process.argv.includes('--dry');
const BUILD_METADATA_TEMP_DIRECTORY = join(__dirname, '.build-metadata');
module.exports = {
BUILD_METADATA_TEMP_DIRECTORY,
CHANGELOG_PATH,
DRY_RUN,
MANIFEST_PATHS,
NPM_PACKAGES,
PACKAGE_PATHS,
PULL_REQUEST_BASE_URL,
RELEASE_SCRIPT_TOKEN,
ROOT_PATH,
};

View File

@@ -0,0 +1,254 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {exec} = require('child-process-promise');
const {readFileSync, writeFileSync} = require('fs');
const {readJsonSync, writeJsonSync} = require('fs-extra');
const inquirer = require('inquirer');
const {join, relative} = require('path');
const semver = require('semver');
const {
CHANGELOG_PATH,
DRY_RUN,
MANIFEST_PATHS,
PACKAGE_PATHS,
PULL_REQUEST_BASE_URL,
RELEASE_SCRIPT_TOKEN,
ROOT_PATH,
} = require('./configuration');
const {
checkNPMPermissions,
clear,
confirmContinue,
execRead,
} = require('./utils');
// This is the primary control function for this script.
async function main() {
clear();
await checkNPMPermissions();
const releaseType = await getReleaseType();
const path = join(ROOT_PATH, PACKAGE_PATHS[0]);
const previousVersion = readJsonSync(path).version;
const {major, minor, patch} = semver(previousVersion);
const nextVersion =
releaseType === 'minor'
? `${major}.${minor + 1}.${patch}`
: `${major}.${minor}.${patch + 1}`;
updatePackageVersions(previousVersion, nextVersion);
updateManifestVersions(previousVersion, nextVersion);
console.log('');
console.log(
`Packages and manifests have been updated from version ${chalk.bold(
previousVersion
)} to ${chalk.bold(nextVersion)}`
);
console.log('');
const sha = await getPreviousCommitSha();
const commitLog = await getCommitLog(sha);
updateChangelog(nextVersion, commitLog);
await reviewChangelogPrompt();
await commitPendingChanges(previousVersion, nextVersion);
printFinalInstructions();
}
async function commitPendingChanges(previousVersion, nextVersion) {
console.log('');
console.log('Committing revision and changelog.');
console.log(chalk.dim(' git add .'));
console.log(
chalk.dim(
` git commit -m "React DevTools ${previousVersion} -> ${nextVersion}"`
)
);
if (!DRY_RUN) {
await exec(`
git add .
git commit -m "React DevTools ${previousVersion} -> ${nextVersion}"
`);
}
console.log('');
console.log(`Please push this commit before continuing:`);
console.log(` ${chalk.bold.green('git push')}`);
await confirmContinue();
}
async function getCommitLog(sha) {
let formattedLog = '';
const rawLog = await execRead(`
git log --topo-order --pretty=format:'%s' ${sha}...HEAD -- packages/react-devtools*
`);
rawLog.split('\n').forEach(line => {
line = line.replace('[DevTools] ', '');
const match = line.match(/(.+) \(#([0-9]+)\)/);
if (match !== null) {
const title = match[1];
const pr = match[2];
formattedLog += `\n* ${title} ([USERNAME](https://github.com/USERNAME) in [#${pr}](${PULL_REQUEST_BASE_URL}${pr}))`;
} else {
formattedLog += `\n* ${line}`;
}
});
return formattedLog;
}
async function getPreviousCommitSha() {
const choices = [];
const lines = await execRead(`
git log --max-count=5 --topo-order --pretty=format:'%H:::%s:::%as' HEAD -- ${PACKAGE_PATHS[0]}
`);
lines.split('\n').forEach((line, index) => {
const [hash, message, date] = line.split(':::');
choices.push({
name: `${chalk.bold(hash)} ${chalk.dim(date)} ${message}`,
value: hash,
short: date,
});
});
const {sha} = await inquirer.prompt([
{
type: 'list',
name: 'sha',
message: 'Which of the commits above marks the last DevTools release?',
choices,
default: choices[0].value,
},
]);
return sha;
}
async function getReleaseType() {
const {releaseType} = await inquirer.prompt([
{
type: 'list',
name: 'releaseType',
message: 'Which type of release is this?',
choices: [
{
name: 'Minor (new user facing functionality)',
value: 'minor',
short: 'Minor',
},
{name: 'Patch (bug fixes only)', value: 'patch', short: 'Patch'},
],
default: 'patch',
},
]);
return releaseType;
}
function printFinalInstructions() {
const buildAndTestcriptPath = join(__dirname, 'build-and-test.js');
const pathToPrint = relative(process.cwd(), buildAndTestcriptPath);
console.log('');
console.log('Continue by running the build-and-test script:');
console.log(chalk.bold.green(' ' + pathToPrint));
}
async function reviewChangelogPrompt() {
console.log('');
console.log(
'The changelog has been updated with commits since the previous release:'
);
console.log(` ${chalk.bold(CHANGELOG_PATH)}`);
console.log('');
console.log('Please review the new changelog text for the following:');
console.log(' 1. Organize the list into Features vs Bugfixes');
console.log(' 1. Filter out any non-user-visible changes (e.g. typo fixes)');
console.log(' 1. Combine related PRs into a single bullet list.');
console.log(
' 1. Replacing the "USERNAME" placeholder text with the GitHub username(s)'
);
console.log('');
console.log(` ${chalk.bold.green(`open ${CHANGELOG_PATH}`)}`);
await confirmContinue();
}
function updateChangelog(nextVersion, commitLog) {
const path = join(ROOT_PATH, CHANGELOG_PATH);
const oldChangelog = readFileSync(path, 'utf8');
const [beginning, end] = oldChangelog.split(RELEASE_SCRIPT_TOKEN);
const dateString = new Date().toLocaleDateString('en-us', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const header = `## ${nextVersion} (${dateString})`;
const newChangelog = `${beginning}${RELEASE_SCRIPT_TOKEN}\n\n${header}\n${commitLog}${end}`;
console.log(chalk.dim(' Updating changelog: ' + CHANGELOG_PATH));
if (!DRY_RUN) {
writeFileSync(path, newChangelog);
}
}
function updateManifestVersions(previousVersion, nextVersion) {
MANIFEST_PATHS.forEach(partialPath => {
const path = join(ROOT_PATH, partialPath);
const json = readJsonSync(path);
json.version = nextVersion;
if (json.hasOwnProperty('version_name')) {
json.version_name = nextVersion;
}
console.log(chalk.dim(' Updating manifest JSON: ' + partialPath));
if (!DRY_RUN) {
writeJsonSync(path, json, {spaces: 2});
}
});
}
function updatePackageVersions(previousVersion, nextVersion) {
PACKAGE_PATHS.forEach(partialPath => {
const path = join(ROOT_PATH, partialPath);
const json = readJsonSync(path);
json.version = nextVersion;
for (let key in json.dependencies) {
if (key.startsWith('react-devtools')) {
const version = json.dependencies[key];
json.dependencies[key] = version.replace(previousVersion, nextVersion);
}
}
console.log(chalk.dim(' Updating package JSON: ' + partialPath));
if (!DRY_RUN) {
writeJsonSync(path, json, {spaces: 2});
}
});
}
main();

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {exec} = require('child-process-promise');
const {readJsonSync} = require('fs-extra');
const inquirer = require('inquirer');
const {join, relative} = require('path');
const {DRY_RUN, NPM_PACKAGES, ROOT_PATH} = require('./configuration');
const {
checkNPMPermissions,
clear,
confirm,
execRead,
logger,
readSavedBuildMetadata,
} = require('./utils');
// This is the primary control function for this script.
async function main() {
clear();
await confirm('Have you run the build-and-test script?', () => {
const buildAndTestScriptPath = join(__dirname, 'build-and-test.js');
const pathToPrint = relative(process.cwd(), buildAndTestScriptPath);
console.log('Begin by running the build-and-test script:');
console.log(chalk.bold.green(' ' + pathToPrint));
});
const {archivePath, buildID} = readSavedBuildMetadata();
await checkNPMPermissions();
await publishToNPM();
await printFinalInstructions(buildID, archivePath);
}
async function printFinalInstructions(buildID, archivePath) {
console.log('');
console.log(
'You are now ready to publish the extension to Chrome, Edge, and Firefox:'
);
console.log(
` ${chalk.blue.underline(
'https://fburl.com/publish-react-devtools-extensions'
)}`
);
console.log('');
console.log('When publishing to Firefox, remember the following:');
console.log(` Build id: ${chalk.bold(buildID)}`);
console.log(` Git archive: ${chalk.bold(archivePath)}`);
console.log('');
console.log('Also consider syncing this release to Facebook:');
console.log(` ${chalk.bold.green('js1 upgrade react-devtools')}`);
}
async function publishToNPM() {
const {otp} = await inquirer.prompt([
{
type: 'input',
name: 'otp',
message: 'Please provide an NPM two-factor auth token:',
},
]);
console.log('');
if (!otp) {
console.error(chalk.red(`Invalid OTP provided: "${chalk.bold(otp)}"`));
process.exit(0);
}
for (let index = 0; index < NPM_PACKAGES.length; index++) {
const npmPackage = NPM_PACKAGES[index];
const packagePath = join(ROOT_PATH, 'packages', npmPackage);
const {version} = readJsonSync(join(packagePath, 'package.json'));
// Check if this package version has already been published.
// If so we might be resuming from a previous run.
// We could infer this by comparing the build-info.json,
// But for now the easiest way is just to ask if this is expected.
const info = await execRead(`npm view ${npmPackage}@${version}`);
if (info) {
console.log('');
console.log(
`${npmPackage} version ${chalk.bold(
version
)} has already been published.`
);
await confirm('Is this expected?');
}
if (DRY_RUN) {
console.log(`Publishing package ${chalk.bold(npmPackage)}`);
console.log(chalk.dim(` npm publish --otp=${otp}`));
} else {
const publishPromise = exec(`npm publish --otp=${otp}`, {
cwd: packagePath,
});
await logger(
publishPromise,
`Publishing package ${chalk.bold(npmPackage)}`,
{
estimate: 2500,
}
);
}
}
}
main();

View File

@@ -1,553 +0,0 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {exec} = require('child-process-promise');
const {readFileSync, writeFileSync} = require('fs');
const {readJsonSync, writeJsonSync} = require('fs-extra');
const inquirer = require('inquirer');
const {homedir} = require('os');
const createLogger = require('progress-estimator');
const {join} = require('path');
const semver = require('semver');
const PACKAGE_PATHS = [
'packages/react-devtools/package.json',
'packages/react-devtools-core/package.json',
'packages/react-devtools-inline/package.json',
'packages/react-devtools-scheduling-profiler/package.json',
];
const MANIFEST_PATHS = [
'packages/react-devtools-extensions/chrome/manifest.json',
'packages/react-devtools-extensions/edge/manifest.json',
'packages/react-devtools-extensions/firefox/manifest.json',
];
const NPM_PACKAGES = [
'react-devtools',
'react-devtools-core',
'react-devtools-inline',
];
const CHANGELOG_PATH = 'packages/react-devtools/CHANGELOG.md';
const PULL_REQUEST_BASE_URL = 'https://github.com/facebook/react/pull/';
const RELEASE_SCRIPT_TOKEN = '<!-- RELEASE_SCRIPT_TOKEN -->';
const ROOT_PATH = join(__dirname, '..', '..');
const DRY_RUN = process.argv.includes('--dry');
const logger = createLogger({
storagePath: join(__dirname, '.progress-estimator'),
});
// This is the primary control function for this script.
async function main() {
await checkNPMPermissions();
const releaseType = await getReleaseType();
const path = join(ROOT_PATH, PACKAGE_PATHS[0]);
const previousVersion = readJsonSync(path).version;
const {major, minor, patch} = semver(previousVersion);
const nextVersion =
releaseType === 'minor'
? `${major}.${minor + 1}.${patch}`
: `${major}.${minor}.${patch + 1}`;
updatePackageVersions(previousVersion, nextVersion);
updateManifestVersions(previousVersion, nextVersion);
console.log('');
console.log(
`Packages and manifests have been updated from version ${chalk.bold(
previousVersion
)} to ${chalk.bold(nextVersion)}`
);
console.log('');
const sha = await getPreviousCommitSha();
const commitLog = await getCommitLog(sha);
updateChangelog(nextVersion, commitLog);
await reviewChangelogPrompt();
await commitPendingChanges(previousVersion, nextVersion);
const archivePath = await archiveGitRevision();
const buildID = await downloadLatestReactBuild();
await buildAndTestInlinePackage();
await buildAndTestStandalonePackage();
await buildAndTestExtensions();
await publishToNPM();
await printFinalInstructions(buildID, archivePath);
}
async function archiveGitRevision() {
const desktopPath = join(homedir(), 'Desktop');
const archivePath = join(desktopPath, 'DevTools.tgz');
console.log(`Creating git archive at ${chalk.dim(archivePath)}`);
console.log('');
if (!DRY_RUN) {
await exec(`git archive main | gzip > ${archivePath}`, {cwd: ROOT_PATH});
}
return archivePath;
}
async function buildAndTestExtensions() {
const extensionsPackagePath = join(
ROOT_PATH,
'packages',
'react-devtools-extensions'
);
const buildExtensionsPromise = exec('yarn build', {
cwd: extensionsPackagePath,
});
await logger(
buildExtensionsPromise,
`Building browser extensions ${chalk.dim('(this may take a minute)')}`,
{
estimate: 60000,
}
);
console.log('');
console.log(`Extensions have been build for Chrome, Edge, and Firefox.`);
console.log('');
console.log('Smoke test each extension before continuing:');
console.log(` ${chalk.bold.green('cd ' + extensionsPackagePath)}`);
console.log('');
console.log(` ${chalk.dim('# Test Chrome extension')}`);
console.log(` ${chalk.bold.green('yarn test:chrome')}`);
console.log('');
console.log(` ${chalk.dim('# Test Edge extension')}`);
console.log(` ${chalk.bold.green('yarn test:edge')}`);
console.log('');
console.log(` ${chalk.dim('# Firefox Chrome extension')}`);
console.log(` ${chalk.bold.green('yarn test:firefox')}`);
await confirmContinue();
}
async function buildAndTestStandalonePackage() {
const corePackagePath = join(ROOT_PATH, 'packages', 'react-devtools-core');
const buildCorePromise = exec('yarn build', {cwd: corePackagePath});
await logger(
buildCorePromise,
`Building ${chalk.bold('react-devtools-core')} package.`,
{
estimate: 25000,
}
);
const standalonePackagePath = join(ROOT_PATH, 'packages', 'react-devtools');
const safariFixturePath = join(
ROOT_PATH,
'fixtures',
'devtools',
'standalone',
'index.html'
);
console.log('');
console.log(
`Test the ${chalk.bold('react-devtools-core')} target before continuing:`
);
console.log(` ${chalk.bold.green('cd ' + standalonePackagePath)}`);
console.log(` ${chalk.bold.green('yarn start')}`);
console.log('');
console.log(
'The following fixture can be useful for testing Safari integration:'
);
console.log(` ${chalk.dim(safariFixturePath)}`);
await confirmContinue();
}
async function buildAndTestInlinePackage() {
const inlinePackagePath = join(
ROOT_PATH,
'packages',
'react-devtools-inline'
);
const buildPromise = exec('yarn build', {cwd: inlinePackagePath});
await logger(
buildPromise,
`Building ${chalk.bold('react-devtools-inline')} package.`,
{
estimate: 10000,
}
);
const shellPackagePath = join(ROOT_PATH, 'packages', 'react-devtools-shell');
console.log('');
console.log(`Built ${chalk.bold('react-devtools-inline')} target.`);
console.log('');
console.log('Test this build before continuing:');
console.log(` ${chalk.bold.green('cd ' + shellPackagePath)}`);
console.log(` ${chalk.bold.green('yarn start')}`);
await confirmContinue();
}
async function checkNPMPermissions() {
const currentUser = await execRead('npm whoami');
const failedProjects = [];
const checkProject = async project => {
const owners = (await execRead(`npm owner ls ${project}`))
.split('\n')
.filter(owner => owner)
.map(owner => owner.split(' ')[0]);
if (!owners.includes(currentUser)) {
failedProjects.push(project);
}
};
await logger(
Promise.all(NPM_PACKAGES.map(checkProject)),
`Checking NPM permissions for ${chalk.bold(currentUser)}.`,
{estimate: 2500}
);
console.log('');
if (failedProjects.length) {
console.error(chalk.bold('Insufficient NPM permissions'));
console.error('');
console.error(
`NPM user {underline ${currentUser}} is not an owner for: ${chalk.bold(
failedProjects.join(', ')
)}`
);
console.error(
'Please contact a React team member to be added to the above project(s).'
);
process.exit(1);
}
}
async function commitPendingChanges(previousVersion, nextVersion) {
console.log('');
console.log('Committing revision and changelog.');
console.log(chalk.dim(' git add .'));
console.log(
chalk.dim(
` git commit -m "React DevTools ${previousVersion} -> ${nextVersion}"`
)
);
if (!DRY_RUN) {
await exec(`
git add .
git commit -m "React DevTools ${previousVersion} -> ${nextVersion}"
`);
}
console.log('');
console.log(`Please push this commit before continuing:`);
console.log(` ${chalk.bold.green('git push')}`);
await confirmContinue();
}
async function confirmContinue() {
console.log('');
const {confirm} = await inquirer.prompt({
name: 'confirm',
type: 'confirm',
message: 'Continue the release?',
});
if (!confirm) {
process.exit(0);
}
console.log('');
}
async function downloadLatestReactBuild() {
const releaseScriptPath = join(ROOT_PATH, 'scripts', 'release');
const installPromise = exec('yarn install', {cwd: releaseScriptPath});
await logger(installPromise, 'Installing release script dependencies.', {
estimate: 5000,
});
console.log('');
const {commit} = await inquirer.prompt([
{
type: 'input',
name: 'commit',
message: 'Which React version (commit) should be used?',
default: 'main',
},
]);
console.log('');
const downloadScriptPath = join(
releaseScriptPath,
'download-experimental-build.js'
);
const downloadPromise = execRead(
`"${downloadScriptPath}" --commit=${commit}`
);
const output = await logger(
downloadPromise,
'Downloading React artifacts from CI.',
{estimate: 15000}
);
const match = output.match('--build=([0-9]+)');
if (match.length === 0) {
console.error(`No build ID found in "${output}"`);
process.exit(1);
}
const buildID = match[1];
console.log('');
console.log(`Downloaded artiacts for CI build ${chalk.bold(buildID)}.`);
await confirmContinue();
return buildID;
}
async function execRead(command, options) {
const {stdout} = await exec(command, options);
return stdout.trim();
}
async function getCommitLog(sha) {
let formattedLog = '';
const rawLog = await execRead(`
git log --topo-order --pretty=format:'%s' ${sha}...HEAD -- packages/react-devtools*
`);
rawLog.split('\n').forEach(line => {
line = line.replace('[DevTools] ', '');
const match = line.match(/(.+) \(#([0-9]+)\)/);
if (match !== null) {
const title = match[1];
const pr = match[2];
formattedLog += `\n* ${title} ([USERNAME](https://github.com/USERNAME) in [#${pr}](${PULL_REQUEST_BASE_URL}${pr}))`;
} else {
formattedLog += `\n* ${line}`;
}
});
return formattedLog;
}
async function getPreviousCommitSha() {
const choices = [];
const lines = await execRead(`
git log --max-count=5 --topo-order --pretty=format:'%H:::%s:::%as' HEAD -- ${PACKAGE_PATHS[0]}
`);
lines.split('\n').forEach((line, index) => {
const [hash, message, date] = line.split(':::');
choices.push({
name: `${chalk.bold(hash)} ${chalk.dim(date)} ${message}`,
value: hash,
short: date,
});
});
const {sha} = await inquirer.prompt([
{
type: 'list',
name: 'sha',
message: 'Which of the commits above marks the last DevTools release?',
choices,
default: choices[0].value,
},
]);
return sha;
}
async function getReleaseType() {
const {releaseType} = await inquirer.prompt([
{
type: 'list',
name: 'releaseType',
message: 'Which type of release is this?',
choices: [
{
name: 'Minor (new user facing functionality)',
value: 'minor',
short: 'Minor',
},
{name: 'Patch (bug fixes only)', value: 'patch', short: 'Patch'},
],
default: 'patch',
},
]);
return releaseType;
}
async function printFinalInstructions(buildID, archivePath) {
console.log('');
console.log(
'You are now ready to publish the extension to Chrome, Edge, and Firefox:'
);
console.log(
` ${chalk.blue.underline(
'https://fburl.com/publish-react-devtools-extensions'
)}`
);
console.log('');
console.log('When publishing to Firefox, remember the following:');
console.log(` Build id: ${chalk.bold(buildID)}`);
console.log(` Git archive: ${chalk.bold(archivePath)}`);
console.log('');
console.log('Also consider syncing this release to Facebook:');
console.log(` ${chalk.bold.green('js1 upgrade react-devtools')}`);
}
async function publishToNPM() {
const {otp} = await inquirer.prompt([
{
type: 'input',
name: 'otp',
message: 'Please provide an NPM two-factor auth token:',
},
]);
console.log('');
if (!otp) {
console.error(`Invalid OTP provided: "${chalk.bold(otp)}"`);
process.exit(0);
}
for (let index = 0; index < NPM_PACKAGES.length; index++) {
const npmPackage = NPM_PACKAGES[index];
const packagePath = join(ROOT_PATH, 'packages', npmPackage);
if (DRY_RUN) {
console.log(`Publishing package ${chalk.bold(npmPackage)}`);
console.log(chalk.dim(` npm publish --otp=${otp}`));
} else {
const publishPromise = exec(`npm publish --otp=${otp}`, {
cwd: packagePath,
});
await logger(
publishPromise,
`Publishing package ${chalk.bold(npmPackage)}`,
{
estimate: 2500,
}
);
}
}
}
async function reviewChangelogPrompt() {
console.log('');
console.log(
'The changelog has been updated with commits since the previous release:'
);
console.log(` ${chalk.bold(CHANGELOG_PATH)}`);
console.log('');
console.log('Please review the new changelog text for the following:');
console.log(' 1. Organize the list into Features vs Bugfixes');
console.log(' 1. Filter out any non-user-visible changes (e.g. typo fixes)');
console.log(' 1. Combine related PRs into a single bullet list.');
console.log(
' 1. Replacing the "USERNAME" placeholder text with the GitHub username(s)'
);
console.log('');
console.log(` ${chalk.bold.green(`open ${CHANGELOG_PATH}`)}`);
await confirmContinue();
}
function updateChangelog(nextVersion, commitLog) {
const path = join(ROOT_PATH, CHANGELOG_PATH);
const oldChangelog = readFileSync(path, 'utf8');
const [beginning, end] = oldChangelog.split(RELEASE_SCRIPT_TOKEN);
const dateString = new Date().toLocaleDateString('en-us', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const header = `## ${nextVersion} (${dateString})`;
const newChangelog = `${beginning}${RELEASE_SCRIPT_TOKEN}\n\n${header}\n${commitLog}${end}`;
console.log(chalk.dim(' Updating changelog: ' + CHANGELOG_PATH));
if (!DRY_RUN) {
writeFileSync(path, newChangelog);
}
}
function updateManifestVersions(previousVersion, nextVersion) {
MANIFEST_PATHS.forEach(partialPath => {
const path = join(ROOT_PATH, partialPath);
const json = readJsonSync(path);
json.version = nextVersion;
if (json.hasOwnProperty('version_name')) {
json.version_name = nextVersion;
}
console.log(chalk.dim(' Updating manifest JSON: ' + partialPath));
if (!DRY_RUN) {
writeJsonSync(path, json, {spaces: 2});
}
});
}
function updatePackageVersions(previousVersion, nextVersion) {
PACKAGE_PATHS.forEach(partialPath => {
const path = join(ROOT_PATH, partialPath);
const json = readJsonSync(path);
json.version = nextVersion;
for (let key in json.dependencies) {
if (key.startsWith('react-devtools')) {
const version = json.dependencies[key];
json.dependencies[key] = version.replace(previousVersion, nextVersion);
}
}
console.log(chalk.dim(' Updating package JSON: ' + partialPath));
if (!DRY_RUN) {
writeJsonSync(path, json, {spaces: 2});
}
});
}
main();

128
scripts/devtools/utils.js Normal file
View File

@@ -0,0 +1,128 @@
'use strict';
const chalk = require('chalk');
const {exec} = require('child-process-promise');
const {existsSync, mkdirSync} = require('fs');
const {readJsonSync, writeJsonSync} = require('fs-extra');
const inquirer = require('inquirer');
const {join} = require('path');
const createLogger = require('progress-estimator');
const {
BUILD_METADATA_TEMP_DIRECTORY,
NPM_PACKAGES,
} = require('./configuration');
const logger = createLogger({
storagePath: join(__dirname, '.progress-estimator'),
});
async function checkNPMPermissions() {
const currentUser = await execRead('npm whoami');
const failedProjects = [];
const checkProject = async project => {
const owners = (await execRead(`npm owner ls ${project}`))
.split('\n')
.filter(owner => owner)
.map(owner => owner.split(' ')[0]);
if (!owners.includes(currentUser)) {
failedProjects.push(project);
}
};
await logger(
Promise.all(NPM_PACKAGES.map(checkProject)),
`Checking NPM permissions for ${chalk.bold(currentUser)}.`,
{estimate: 2500}
);
console.log('');
if (failedProjects.length) {
console.error(chalk.red.bold('Insufficient NPM permissions'));
console.error('');
console.error(
chalk.red(
`NPM user {underline ${currentUser}} is not an owner for: ${chalk.bold(
failedProjects.join(', ')
)}`
)
);
console.error(
chalk.red(
'Please contact a React team member to be added to the above project(s).'
)
);
process.exit(1);
}
}
function clear() {
console.clear();
}
async function confirm(message, exitFunction) {
console.log('');
const {confirmation} = await inquirer.prompt({
name: 'confirmation',
type: 'confirm',
message,
});
console.log('');
if (!confirmation) {
if (typeof exitFunction === 'function') {
exitFunction();
}
process.exit(0);
}
}
async function confirmContinue(exitFunction) {
await confirm('Continue the release?', exitFunction);
}
async function execRead(command, options) {
const {stdout} = await exec(command, options);
return stdout.trim();
}
function readSavedBuildMetadata() {
const path = join(BUILD_METADATA_TEMP_DIRECTORY, 'metadata');
if (!existsSync(path)) {
console.error(chalk.red('Expected to find build metadata at:'));
console.error(chalk.dim(` ${path}`));
process.exit(1);
}
const {archivePath, buildID} = readJsonSync(path);
return {archivePath, buildID};
}
function saveBuildMetadata({archivePath, buildID}) {
const path = join(BUILD_METADATA_TEMP_DIRECTORY, 'metadata');
if (!existsSync(BUILD_METADATA_TEMP_DIRECTORY)) {
mkdirSync(BUILD_METADATA_TEMP_DIRECTORY);
}
writeJsonSync(path, {archivePath, buildID}, {spaces: 2});
}
module.exports = {
checkNPMPermissions,
clear,
confirm,
confirmContinue,
execRead,
logger,
readSavedBuildMetadata,
saveBuildMetadata,
};