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