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