electron/script/release/prepare-release.js

232 lines
7.9 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
2020-03-20 20:28:31 +00:00
if (!process.env.CI) require('dotenv-safe').load();
const args = require('minimist')(process.argv.slice(2), {
boolean: ['automaticRelease', 'notesOnly', 'stable']
2020-03-20 20:28:31 +00:00
});
const ciReleaseBuild = require('./ci-release-build');
const { Octokit } = require('@octokit/rest');
const { execSync } = require('node:child_process');
2020-03-20 20:28:31 +00:00
const { GitProcess } = require('dugite');
const path = require('node:path');
const readline = require('node:readline');
2020-03-20 20:28:31 +00:00
const releaseNotesGenerator = require('./notes/index.js');
const { getCurrentBranch, ELECTRON_DIR } = require('../lib/utils.js');
const bumpType = args._[0];
const targetRepo = getRepo();
function getRepo () {
return bumpType === 'nightly' ? 'nightlies' : 'electron';
}
const octokit = new Octokit({
auth: process.env.ELECTRON_GITHUB_TOKEN
});
2020-03-20 20:28:31 +00:00
require('colors');
const pass = '✓'.green;
const fail = '✗'.red;
if (!bumpType && !args.notesOnly) {
console.log('Usage: prepare-release [stable | minor | beta | alpha | nightly]' +
2020-03-20 20:28:31 +00:00
' (--stable) (--notesOnly) (--automaticRelease) (--branch)');
process.exit(1);
}
async function getNewVersion (dryRun) {
if (!dryRun) {
2020-03-20 20:28:31 +00:00
console.log(`Bumping for new "${bumpType}" version.`);
}
2020-03-20 20:28:31 +00:00
const bumpScript = path.join(__dirname, 'version-bumper.js');
const scriptArgs = ['node', bumpScript, `--bump=${bumpType}`];
if (dryRun) scriptArgs.push('--dryRun');
try {
2020-03-20 20:28:31 +00:00
let bumpVersion = execSync(scriptArgs.join(' '), { encoding: 'UTF-8' });
bumpVersion = bumpVersion.substr(bumpVersion.indexOf(':') + 1).trim();
const newVersion = `v${bumpVersion}`;
if (!dryRun) {
2020-03-20 20:28:31 +00:00
console.log(`${pass} Successfully bumped version to ${newVersion}`);
}
2020-03-20 20:28:31 +00:00
return newVersion;
} catch (err) {
2020-03-20 20:28:31 +00:00
console.log(`${fail} Could not bump version, error was:`, err);
throw err;
}
}
async function getReleaseNotes (currentBranch, newVersion) {
if (bumpType === 'nightly') {
2020-03-20 20:28:31 +00:00
return { text: 'Nightlies do not get release notes, please compare tags for info.' };
}
2020-03-20 20:28:31 +00:00
console.log(`Generating release notes for ${currentBranch}.`);
const releaseNotes = await releaseNotesGenerator(currentBranch, newVersion);
better release notes (#15169) * fix: use PR 'Notes' comment in release notes * fix: follow links in roller-bot PRs * refactor: better reference point version selection * if we're a stable release, use the current brnach's previous stable * if we're a beta release, use the current branch's previous beta * if no match found, use the newest stable that precedes this branch * refactor: dedup the caching functions' code * refactor: partially rewrite release note generator * parse release notes comments from PRs * do not display no-notes PRs * handle roller-bot commits by following cross-repo commits/PRs * minor tweaks to note rendering, e.g. capitalization * fix: fix lint:js script typo * fix: copy originalPr value to rollerbot PR chains * fix: handle more cases in release notes generator * handle force-pushes where no PR * better type guessing on pre-semantic commits * fix: handle more edge cases in the note generator * better removal of commits that landed before the reference point * ensure '<!-- One-line Change Summary Here-->' is removed from notes * handle more legacy commit body notes e.g. "Chore(docs)" * check for fix markdown in PR body e.g. a link to the issue page * chore: tweak code comments * refactor: easier note generator command-line args * refactor: group related notes together * feat: query commits locally for gyp and gn deps * chore: slightly better filtering of old commits * feat: omit submodule commits for .0.0 releases More specifically, only include them if generating release notes relative to another release on the same branch. Before that first release, there's just too much churn. * refactor: make release-notes usable as a module Calling it from the command line and from require()() now do pretty much the same thing. * refactor: passing command-line args means use HEAD * chore: plug in the release note generator * feat: support multiline 'Notes:' messages. xref: https://github.com/electron/trop/pull/56 xref: https://github.com/electron/clerk/pull/16 * remove accidental change in package.json * simplify an overcomplicated require() call * Don't use PascalCase on releaseNotesGenerator() * Remove code duplication in release notes warnings * remove commented-out code. * don't use single-character variable names. For example, use 'tag' instead of 't'. The latter was being used for map/filter arrow function args. * Look for 'backport' rather than 'ackport'. * Wrap all block statements in curly braces. * fix tyop * fix oops * Check semver validity before calling semver.sort()
2018-11-06 20:06:11 +00:00
if (releaseNotes.warning) {
2020-03-20 20:28:31 +00:00
console.warn(releaseNotes.warning);
}
2020-03-20 20:28:31 +00:00
return releaseNotes;
}
async function createRelease (branchToTarget, isBeta) {
2020-03-20 20:28:31 +00:00
const newVersion = await getNewVersion();
const releaseNotes = await getReleaseNotes(branchToTarget, newVersion);
await tagRelease(newVersion);
2020-03-20 20:28:31 +00:00
console.log('Checking for existing draft release.');
const releases = await octokit.repos.listReleases({
owner: 'electron',
repo: targetRepo
}).catch(err => {
2020-03-20 20:28:31 +00:00
console.log(`${fail} Could not get releases. Error was: `, err);
});
const drafts = releases.data.filter(release => release.draft &&
2020-03-20 20:28:31 +00:00
release.tag_name === newVersion);
if (drafts.length > 0) {
console.log(`${fail} Aborting because draft release for
2020-03-20 20:28:31 +00:00
${drafts[0].tag_name} already exists.`);
process.exit(1);
}
2020-03-20 20:28:31 +00:00
console.log(`${pass} A draft release does not exist; creating one.`);
2020-03-20 20:28:31 +00:00
let releaseBody;
let releaseIsPrelease = false;
if (isBeta) {
if (newVersion.indexOf('nightly') > 0) {
releaseBody = 'Note: This is a nightly release. Please file new issues ' +
'for any bugs you find in it.\n \n This release is published to npm ' +
'under the electron-nightly package and can be installed via `npm install electron-nightly`, ' +
`or \`npm install electron-nightly@${newVersion.substr(1)}\`.\n \n ${releaseNotes.text}`;
} else if (newVersion.indexOf('alpha') > 0) {
releaseBody = 'Note: This is an alpha release. Please file new issues ' +
'for any bugs you find in it.\n \n This release is published to npm ' +
'under the alpha tag and can be installed via `npm install electron@alpha`, ' +
`or \`npm install electron@${newVersion.substr(1)}\`.\n \n ${releaseNotes.text}`;
} else {
releaseBody = 'Note: This is a beta release. Please file new issues ' +
'for any bugs you find in it.\n \n This release is published to npm ' +
'under the beta tag and can be installed via `npm install electron@beta`, ' +
`or \`npm install electron@${newVersion.substr(1)}\`.\n \n ${releaseNotes.text}`;
}
2020-03-20 20:28:31 +00:00
releaseIsPrelease = true;
} else {
2020-03-20 20:28:31 +00:00
releaseBody = releaseNotes.text;
}
const release = await octokit.repos.createRelease({
owner: 'electron',
repo: targetRepo,
tag_name: newVersion,
draft: true,
name: `electron ${newVersion}`,
body: releaseBody,
prerelease: releaseIsPrelease,
target_commitish: newVersion.includes('nightly') ? 'main' : branchToTarget
}).catch(err => {
2020-03-20 20:28:31 +00:00
console.log(`${fail} Error creating new release: `, err);
process.exit(1);
});
2020-03-20 20:28:31 +00:00
console.log(`Release has been created with id: ${release.data.id}.`);
console.log(`${pass} Draft release for ${newVersion} successful.`);
}
async function pushRelease (branch) {
2020-03-20 20:28:31 +00:00
const pushDetails = await GitProcess.exec(['push', 'origin', `HEAD:${branch}`, '--follow-tags'], ELECTRON_DIR);
if (pushDetails.exitCode === 0) {
console.log(`${pass} Successfully pushed the release. Wait for ` +
2020-03-20 20:28:31 +00:00
'release builds to finish before running "npm run release".');
} else {
2020-03-20 20:28:31 +00:00
console.log(`${fail} Error pushing the release: ${pushDetails.stderr}`);
process.exit(1);
}
}
async function runReleaseBuilds (branch, newVersion) {
await ciReleaseBuild(branch, {
ghRelease: true,
newVersion
2020-03-20 20:28:31 +00:00
});
}
async function tagRelease (version) {
2020-03-20 20:28:31 +00:00
console.log(`Tagging release ${version}.`);
const checkoutDetails = await GitProcess.exec(['tag', '-a', '-m', version, version], ELECTRON_DIR);
if (checkoutDetails.exitCode === 0) {
2020-03-20 20:28:31 +00:00
console.log(`${pass} Successfully tagged ${version}.`);
} else {
console.log(`${fail} Error tagging ${version}: ` +
2020-03-20 20:28:31 +00:00
`${checkoutDetails.stderr}`);
process.exit(1);
}
}
async function verifyNewVersion () {
2020-03-20 20:28:31 +00:00
const newVersion = await getNewVersion(true);
let response;
if (args.automaticRelease) {
2020-03-20 20:28:31 +00:00
response = 'y';
} else {
2020-03-20 20:28:31 +00:00
response = await promptForVersion(newVersion);
}
if (response.match(/^y/i)) {
2020-03-20 20:28:31 +00:00
console.log(`${pass} Starting release of ${newVersion}`);
} else {
2020-03-20 20:28:31 +00:00
console.log(`${fail} Aborting release of ${newVersion}`);
process.exit();
}
return newVersion;
}
async function promptForVersion (version) {
return new Promise(resolve => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
2020-03-20 20:28:31 +00:00
});
rl.question(`Do you want to create the release ${version.green} (y/N)? `, (answer) => {
2020-03-20 20:28:31 +00:00
rl.close();
resolve(answer);
});
});
}
// function to determine if there have been commits to main since the last release
async function changesToRelease () {
const lastCommitWasRelease = /^Bump v[0-9]+.[0-9]+.[0-9]+(-beta.[0-9]+)?(-alpha.[0-9]+)?(-nightly.[0-9]+)?$/g;
2020-03-20 20:28:31 +00:00
const lastCommit = await GitProcess.exec(['log', '-n', '1', '--pretty=format:\'%s\''], ELECTRON_DIR);
return !lastCommitWasRelease.test(lastCommit.stdout);
}
async function prepareRelease (isBeta, notesOnly) {
if (args.dryRun) {
2020-03-20 20:28:31 +00:00
const newVersion = await getNewVersion(true);
console.log(newVersion);
} else {
2020-03-20 20:28:31 +00:00
const currentBranch = (args.branch) ? args.branch : await getCurrentBranch(ELECTRON_DIR);
if (notesOnly) {
2020-03-20 20:28:31 +00:00
const newVersion = await getNewVersion(true);
const releaseNotes = await getReleaseNotes(currentBranch, newVersion);
console.log(`Draft release notes are: \n${releaseNotes.text}`);
} else {
const changes = await changesToRelease();
if (changes) {
const newVersion = await verifyNewVersion();
2020-03-20 20:28:31 +00:00
await createRelease(currentBranch, isBeta);
await pushRelease(currentBranch);
await runReleaseBuilds(currentBranch, newVersion);
} else {
2020-03-20 20:28:31 +00:00
console.log('There are no new changes to this branch since the last release, aborting release.');
process.exit(1);
}
}
}
}
prepareRelease(!args.stable, args.notesOnly)
.catch((err) => {
console.error(err);
process.exit(1);
});