main
js 286 lines 7.43 KB
Raw
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 sha = await getPreviousCommitSha();
35 const [shortCommitLog, formattedCommitLog] = await getCommitLog(sha);
36
37 console.log('');
38 console.log(
39 'This release includes the following commits:',
40 chalk.gray(shortCommitLog)
41 );
42 console.log('');
43
44 const releaseType = await getReleaseType();
45
46 const path = join(ROOT_PATH, PACKAGE_PATHS[0]);
47 const previousVersion = readJsonSync(path).version;
48 const {major, minor, patch} = semver(previousVersion);
49 const nextVersion =
50 releaseType === 'minor'
51 ? `${major}.${minor + 1}.0`
52 : `${major}.${minor}.${patch + 1}`;
53
54 updateChangelog(nextVersion, formattedCommitLog);
55
56 await reviewChangelogPrompt();
57
58 updatePackageVersions(previousVersion, nextVersion);
59 updateManifestVersions(previousVersion, nextVersion);
60
61 console.log('');
62 console.log(
63 `Packages and manifests have been updated from version ${chalk.bold(
64 previousVersion
65 )} to ${chalk.bold(nextVersion)}`
66 );
67 console.log('');
68
69 await commitPendingChanges(previousVersion, nextVersion);
70
71 printFinalInstructions();
72 }
73
74 async function commitPendingChanges(previousVersion, nextVersion) {
75 console.log('');
76 console.log('Committing revision and changelog.');
77 console.log(chalk.dim(' git add .'));
78 console.log(
79 chalk.dim(
80 ` git commit -m "React DevTools ${previousVersion} -> ${nextVersion}"`
81 )
82 );
83
84 if (!DRY_RUN) {
85 await exec(`
86 git add .
87 git commit -m "React DevTools ${previousVersion} -> ${nextVersion}"
88 `);
89 }
90
91 console.log('');
92 console.log(`Please push this commit before continuing:`);
93 console.log(` ${chalk.bold.green('git push')}`);
94
95 await confirmContinue();
96 }
97
98 async function getCommitLog(sha) {
99 let shortLog = '';
100 let formattedLog = '';
101
102 const hasGh = await hasGithubCLI();
103 const rawLog = await execRead(`
104 git log --topo-order --pretty=format:'%s' ${sha}...HEAD -- packages/react-devtools*
105 `);
106 const lines = rawLog.split('\n');
107 for (let i = 0; i < lines.length; i++) {
108 const line = lines[i].replace(/^\[devtools\] */i, '');
109 const match = line.match(/(.+) \(#([0-9]+)\)/);
110 if (match !== null) {
111 const title = match[1];
112 const pr = match[2];
113 let username;
114 if (hasGh) {
115 const response = await execRead(
116 `gh api /repos/facebook/react/pulls/${pr}`
117 );
118 const {user} = JSON.parse(response);
119 username = `[${user.login}](${user.html_url})`;
120 } else {
121 username = '[USERNAME](https://github.com/USERNAME)';
122 }
123 formattedLog += `\n* ${title} (${username} in [#${pr}](${PULL_REQUEST_BASE_URL}${pr}))`;
124 shortLog += `\n* ${title}`;
125 } else {
126 formattedLog += `\n* ${line}`;
127 shortLog += `\n* ${line}`;
128 }
129 }
130
131 return [shortLog, formattedLog];
132 }
133
134 async function hasGithubCLI() {
135 try {
136 await exec('which gh');
137 return true;
138 } catch (_) {}
139 return false;
140 }
141
142 async function getPreviousCommitSha() {
143 const choices = [];
144
145 const lines = await execRead(`
146 git log --max-count=5 --topo-order --pretty=format:'%H:::%s:::%as' HEAD -- ${join(
147 ROOT_PATH,
148 PACKAGE_PATHS[0]
149 )}
150 `);
151
152 lines.split('\n').forEach((line, index) => {
153 const [hash, message, date] = line.split(':::');
154 choices.push({
155 name: `${chalk.bold(hash)} ${chalk.dim(date)} ${message}`,
156 value: hash,
157 short: date,
158 });
159 });
160
161 const {sha} = await inquirer.prompt([
162 {
163 type: 'list',
164 name: 'sha',
165 message: 'Which of the commits above marks the last DevTools release?',
166 choices,
167 default: choices[0].value,
168 },
169 ]);
170
171 return sha;
172 }
173
174 async function getReleaseType() {
175 const {releaseType} = await inquirer.prompt([
176 {
177 type: 'list',
178 name: 'releaseType',
179 message: 'Which type of release is this?',
180 choices: [
181 {
182 name: 'Minor (new user facing functionality)',
183 value: 'minor',
184 short: 'Minor',
185 },
186 {name: 'Patch (bug fixes only)', value: 'patch', short: 'Patch'},
187 ],
188 default: 'patch',
189 },
190 ]);
191
192 return releaseType;
193 }
194
195 function printFinalInstructions() {
196 const buildAndTestcriptPath = join(__dirname, 'build-and-test.js');
197 const pathToPrint = relative(process.cwd(), buildAndTestcriptPath);
198
199 console.log('');
200 console.log('Continue by running the build-and-test script:');
201 console.log(chalk.bold.green(' ' + pathToPrint));
202 }
203
204 async function reviewChangelogPrompt() {
205 console.log('');
206 console.log(
207 'The changelog has been updated with commits since the previous release:'
208 );
209 console.log(` ${chalk.bold(CHANGELOG_PATH)}`);
210 console.log('');
211 console.log('Please review the new changelog text for the following:');
212 console.log(' 1. Filter out any non-user-visible changes (e.g. typo fixes)');
213 console.log(' 2. Organize the list into Features vs Bugfixes');
214 console.log(' 3. Combine related PRs into a single bullet list');
215 console.log(
216 ' 4. Replacing the "USERNAME" placeholder text with the GitHub username(s)'
217 );
218 console.log('');
219 console.log(` ${chalk.bold.green(`open ${CHANGELOG_PATH}`)}`);
220
221 await confirmContinue();
222 }
223
224 function updateChangelog(nextVersion, commitLog) {
225 const path = join(ROOT_PATH, CHANGELOG_PATH);
226 const oldChangelog = readFileSync(path, 'utf8');
227
228 const [beginning, end] = oldChangelog.split(RELEASE_SCRIPT_TOKEN);
229
230 const dateString = new Date().toLocaleDateString('en-us', {
231 year: 'numeric',
232 month: 'long',
233 day: 'numeric',
234 });
235 const header = `---\n\n### ${nextVersion}\n${dateString}`;
236
237 const newChangelog = `${beginning}${RELEASE_SCRIPT_TOKEN}\n\n${header}\n${commitLog}${end}`;
238
239 console.log(chalk.dim(' Updating changelog: ' + CHANGELOG_PATH));
240
241 if (!DRY_RUN) {
242 writeFileSync(path, newChangelog);
243 }
244 }
245
246 function updateManifestVersions(previousVersion, nextVersion) {
247 MANIFEST_PATHS.forEach(partialPath => {
248 const path = join(ROOT_PATH, partialPath);
249 const json = readJsonSync(path);
250 json.version = nextVersion;
251
252 if (json.hasOwnProperty('version_name')) {
253 json.version_name = nextVersion;
254 }
255
256 console.log(chalk.dim(' Updating manifest JSON: ' + partialPath));
257
258 if (!DRY_RUN) {
259 writeJsonSync(path, json, {spaces: 2});
260 }
261 });
262 }
263
264 function updatePackageVersions(previousVersion, nextVersion) {
265 PACKAGE_PATHS.forEach(partialPath => {
266 const path = join(ROOT_PATH, partialPath);
267 const json = readJsonSync(path);
268 json.version = nextVersion;
269
270 for (let key in json.dependencies) {
271 if (key.startsWith('react-devtools')) {
272 const version = json.dependencies[key];
273
274 json.dependencies[key] = version.replace(previousVersion, nextVersion);
275 }
276 }
277
278 console.log(chalk.dim(' Updating package JSON: ' + partialPath));
279
280 if (!DRY_RUN) {
281 writeJsonSync(path, json, {spaces: 2});
282 }
283 });
284 }
285
286 main();