scripts/devtools/prepare-release.js (218 lines of code) (raw):

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