import { Octokit } from '@octokit/rest'; import * as chalk from 'chalk'; import { GitProcess } from 'dugite'; import { execSync } from 'node:child_process'; import { join } from 'node:path'; import { createGitHubTokenStrategy } from './github-token'; import releaseNotesGenerator from './notes'; import { runReleaseCIJobs } from './run-release-ci-jobs'; import { ELECTRON_ORG, ElectronReleaseRepo, VersionBumpType } from './types'; import { getCurrentBranch, ELECTRON_DIR } from '../lib/utils.js'; const pass = chalk.green('✓'); const fail = chalk.red('✗'); enum DryRunMode { DRY_RUN, REAL_RUN, } type PrepareReleaseOptions = { targetRepo: ElectronReleaseRepo; targetBranch?: string; bumpType: VersionBumpType; isPreRelease: boolean; }; async function getNewVersion ( options: PrepareReleaseOptions, dryRunMode: DryRunMode ) { if (dryRunMode === DryRunMode.REAL_RUN) { console.log(`Bumping for new "${options.bumpType}" version.`); } const bumpScript = join(__dirname, 'version-bumper.ts'); const scriptArgs = [ 'node', 'node_modules/.bin/ts-node', bumpScript, `--bump=${options.bumpType}` ]; if (dryRunMode === DryRunMode.DRY_RUN) scriptArgs.push('--dryRun'); try { let bumpVersion = execSync(scriptArgs.join(' '), { encoding: 'utf-8' }); bumpVersion = bumpVersion.substr(bumpVersion.indexOf(':') + 1).trim(); const newVersion = `v${bumpVersion}`; if (dryRunMode === DryRunMode.REAL_RUN) { console.log(`${pass} Successfully bumped version to ${newVersion}`); } return newVersion; } catch (err) { console.log(`${fail} Could not bump version, error was:`, err); throw err; } } async function getReleaseNotes ( options: PrepareReleaseOptions, currentBranch: string, newVersion: string ) { if (options.bumpType === 'nightly') { return { text: 'Nightlies do not get release notes, please compare tags for info.' }; } console.log(`Generating release notes for ${currentBranch}.`); const releaseNotes = await releaseNotesGenerator(currentBranch, newVersion); if (releaseNotes.warning) { console.warn(releaseNotes.warning); } return releaseNotes; } async function createRelease ( options: PrepareReleaseOptions, branchToTarget: string ) { const newVersion = await getNewVersion(options, DryRunMode.REAL_RUN); const releaseNotes = await getReleaseNotes( options, branchToTarget, newVersion ); await tagRelease(newVersion); const octokit = new Octokit({ authStrategy: createGitHubTokenStrategy(options.targetRepo) }); console.log('Checking for existing draft release.'); const releases = await octokit.repos .listReleases({ owner: ELECTRON_ORG, repo: options.targetRepo }) .catch((err) => { console.log(`${fail} Could not get releases. Error was: `, err); throw err; }); const drafts = releases.data.filter( (release) => release.draft && release.tag_name === newVersion ); if (drafts.length > 0) { console.log(`${fail} Aborting because draft release for ${drafts[0].tag_name} already exists.`); process.exit(1); } console.log(`${pass} A draft release does not exist; creating one.`); let releaseBody; let releaseIsPrelease = false; if (options.isPreRelease) { 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 }`; } releaseIsPrelease = true; } else { releaseBody = releaseNotes.text; } const release = await octokit.repos .createRelease({ owner: ELECTRON_ORG, repo: options.targetRepo, tag_name: newVersion, draft: true, name: `electron ${newVersion}`, body: releaseBody, prerelease: releaseIsPrelease, target_commitish: newVersion.includes('nightly') ? 'main' : branchToTarget }) .catch((err) => { console.log(`${fail} Error creating new release: `, err); process.exit(1); }); console.log(`Release has been created with id: ${release.data.id}.`); console.log(`${pass} Draft release for ${newVersion} successful.`); } async function pushRelease (branch: string) { 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 ` + 'release builds to finish before running "npm run release".' ); } else { console.log(`${fail} Error pushing the release: ${pushDetails.stderr}`); process.exit(1); } } async function runReleaseBuilds (branch: string, newVersion: string) { await runReleaseCIJobs(branch, { ci: undefined, ghRelease: true, newVersion }); } async function tagRelease (version: string) { console.log(`Tagging release ${version}.`); const checkoutDetails = await GitProcess.exec( ['tag', '-a', '-m', version, version], ELECTRON_DIR ); if (checkoutDetails.exitCode === 0) { console.log(`${pass} Successfully tagged ${version}.`); } else { console.log( `${fail} Error tagging ${version}: ` + `${checkoutDetails.stderr}` ); process.exit(1); } } // 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; const lastCommit = await GitProcess.exec( ['log', '-n', '1', "--pretty=format:'%s'"], ELECTRON_DIR ); return !lastCommitWasRelease.test(lastCommit.stdout); } export async function printNextVersion (options: PrepareReleaseOptions) { const newVersion = await getNewVersion(options, DryRunMode.DRY_RUN); console.log(newVersion); } export async function prepareRelease (options: PrepareReleaseOptions) { const currentBranch = options.targetBranch || (await getCurrentBranch(ELECTRON_DIR)); const changes = await changesToRelease(); if (changes) { const newVersion = await getNewVersion(options, DryRunMode.DRY_RUN); console.log(`${pass} Starting release of ${newVersion}`); await createRelease(options, currentBranch); await pushRelease(currentBranch); await runReleaseBuilds(currentBranch, newVersion); } else { console.log( 'There are no new changes to this branch since the last release, aborting release.' ); process.exit(1); } }