@samitouri / QOS-React-2 / commits / b9964684bd

DevTools release script updates: (#22170)

Brian Vaughn committed Aug 24, 2021 at 18:59 UTC b9964684bd8c909fc3d88f1cd47aa1f45ea7ba32
8 files changed +797 -554
scripts/devtools/.gitignore
+2 -1
@@ -1 +1,2 @@
1 -.progress-estimator
\ No newline at end of file
1 +.build-metadata
2 +.progress-estimator
scripts/devtools/README.md
+30
@@ -0,0 +1,30 @@
1 +# Releasing DevTools
2 +
3 +To release DevTools, do the following steps (in order):
4 +1. [Prepare a release](#prepare-a-release)
5 +2. [Build and test a release](#build-and-test-a-release)
6 +3. [Publish a release](#publish-a-release)
7 +
8 +Each of the scripts can be run with a `--dry` flag to test without committing or publishing any changes.
9 +
10 +### Prepare a release
11 +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:
12 +```sh
13 +./prepare-release.js
14 +```
15 +
16 +You'll need to follow the instructions at the end of the script to push the committed changes to the main fork on GitHub.
17 +
18 +### Build and test a release
19 +To build and test a release, run the `build-and-test` script:
20 +```sh
21 +./build-and-test.js
22 +```
23 +
24 +### Publish a release
25 +To publish a release to NPM, run the `publish-release` script:
26 +```sh
27 +./publish-release.js
28 +```
29 +
30 +You'll need to follow the instructions at the end of the script to upload the extension to Chrome, Edge, and Firefox stores.
\ No newline at end of file
scripts/devtools/build-and-test.js new
+221
@@ -0,0 +1,221 @@
1 +#!/usr/bin/env node
2 +
3 +'use strict';
4 +
5 +const chalk = require('chalk');
6 +const {exec} = require('child-process-promise');
7 +const inquirer = require('inquirer');
8 +const {homedir} = require('os');
9 +const {join, relative} = require('path');
10 +const {DRY_RUN, ROOT_PATH} = require('./configuration');
11 +const {
12 + clear,
13 + confirm,
14 + confirmContinue,
15 + execRead,
16 + logger,
17 + saveBuildMetadata,
18 +} = require('./utils');
19 +
20 +// This is the primary control function for this script.
21 +async function main() {
22 + clear();
23 +
24 + await confirm('Have you run the prepare-release script?', () => {
25 + const prepareReleaseScriptPath = join(__dirname, 'prepare-release.js');
26 + const pathToPrint = relative(process.cwd(), prepareReleaseScriptPath);
27 +
28 + console.log('Begin by running the prepare-release script:');
29 + console.log(chalk.bold.green(' ' + pathToPrint));
30 + });
31 +
32 + const archivePath = await archiveGitRevision();
33 + const buildID = await downloadLatestReactBuild();
34 +
35 + await buildAndTestInlinePackage();
36 + await buildAndTestStandalonePackage();
37 + await buildAndTestExtensions();
38 +
39 + saveBuildMetadata({archivePath, buildID});
40 +
41 + printFinalInstructions();
42 +}
43 +
44 +async function archiveGitRevision() {
45 + const desktopPath = join(homedir(), 'Desktop');
46 + const archivePath = join(desktopPath, 'DevTools.tgz');
47 +
48 + console.log(`Creating git archive at ${chalk.dim(archivePath)}`);
49 + console.log('');
50 +
51 + if (!DRY_RUN) {
52 + await exec(`git archive main | gzip > ${archivePath}`, {cwd: ROOT_PATH});
53 + }
54 +
55 + return archivePath;
56 +}
57 +
58 +async function buildAndTestExtensions() {
59 + const extensionsPackagePath = join(
60 + ROOT_PATH,
61 + 'packages',
62 + 'react-devtools-extensions'
63 + );
64 + const buildExtensionsPromise = exec('yarn build', {
65 + cwd: extensionsPackagePath,
66 + });
67 +
68 + await logger(
69 + buildExtensionsPromise,
70 + `Building browser extensions ${chalk.dim('(this may take a minute)')}`,
71 + {
72 + estimate: 60000,
73 + }
74 + );
75 +
76 + console.log('');
77 + console.log(`Extensions have been build for Chrome, Edge, and Firefox.`);
78 + console.log('');
79 + console.log('Smoke test each extension before continuing:');
80 + console.log(` ${chalk.bold.green('cd ' + extensionsPackagePath)}`);
81 + console.log('');
82 + console.log(` ${chalk.dim('# Test Chrome extension')}`);
83 + console.log(` ${chalk.bold.green('yarn test:chrome')}`);
84 + console.log('');
85 + console.log(` ${chalk.dim('# Test Edge extension')}`);
86 + console.log(` ${chalk.bold.green('yarn test:edge')}`);
87 + console.log('');
88 + console.log(` ${chalk.dim('# Firefox Chrome extension')}`);
89 + console.log(` ${chalk.bold.green('yarn test:firefox')}`);
90 +
91 + await confirmContinue();
92 +}
93 +
94 +async function buildAndTestStandalonePackage() {
95 + const corePackagePath = join(ROOT_PATH, 'packages', 'react-devtools-core');
96 + const buildCorePromise = exec('yarn build', {cwd: corePackagePath});
97 +
98 + await logger(
99 + buildCorePromise,
100 + `Building ${chalk.bold('react-devtools-core')} package.`,
101 + {
102 + estimate: 25000,
103 + }
104 + );
105 +
106 + const standalonePackagePath = join(ROOT_PATH, 'packages', 'react-devtools');
107 + const safariFixturePath = join(
108 + ROOT_PATH,
109 + 'fixtures',
110 + 'devtools',
111 + 'standalone',
112 + 'index.html'
113 + );
114 +
115 + console.log('');
116 + console.log(
117 + `Test the ${chalk.bold('react-devtools-core')} target before continuing:`
118 + );
119 + console.log(` ${chalk.bold.green('cd ' + standalonePackagePath)}`);
120 + console.log(` ${chalk.bold.green('yarn start')}`);
121 + console.log('');
122 + console.log(
123 + 'The following fixture can be useful for testing Safari integration:'
124 + );
125 + console.log(` ${chalk.dim(safariFixturePath)}`);
126 +
127 + await confirmContinue();
128 +}
129 +
130 +async function buildAndTestInlinePackage() {
131 + const inlinePackagePath = join(
132 + ROOT_PATH,
133 + 'packages',
134 + 'react-devtools-inline'
135 + );
136 + const buildPromise = exec('yarn build', {cwd: inlinePackagePath});
137 +
138 + await logger(
139 + buildPromise,
140 + `Building ${chalk.bold('react-devtools-inline')} package.`,
141 + {
142 + estimate: 10000,
143 + }
144 + );
145 +
146 + const shellPackagePath = join(ROOT_PATH, 'packages', 'react-devtools-shell');
147 +
148 + console.log('');
149 + console.log(`Built ${chalk.bold('react-devtools-inline')} target.`);
150 + console.log('');
151 + console.log('Test this build before continuing:');
152 + console.log(` ${chalk.bold.green('cd ' + shellPackagePath)}`);
153 + console.log(` ${chalk.bold.green('yarn start')}`);
154 +
155 + await confirmContinue();
156 +}
157 +
158 +async function downloadLatestReactBuild() {
159 + const releaseScriptPath = join(ROOT_PATH, 'scripts', 'release');
160 + const installPromise = exec('yarn install', {cwd: releaseScriptPath});
161 +
162 + await logger(
163 + installPromise,
164 + `Installing release script dependencies. ${chalk.dim(
165 + '(this may take a minute if CI is still running)'
166 + )}`,
167 + {
168 + estimate: 5000,
169 + }
170 + );
171 +
172 + console.log('');
173 +
174 + const {commit} = await inquirer.prompt([
175 + {
176 + type: 'input',
177 + name: 'commit',
178 + message: 'Which React version (commit) should be used?',
179 + default: 'main',
180 + },
181 + ]);
182 + console.log('');
183 +
184 + const downloadScriptPath = join(
185 + releaseScriptPath,
186 + 'download-experimental-build.js'
187 + );
188 + const downloadPromise = execRead(
189 + `"${downloadScriptPath}" --commit=${commit}`
190 + );
191 +
192 + const output = await logger(
193 + downloadPromise,
194 + 'Downloading React artifacts from CI.',
195 + {estimate: 15000}
196 + );
197 +
198 + const match = output.match('--build=([0-9]+)');
199 + if (match.length === 0) {
200 + console.error(chalk.red(`No build ID found in "${output}"`));
201 + process.exit(1);
202 + }
203 +
204 + const buildID = match[1];
205 +
206 + console.log('');
207 + console.log(`Downloaded artiacts for CI build ${chalk.bold(buildID)}.`);
208 +
209 + return buildID;
210 +}
211 +
212 +function printFinalInstructions() {
213 + const publishReleaseScriptPath = join(__dirname, 'publish-release.js');
214 + const pathToPrint = relative(process.cwd(), publishReleaseScriptPath);
215 +
216 + console.log('');
217 + console.log('Continue by running the publish-release script:');
218 + console.log(chalk.bold.green(' ' + pathToPrint));
219 +}
220 +
221 +main();
scripts/devtools/configuration.js new
+46
@@ -0,0 +1,46 @@
1 +'use strict';
2 +
3 +const {join} = require('path');
4 +
5 +const PACKAGE_PATHS = [
6 + 'packages/react-devtools/package.json',
7 + 'packages/react-devtools-core/package.json',
8 + 'packages/react-devtools-inline/package.json',
9 + 'packages/react-devtools-scheduling-profiler/package.json',
10 +];
11 +
12 +const MANIFEST_PATHS = [
13 + 'packages/react-devtools-extensions/chrome/manifest.json',
14 + 'packages/react-devtools-extensions/edge/manifest.json',
15 + 'packages/react-devtools-extensions/firefox/manifest.json',
16 +];
17 +
18 +const NPM_PACKAGES = [
19 + 'react-devtools',
20 + 'react-devtools-core',
21 + 'react-devtools-inline',
22 +];
23 +
24 +const CHANGELOG_PATH = 'packages/react-devtools/CHANGELOG.md';
25 +
26 +const PULL_REQUEST_BASE_URL = 'https://github.com/facebook/react/pull/';
27 +
28 +const RELEASE_SCRIPT_TOKEN = '<!-- RELEASE_SCRIPT_TOKEN -->';
29 +
30 +const ROOT_PATH = join(__dirname, '..', '..');
31 +
32 +const DRY_RUN = process.argv.includes('--dry');
33 +
34 +const BUILD_METADATA_TEMP_DIRECTORY = join(__dirname, '.build-metadata');
35 +
36 +module.exports = {
37 + BUILD_METADATA_TEMP_DIRECTORY,
38 + CHANGELOG_PATH,
39 + DRY_RUN,
40 + MANIFEST_PATHS,
41 + NPM_PACKAGES,
42 + PACKAGE_PATHS,
43 + PULL_REQUEST_BASE_URL,
44 + RELEASE_SCRIPT_TOKEN,
45 + ROOT_PATH,
46 +};
scripts/devtools/prepare-release.js new
+254
@@ -0,0 +1,254 @@
1 +#!/usr/bin/env node
2 +
3 +'use strict';
4 +
5 +const chalk = require('chalk');
6 +const {exec} = require('child-process-promise');
7 +const {readFileSync, writeFileSync} = require('fs');
8 +const {readJsonSync, writeJsonSync} = require('fs-extra');
9 +const inquirer = require('inquirer');
10 +const {join, relative} = require('path');
11 +const semver = require('semver');
12 +const {
13 + CHANGELOG_PATH,
14 + DRY_RUN,
15 + MANIFEST_PATHS,
16 + PACKAGE_PATHS,
17 + PULL_REQUEST_BASE_URL,
18 + RELEASE_SCRIPT_TOKEN,
19 + ROOT_PATH,
20 +} = require('./configuration');
21 +const {
22 + checkNPMPermissions,
23 + clear,
24 + confirmContinue,
25 + execRead,
26 +} = require('./utils');
27 +
28 +// This is the primary control function for this script.
29 +async function main() {
30 + clear();
31 +
32 + await checkNPMPermissions();
33 +
34 + const releaseType = await getReleaseType();
35 +
36 + const path = join(ROOT_PATH, PACKAGE_PATHS[0]);
37 + const previousVersion = readJsonSync(path).version;
38 + const {major, minor, patch} = semver(previousVersion);
39 + const nextVersion =
40 + releaseType === 'minor'
41 + ? `${major}.${minor + 1}.${patch}`
42 + : `${major}.${minor}.${patch + 1}`;
43 +
44 + updatePackageVersions(previousVersion, nextVersion);
45 + updateManifestVersions(previousVersion, nextVersion);
46 +
47 + console.log('');
48 + console.log(
49 + `Packages and manifests have been updated from version ${chalk.bold(
50 + previousVersion
51 + )} to ${chalk.bold(nextVersion)}`
52 + );
53 + console.log('');
54 +
55 + const sha = await getPreviousCommitSha();
56 + const commitLog = await getCommitLog(sha);
57 +
58 + updateChangelog(nextVersion, commitLog);
59 +
60 + await reviewChangelogPrompt();
61 +
62 + await commitPendingChanges(previousVersion, nextVersion);
63 +
64 + printFinalInstructions();
65 +}
66 +
67 +async function commitPendingChanges(previousVersion, nextVersion) {
68 + console.log('');
69 + console.log('Committing revision and changelog.');
70 + console.log(chalk.dim(' git add .'));
71 + console.log(
72 + chalk.dim(
73 + ` git commit -m "React DevTools ${previousVersion} -> ${nextVersion}"`
74 + )
75 + );
76 +
77 + if (!DRY_RUN) {
78 + await exec(`
79 + git add .
80 + git commit -m "React DevTools ${previousVersion} -> ${nextVersion}"
81 + `);
82 + }
83 +
84 + console.log('');
85 + console.log(`Please push this commit before continuing:`);
86 + console.log(` ${chalk.bold.green('git push')}`);
87 +
88 + await confirmContinue();
89 +}
90 +
91 +async function getCommitLog(sha) {
92 + let formattedLog = '';
93 +
94 + const rawLog = await execRead(`
95 + git log --topo-order --pretty=format:'%s' ${sha}...HEAD -- packages/react-devtools*
96 + `);
97 + rawLog.split('\n').forEach(line => {
98 + line = line.replace('[DevTools] ', '');
99 +
100 + const match = line.match(/(.+) \(#([0-9]+)\)/);
101 + if (match !== null) {
102 + const title = match[1];
103 + const pr = match[2];
104 +
105 + formattedLog += `\n* ${title} ([USERNAME](https://github.com/USERNAME) in [#${pr}](${PULL_REQUEST_BASE_URL}${pr}))`;
106 + } else {
107 + formattedLog += `\n* ${line}`;
108 + }
109 + });
110 +
111 + return formattedLog;
112 +}
113 +
114 +async function getPreviousCommitSha() {
115 + const choices = [];
116 +
117 + const lines = await execRead(`
118 + git log --max-count=5 --topo-order --pretty=format:'%H:::%s:::%as' HEAD -- ${PACKAGE_PATHS[0]}
119 + `);
120 + lines.split('\n').forEach((line, index) => {
121 + const [hash, message, date] = line.split(':::');
122 + choices.push({
123 + name: `${chalk.bold(hash)} ${chalk.dim(date)} ${message}`,
124 + value: hash,
125 + short: date,
126 + });
127 + });
128 +
129 + const {sha} = await inquirer.prompt([
130 + {
131 + type: 'list',
132 + name: 'sha',
133 + message: 'Which of the commits above marks the last DevTools release?',
134 + choices,
135 + default: choices[0].value,
136 + },
137 + ]);
138 +
139 + return sha;
140 +}
141 +
142 +async function getReleaseType() {
143 + const {releaseType} = await inquirer.prompt([
144 + {
145 + type: 'list',
146 + name: 'releaseType',
147 + message: 'Which type of release is this?',
148 + choices: [
149 + {
150 + name: 'Minor (new user facing functionality)',
151 + value: 'minor',
152 + short: 'Minor',
153 + },
154 + {name: 'Patch (bug fixes only)', value: 'patch', short: 'Patch'},
155 + ],
156 + default: 'patch',
157 + },
158 + ]);
159 +
160 + return releaseType;
161 +}
162 +
163 +function printFinalInstructions() {
164 + const buildAndTestcriptPath = join(__dirname, 'build-and-test.js');
165 + const pathToPrint = relative(process.cwd(), buildAndTestcriptPath);
166 +
167 + console.log('');
168 + console.log('Continue by running the build-and-test script:');
169 + console.log(chalk.bold.green(' ' + pathToPrint));
170 +}
171 +
172 +async function reviewChangelogPrompt() {
173 + console.log('');
174 + console.log(
175 + 'The changelog has been updated with commits since the previous release:'
176 + );
177 + console.log(` ${chalk.bold(CHANGELOG_PATH)}`);
178 + console.log('');
179 + console.log('Please review the new changelog text for the following:');
180 + console.log(' 1. Organize the list into Features vs Bugfixes');
181 + console.log(' 1. Filter out any non-user-visible changes (e.g. typo fixes)');
182 + console.log(' 1. Combine related PRs into a single bullet list.');
183 + console.log(
184 + ' 1. Replacing the "USERNAME" placeholder text with the GitHub username(s)'
185 + );
186 + console.log('');
187 + console.log(` ${chalk.bold.green(`open ${CHANGELOG_PATH}`)}`);
188 +
189 + await confirmContinue();
190 +}
191 +
192 +function updateChangelog(nextVersion, commitLog) {
193 + const path = join(ROOT_PATH, CHANGELOG_PATH);
194 + const oldChangelog = readFileSync(path, 'utf8');
195 +
196 + const [beginning, end] = oldChangelog.split(RELEASE_SCRIPT_TOKEN);
197 +
198 + const dateString = new Date().toLocaleDateString('en-us', {
199 + year: 'numeric',
200 + month: 'long',
201 + day: 'numeric',
202 + });
203 + const header = `## ${nextVersion} (${dateString})`;
204 +
205 + const newChangelog = `${beginning}${RELEASE_SCRIPT_TOKEN}\n\n${header}\n${commitLog}${end}`;
206 +
207 + console.log(chalk.dim(' Updating changelog: ' + CHANGELOG_PATH));
208 +
209 + if (!DRY_RUN) {
210 + writeFileSync(path, newChangelog);
211 + }
212 +}
213 +
214 +function updateManifestVersions(previousVersion, nextVersion) {
215 + MANIFEST_PATHS.forEach(partialPath => {
216 + const path = join(ROOT_PATH, partialPath);
217 + const json = readJsonSync(path);
218 + json.version = nextVersion;
219 +
220 + if (json.hasOwnProperty('version_name')) {
221 + json.version_name = nextVersion;
222 + }
223 +
224 + console.log(chalk.dim(' Updating manifest JSON: ' + partialPath));
225 +
226 + if (!DRY_RUN) {
227 + writeJsonSync(path, json, {spaces: 2});
228 + }
229 + });
230 +}
231 +
232 +function updatePackageVersions(previousVersion, nextVersion) {
233 + PACKAGE_PATHS.forEach(partialPath => {
234 + const path = join(ROOT_PATH, partialPath);
235 + const json = readJsonSync(path);
236 + json.version = nextVersion;
237 +
238 + for (let key in json.dependencies) {
239 + if (key.startsWith('react-devtools')) {
240 + const version = json.dependencies[key];
241 +
242 + json.dependencies[key] = version.replace(previousVersion, nextVersion);
243 + }
244 + }
245 +
246 + console.log(chalk.dim(' Updating package JSON: ' + partialPath));
247 +
248 + if (!DRY_RUN) {
249 + writeJsonSync(path, json, {spaces: 2});
250 + }
251 + });
252 +}
253 +
254 +main();
scripts/devtools/publish-release.js new
+116
@@ -0,0 +1,116 @@
1 +#!/usr/bin/env node
2 +
3 +'use strict';
4 +
5 +const chalk = require('chalk');
6 +const {exec} = require('child-process-promise');
7 +const {readJsonSync} = require('fs-extra');
8 +const inquirer = require('inquirer');
9 +const {join, relative} = require('path');
10 +const {DRY_RUN, NPM_PACKAGES, ROOT_PATH} = require('./configuration');
11 +const {
12 + checkNPMPermissions,
13 + clear,
14 + confirm,
15 + execRead,
16 + logger,
17 + readSavedBuildMetadata,
18 +} = require('./utils');
19 +
20 +// This is the primary control function for this script.
21 +async function main() {
22 + clear();
23 +
24 + await confirm('Have you run the build-and-test script?', () => {
25 + const buildAndTestScriptPath = join(__dirname, 'build-and-test.js');
26 + const pathToPrint = relative(process.cwd(), buildAndTestScriptPath);
27 +
28 + console.log('Begin by running the build-and-test script:');
29 + console.log(chalk.bold.green(' ' + pathToPrint));
30 + });
31 +
32 + const {archivePath, buildID} = readSavedBuildMetadata();
33 +
34 + await checkNPMPermissions();
35 +
36 + await publishToNPM();
37 +
38 + await printFinalInstructions(buildID, archivePath);
39 +}
40 +
41 +async function printFinalInstructions(buildID, archivePath) {
42 + console.log('');
43 + console.log(
44 + 'You are now ready to publish the extension to Chrome, Edge, and Firefox:'
45 + );
46 + console.log(
47 + ` ${chalk.blue.underline(
48 + 'https://fburl.com/publish-react-devtools-extensions'
49 + )}`
50 + );
51 + console.log('');
52 + console.log('When publishing to Firefox, remember the following:');
53 + console.log(` Build id: ${chalk.bold(buildID)}`);
54 + console.log(` Git archive: ${chalk.bold(archivePath)}`);
55 + console.log('');
56 + console.log('Also consider syncing this release to Facebook:');
57 + console.log(` ${chalk.bold.green('js1 upgrade react-devtools')}`);
58 +}
59 +
60 +async function publishToNPM() {
61 + const {otp} = await inquirer.prompt([
62 + {
63 + type: 'input',
64 + name: 'otp',
65 + message: 'Please provide an NPM two-factor auth token:',
66 + },
67 + ]);
68 +
69 + console.log('');
70 +
71 + if (!otp) {
72 + console.error(chalk.red(`Invalid OTP provided: "${chalk.bold(otp)}"`));
73 + process.exit(0);
74 + }
75 +
76 + for (let index = 0; index < NPM_PACKAGES.length; index++) {
77 + const npmPackage = NPM_PACKAGES[index];
78 + const packagePath = join(ROOT_PATH, 'packages', npmPackage);
79 + const {version} = readJsonSync(join(packagePath, 'package.json'));
80 +
81 + // Check if this package version has already been published.
82 + // If so we might be resuming from a previous run.
83 + // We could infer this by comparing the build-info.json,
84 + // But for now the easiest way is just to ask if this is expected.
85 + const info = await execRead(`npm view ${npmPackage}@${version}`);
86 + if (info) {
87 + console.log('');
88 + console.log(
89 + `${npmPackage} version ${chalk.bold(
90 + version
91 + )} has already been published.`
92 + );
93 +
94 + await confirm('Is this expected?');
95 + }
96 +
97 + if (DRY_RUN) {
98 + console.log(`Publishing package ${chalk.bold(npmPackage)}`);
99 + console.log(chalk.dim(` npm publish --otp=${otp}`));
100 + } else {
101 + const publishPromise = exec(`npm publish --otp=${otp}`, {
102 + cwd: packagePath,
103 + });
104 +
105 + await logger(
106 + publishPromise,
107 + `Publishing package ${chalk.bold(npmPackage)}`,
108 + {
109 + estimate: 2500,
110 + }
111 + );
112 + }
113 + }
114 +}
115 +
116 +main();
scripts/devtools/release.js deleted
-553
@@ -1,553 +0,0 @@
1 -#!/usr/bin/env node
2 -
3 -'use strict';
4 -
5 -const chalk = require('chalk');
6 -const {exec} = require('child-process-promise');
7 -const {readFileSync, writeFileSync} = require('fs');
8 -const {readJsonSync, writeJsonSync} = require('fs-extra');
9 -const inquirer = require('inquirer');
10 -const {homedir} = require('os');
11 -const createLogger = require('progress-estimator');
12 -const {join} = require('path');
13 -const semver = require('semver');
14 -
15 -const PACKAGE_PATHS = [
16 - 'packages/react-devtools/package.json',
17 - 'packages/react-devtools-core/package.json',
18 - 'packages/react-devtools-inline/package.json',
19 - 'packages/react-devtools-scheduling-profiler/package.json',
20 -];
21 -
22 -const MANIFEST_PATHS = [
23 - 'packages/react-devtools-extensions/chrome/manifest.json',
24 - 'packages/react-devtools-extensions/edge/manifest.json',
25 - 'packages/react-devtools-extensions/firefox/manifest.json',
26 -];
27 -
28 -const NPM_PACKAGES = [
29 - 'react-devtools',
30 - 'react-devtools-core',
31 - 'react-devtools-inline',
32 -];
33 -
34 -const CHANGELOG_PATH = 'packages/react-devtools/CHANGELOG.md';
35 -
36 -const PULL_REQUEST_BASE_URL = 'https://github.com/facebook/react/pull/';
37 -
38 -const RELEASE_SCRIPT_TOKEN = '<!-- RELEASE_SCRIPT_TOKEN -->';
39 -
40 -const ROOT_PATH = join(__dirname, '..', '..');
41 -
42 -const DRY_RUN = process.argv.includes('--dry');
43 -
44 -const logger = createLogger({
45 - storagePath: join(__dirname, '.progress-estimator'),
46 -});
47 -
48 -// This is the primary control function for this script.
49 -async function main() {
50 - await checkNPMPermissions();
51 -
52 - const releaseType = await getReleaseType();
53 -
54 - const path = join(ROOT_PATH, PACKAGE_PATHS[0]);
55 - const previousVersion = readJsonSync(path).version;
56 - const {major, minor, patch} = semver(previousVersion);
57 - const nextVersion =
58 - releaseType === 'minor'
59 - ? `${major}.${minor + 1}.${patch}`
60 - : `${major}.${minor}.${patch + 1}`;
61 -
62 - updatePackageVersions(previousVersion, nextVersion);
63 - updateManifestVersions(previousVersion, nextVersion);
64 -
65 - console.log('');
66 - console.log(
67 - `Packages and manifests have been updated from version ${chalk.bold(
68 - previousVersion
69 - )} to ${chalk.bold(nextVersion)}`
70 - );
71 - console.log('');
72 -
73 - const sha = await getPreviousCommitSha();
74 - const commitLog = await getCommitLog(sha);
75 -
76 - updateChangelog(nextVersion, commitLog);
77 -
78 - await reviewChangelogPrompt();
79 -
80 - await commitPendingChanges(previousVersion, nextVersion);
81 -
82 - const archivePath = await archiveGitRevision();
83 - const buildID = await downloadLatestReactBuild();
84 -
85 - await buildAndTestInlinePackage();
86 - await buildAndTestStandalonePackage();
87 - await buildAndTestExtensions();
88 -
89 - await publishToNPM();
90 -
91 - await printFinalInstructions(buildID, archivePath);
92 -}
93 -
94 -async function archiveGitRevision() {
95 - const desktopPath = join(homedir(), 'Desktop');
96 - const archivePath = join(desktopPath, 'DevTools.tgz');
97 -
98 - console.log(`Creating git archive at ${chalk.dim(archivePath)}`);
99 - console.log('');
100 -
101 - if (!DRY_RUN) {
102 - await exec(`git archive main | gzip > ${archivePath}`, {cwd: ROOT_PATH});
103 - }
104 -
105 - return archivePath;
106 -}
107 -
108 -async function buildAndTestExtensions() {
109 - const extensionsPackagePath = join(
110 - ROOT_PATH,
111 - 'packages',
112 - 'react-devtools-extensions'
113 - );
114 - const buildExtensionsPromise = exec('yarn build', {
115 - cwd: extensionsPackagePath,
116 - });
117 -
118 - await logger(
119 - buildExtensionsPromise,
120 - `Building browser extensions ${chalk.dim('(this may take a minute)')}`,
121 - {
122 - estimate: 60000,
123 - }
124 - );
125 -
126 - console.log('');
127 - console.log(`Extensions have been build for Chrome, Edge, and Firefox.`);
128 - console.log('');
129 - console.log('Smoke test each extension before continuing:');
130 - console.log(` ${chalk.bold.green('cd ' + extensionsPackagePath)}`);
131 - console.log('');
132 - console.log(` ${chalk.dim('# Test Chrome extension')}`);
133 - console.log(` ${chalk.bold.green('yarn test:chrome')}`);
134 - console.log('');
135 - console.log(` ${chalk.dim('# Test Edge extension')}`);
136 - console.log(` ${chalk.bold.green('yarn test:edge')}`);
137 - console.log('');
138 - console.log(` ${chalk.dim('# Firefox Chrome extension')}`);
139 - console.log(` ${chalk.bold.green('yarn test:firefox')}`);
140 -
141 - await confirmContinue();
142 -}
143 -
144 -async function buildAndTestStandalonePackage() {
145 - const corePackagePath = join(ROOT_PATH, 'packages', 'react-devtools-core');
146 - const buildCorePromise = exec('yarn build', {cwd: corePackagePath});
147 -
148 - await logger(
149 - buildCorePromise,
150 - `Building ${chalk.bold('react-devtools-core')} package.`,
151 - {
152 - estimate: 25000,
153 - }
154 - );
155 -
156 - const standalonePackagePath = join(ROOT_PATH, 'packages', 'react-devtools');
157 - const safariFixturePath = join(
158 - ROOT_PATH,
159 - 'fixtures',
160 - 'devtools',
161 - 'standalone',
162 - 'index.html'
163 - );
164 -
165 - console.log('');
166 - console.log(
167 - `Test the ${chalk.bold('react-devtools-core')} target before continuing:`
168 - );
169 - console.log(` ${chalk.bold.green('cd ' + standalonePackagePath)}`);
170 - console.log(` ${chalk.bold.green('yarn start')}`);
171 - console.log('');
172 - console.log(
173 - 'The following fixture can be useful for testing Safari integration:'
174 - );
175 - console.log(` ${chalk.dim(safariFixturePath)}`);
176 -
177 - await confirmContinue();
178 -}
179 -
180 -async function buildAndTestInlinePackage() {
181 - const inlinePackagePath = join(
182 - ROOT_PATH,
183 - 'packages',
184 - 'react-devtools-inline'
185 - );
186 - const buildPromise = exec('yarn build', {cwd: inlinePackagePath});
187 -
188 - await logger(
189 - buildPromise,
190 - `Building ${chalk.bold('react-devtools-inline')} package.`,
191 - {
192 - estimate: 10000,
193 - }
194 - );
195 -
196 - const shellPackagePath = join(ROOT_PATH, 'packages', 'react-devtools-shell');
197 -
198 - console.log('');
199 - console.log(`Built ${chalk.bold('react-devtools-inline')} target.`);
200 - console.log('');
201 - console.log('Test this build before continuing:');
202 - console.log(` ${chalk.bold.green('cd ' + shellPackagePath)}`);
203 - console.log(` ${chalk.bold.green('yarn start')}`);
204 -
205 - await confirmContinue();
206 -}
207 -
208 -async function checkNPMPermissions() {
209 - const currentUser = await execRead('npm whoami');
210 - const failedProjects = [];
211 -
212 - const checkProject = async project => {
213 - const owners = (await execRead(`npm owner ls ${project}`))
214 - .split('\n')
215 - .filter(owner => owner)
216 - .map(owner => owner.split(' ')[0]);
217 -
218 - if (!owners.includes(currentUser)) {
219 - failedProjects.push(project);
220 - }
221 - };
222 -
223 - await logger(
224 - Promise.all(NPM_PACKAGES.map(checkProject)),
225 - `Checking NPM permissions for ${chalk.bold(currentUser)}.`,
226 - {estimate: 2500}
227 - );
228 -
229 - console.log('');
230 -
231 - if (failedProjects.length) {
232 - console.error(chalk.bold('Insufficient NPM permissions'));
233 - console.error('');
234 - console.error(
235 - `NPM user {underline ${currentUser}} is not an owner for: ${chalk.bold(
236 - failedProjects.join(', ')
237 - )}`
238 - );
239 - console.error(
240 - 'Please contact a React team member to be added to the above project(s).'
241 - );
242 - process.exit(1);
243 - }
244 -}
245 -
246 -async function commitPendingChanges(previousVersion, nextVersion) {
247 - console.log('');
248 - console.log('Committing revision and changelog.');
249 - console.log(chalk.dim(' git add .'));
250 - console.log(
251 - chalk.dim(
252 - ` git commit -m "React DevTools ${previousVersion} -> ${nextVersion}"`
253 - )
254 - );
255 -
256 - if (!DRY_RUN) {
257 - await exec(`
258 - git add .
259 - git commit -m "React DevTools ${previousVersion} -> ${nextVersion}"
260 - `);
261 - }
262 -
263 - console.log('');
264 - console.log(`Please push this commit before continuing:`);
265 - console.log(` ${chalk.bold.green('git push')}`);
266 -
267 - await confirmContinue();
268 -}
269 -
270 -async function confirmContinue() {
271 - console.log('');
272 -
273 - const {confirm} = await inquirer.prompt({
274 - name: 'confirm',
275 - type: 'confirm',
276 - message: 'Continue the release?',
277 - });
278 - if (!confirm) {
279 - process.exit(0);
280 - }
281 -
282 - console.log('');
283 -}
284 -
285 -async function downloadLatestReactBuild() {
286 - const releaseScriptPath = join(ROOT_PATH, 'scripts', 'release');
287 - const installPromise = exec('yarn install', {cwd: releaseScriptPath});
288 -
289 - await logger(installPromise, 'Installing release script dependencies.', {
290 - estimate: 5000,
291 - });
292 -
293 - console.log('');
294 -
295 - const {commit} = await inquirer.prompt([
296 - {
297 - type: 'input',
298 - name: 'commit',
299 - message: 'Which React version (commit) should be used?',
300 - default: 'main',
301 - },
302 - ]);
303 - console.log('');
304 -
305 - const downloadScriptPath = join(
306 - releaseScriptPath,
307 - 'download-experimental-build.js'
308 - );
309 - const downloadPromise = execRead(
310 - `"${downloadScriptPath}" --commit=${commit}`
311 - );
312 -
313 - const output = await logger(
314 - downloadPromise,
315 - 'Downloading React artifacts from CI.',
316 - {estimate: 15000}
317 - );
318 -
319 - const match = output.match('--build=([0-9]+)');
320 - if (match.length === 0) {
321 - console.error(`No build ID found in "${output}"`);
322 - process.exit(1);
323 - }
324 -
325 - const buildID = match[1];
326 -
327 - console.log('');
328 - console.log(`Downloaded artiacts for CI build ${chalk.bold(buildID)}.`);
329 -
330 - await confirmContinue();
331 -
332 - return buildID;
333 -}
334 -
335 -async function execRead(command, options) {
336 - const {stdout} = await exec(command, options);
337 -
338 - return stdout.trim();
339 -}
340 -
341 -async function getCommitLog(sha) {
342 - let formattedLog = '';
343 -
344 - const rawLog = await execRead(`
345 - git log --topo-order --pretty=format:'%s' ${sha}...HEAD -- packages/react-devtools*
346 - `);
347 - rawLog.split('\n').forEach(line => {
348 - line = line.replace('[DevTools] ', '');
349 -
350 - const match = line.match(/(.+) \(#([0-9]+)\)/);
351 - if (match !== null) {
352 - const title = match[1];
353 - const pr = match[2];
354 -
355 - formattedLog += `\n* ${title} ([USERNAME](https://github.com/USERNAME) in [#${pr}](${PULL_REQUEST_BASE_URL}${pr}))`;
356 - } else {
357 - formattedLog += `\n* ${line}`;
358 - }
359 - });
360 -
361 - return formattedLog;
362 -}
363 -
364 -async function getPreviousCommitSha() {
365 - const choices = [];
366 -
367 - const lines = await execRead(`
368 - git log --max-count=5 --topo-order --pretty=format:'%H:::%s:::%as' HEAD -- ${PACKAGE_PATHS[0]}
369 - `);
370 - lines.split('\n').forEach((line, index) => {
371 - const [hash, message, date] = line.split(':::');
372 - choices.push({
373 - name: `${chalk.bold(hash)} ${chalk.dim(date)} ${message}`,
374 - value: hash,
375 - short: date,
376 - });
377 - });
378 -
379 - const {sha} = await inquirer.prompt([
380 - {
381 - type: 'list',
382 - name: 'sha',
383 - message: 'Which of the commits above marks the last DevTools release?',
384 - choices,
385 - default: choices[0].value,
386 - },
387 - ]);
388 -
389 - return sha;
390 -}
391 -
392 -async function getReleaseType() {
393 - const {releaseType} = await inquirer.prompt([
394 - {
395 - type: 'list',
396 - name: 'releaseType',
397 - message: 'Which type of release is this?',
398 - choices: [
399 - {
400 - name: 'Minor (new user facing functionality)',
401 - value: 'minor',
402 - short: 'Minor',
403 - },
404 - {name: 'Patch (bug fixes only)', value: 'patch', short: 'Patch'},
405 - ],
406 - default: 'patch',
407 - },
408 - ]);
409 -
410 - return releaseType;
411 -}
412 -
413 -async function printFinalInstructions(buildID, archivePath) {
414 - console.log('');
415 - console.log(
416 - 'You are now ready to publish the extension to Chrome, Edge, and Firefox:'
417 - );
418 - console.log(
419 - ` ${chalk.blue.underline(
420 - 'https://fburl.com/publish-react-devtools-extensions'
421 - )}`
422 - );
423 - console.log('');
424 - console.log('When publishing to Firefox, remember the following:');
425 - console.log(` Build id: ${chalk.bold(buildID)}`);
426 - console.log(` Git archive: ${chalk.bold(archivePath)}`);
427 - console.log('');
428 - console.log('Also consider syncing this release to Facebook:');
429 - console.log(` ${chalk.bold.green('js1 upgrade react-devtools')}`);
430 -}
431 -
432 -async function publishToNPM() {
433 - const {otp} = await inquirer.prompt([
434 - {
435 - type: 'input',
436 - name: 'otp',
437 - message: 'Please provide an NPM two-factor auth token:',
438 - },
439 - ]);
440 -
441 - console.log('');
442 -
443 - if (!otp) {
444 - console.error(`Invalid OTP provided: "${chalk.bold(otp)}"`);
445 - process.exit(0);
446 - }
447 -
448 - for (let index = 0; index < NPM_PACKAGES.length; index++) {
449 - const npmPackage = NPM_PACKAGES[index];
450 - const packagePath = join(ROOT_PATH, 'packages', npmPackage);
451 -
452 - if (DRY_RUN) {
453 - console.log(`Publishing package ${chalk.bold(npmPackage)}`);
454 - console.log(chalk.dim(` npm publish --otp=${otp}`));
455 - } else {
456 - const publishPromise = exec(`npm publish --otp=${otp}`, {
457 - cwd: packagePath,
458 - });
459 -
460 - await logger(
461 - publishPromise,
462 - `Publishing package ${chalk.bold(npmPackage)}`,
463 - {
464 - estimate: 2500,
465 - }
466 - );
467 - }
468 - }
469 -}
470 -
471 -async function reviewChangelogPrompt() {
472 - console.log('');
473 - console.log(
474 - 'The changelog has been updated with commits since the previous release:'
475 - );
476 - console.log(` ${chalk.bold(CHANGELOG_PATH)}`);
477 - console.log('');
478 - console.log('Please review the new changelog text for the following:');
479 - console.log(' 1. Organize the list into Features vs Bugfixes');
480 - console.log(' 1. Filter out any non-user-visible changes (e.g. typo fixes)');
481 - console.log(' 1. Combine related PRs into a single bullet list.');
482 - console.log(
483 - ' 1. Replacing the "USERNAME" placeholder text with the GitHub username(s)'
484 - );
485 - console.log('');
486 - console.log(` ${chalk.bold.green(`open ${CHANGELOG_PATH}`)}`);
487 -
488 - await confirmContinue();
489 -}
490 -
491 -function updateChangelog(nextVersion, commitLog) {
492 - const path = join(ROOT_PATH, CHANGELOG_PATH);
493 - const oldChangelog = readFileSync(path, 'utf8');
494 -
495 - const [beginning, end] = oldChangelog.split(RELEASE_SCRIPT_TOKEN);
496 -
497 - const dateString = new Date().toLocaleDateString('en-us', {
498 - year: 'numeric',
499 - month: 'long',
500 - day: 'numeric',
501 - });
502 - const header = `## ${nextVersion} (${dateString})`;
503 -
504 - const newChangelog = `${beginning}${RELEASE_SCRIPT_TOKEN}\n\n${header}\n${commitLog}${end}`;
505 -
506 - console.log(chalk.dim(' Updating changelog: ' + CHANGELOG_PATH));
507 -
508 - if (!DRY_RUN) {
509 - writeFileSync(path, newChangelog);
510 - }
511 -}
512 -
513 -function updateManifestVersions(previousVersion, nextVersion) {
514 - MANIFEST_PATHS.forEach(partialPath => {
515 - const path = join(ROOT_PATH, partialPath);
516 - const json = readJsonSync(path);
517 - json.version = nextVersion;
518 -
519 - if (json.hasOwnProperty('version_name')) {
520 - json.version_name = nextVersion;
521 - }
522 -
523 - console.log(chalk.dim(' Updating manifest JSON: ' + partialPath));
524 -
525 - if (!DRY_RUN) {
526 - writeJsonSync(path, json, {spaces: 2});
527 - }
528 - });
529 -}
530 -
531 -function updatePackageVersions(previousVersion, nextVersion) {
532 - PACKAGE_PATHS.forEach(partialPath => {
533 - const path = join(ROOT_PATH, partialPath);
534 - const json = readJsonSync(path);
535 - json.version = nextVersion;
536 -
537 - for (let key in json.dependencies) {
538 - if (key.startsWith('react-devtools')) {
539 - const version = json.dependencies[key];
540 -
541 - json.dependencies[key] = version.replace(previousVersion, nextVersion);
542 - }
543 - }
544 -
545 - console.log(chalk.dim(' Updating package JSON: ' + partialPath));
546 -
547 - if (!DRY_RUN) {
548 - writeJsonSync(path, json, {spaces: 2});
549 - }
550 - });
551 -}
552 -
553 -main();
scripts/devtools/utils.js new
+128
@@ -0,0 +1,128 @@
1 +'use strict';
2 +
3 +const chalk = require('chalk');
4 +const {exec} = require('child-process-promise');
5 +const {existsSync, mkdirSync} = require('fs');
6 +const {readJsonSync, writeJsonSync} = require('fs-extra');
7 +const inquirer = require('inquirer');
8 +const {join} = require('path');
9 +const createLogger = require('progress-estimator');
10 +const {
11 + BUILD_METADATA_TEMP_DIRECTORY,
12 + NPM_PACKAGES,
13 +} = require('./configuration');
14 +
15 +const logger = createLogger({
16 + storagePath: join(__dirname, '.progress-estimator'),
17 +});
18 +
19 +async function checkNPMPermissions() {
20 + const currentUser = await execRead('npm whoami');
21 + const failedProjects = [];
22 +
23 + const checkProject = async project => {
24 + const owners = (await execRead(`npm owner ls ${project}`))
25 + .split('\n')
26 + .filter(owner => owner)
27 + .map(owner => owner.split(' ')[0]);
28 +
29 + if (!owners.includes(currentUser)) {
30 + failedProjects.push(project);
31 + }
32 + };
33 +
34 + await logger(
35 + Promise.all(NPM_PACKAGES.map(checkProject)),
36 + `Checking NPM permissions for ${chalk.bold(currentUser)}.`,
37 + {estimate: 2500}
38 + );
39 +
40 + console.log('');
41 +
42 + if (failedProjects.length) {
43 + console.error(chalk.red.bold('Insufficient NPM permissions'));
44 + console.error('');
45 + console.error(
46 + chalk.red(
47 + `NPM user {underline ${currentUser}} is not an owner for: ${chalk.bold(
48 + failedProjects.join(', ')
49 + )}`
50 + )
51 + );
52 + console.error(
53 + chalk.red(
54 + 'Please contact a React team member to be added to the above project(s).'
55 + )
56 + );
57 + process.exit(1);
58 + }
59 +}
60 +
61 +function clear() {
62 + console.clear();
63 +}
64 +
65 +async function confirm(message, exitFunction) {
66 + console.log('');
67 +
68 + const {confirmation} = await inquirer.prompt({
69 + name: 'confirmation',
70 + type: 'confirm',
71 + message,
72 + });
73 +
74 + console.log('');
75 +
76 + if (!confirmation) {
77 + if (typeof exitFunction === 'function') {
78 + exitFunction();
79 + }
80 +
81 + process.exit(0);
82 + }
83 +}
84 +
85 +async function confirmContinue(exitFunction) {
86 + await confirm('Continue the release?', exitFunction);
87 +}
88 +
89 +async function execRead(command, options) {
90 + const {stdout} = await exec(command, options);
91 +
92 + return stdout.trim();
93 +}
94 +
95 +function readSavedBuildMetadata() {
96 + const path = join(BUILD_METADATA_TEMP_DIRECTORY, 'metadata');
97 +
98 + if (!existsSync(path)) {
99 + console.error(chalk.red('Expected to find build metadata at:'));
100 + console.error(chalk.dim(` ${path}`));
101 + process.exit(1);
102 + }
103 +
104 + const {archivePath, buildID} = readJsonSync(path);
105 +
106 + return {archivePath, buildID};
107 +}
108 +
109 +function saveBuildMetadata({archivePath, buildID}) {
110 + const path = join(BUILD_METADATA_TEMP_DIRECTORY, 'metadata');
111 +
112 + if (!existsSync(BUILD_METADATA_TEMP_DIRECTORY)) {
113 + mkdirSync(BUILD_METADATA_TEMP_DIRECTORY);
114 + }
115 +
116 + writeJsonSync(path, {archivePath, buildID}, {spaces: 2});
117 +}
118 +
119 +module.exports = {
120 + checkNPMPermissions,
121 + clear,
122 + confirm,
123 + confirmContinue,
124 + execRead,
125 + logger,
126 + readSavedBuildMetadata,
127 + saveBuildMetadata,
128 +};