From a8529edaab765e293965054b7027774a3f4938f2 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 28 Aug 2024 11:00:38 +0200 Subject: [PATCH] feat(git-node): auto-fetch comparison branch when preparing release (#846) --- lib/prepare_release.js | 93 ++++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 27 deletions(-) diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 9f3c3866..6b0aa131 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -4,7 +4,7 @@ import { promises as fs } from 'node:fs'; import semver from 'semver'; import { replaceInFile } from 'replace-in-file'; -import { runAsync, runSync } from './run.js'; +import { forceRunAsync, runAsync, runSync } from './run.js'; import { writeJson, readJson } from './file.js'; import Request from './request.js'; import auth from './auth.js'; @@ -171,7 +171,7 @@ export default class ReleasePreparation extends Session { // Check the branch diff to determine if the releaser // wants to backport any more commits before proceeding. cli.startSpinner('Fetching branch-diff'); - const raw = this.getBranchDiff({ + const raw = await this.getBranchDiff({ onlyNotableChanges: false, comparisonBranch: newVersion }); @@ -181,10 +181,9 @@ export default class ReleasePreparation extends Session { const outstandingCommits = diff.length - 1; if (outstandingCommits !== 0) { - const staging = `v${semver.major(newVersion)}.x-staging`; const proceed = await cli.prompt( `There are ${outstandingCommits} commits that may be ` + - `backported to ${staging} - do you still want to proceed?`, + `backported to ${this.stagingBranch} - do you still want to proceed?`, { defaultAnswer: false }); if (!proceed) { @@ -335,18 +334,8 @@ export default class ReleasePreparation extends Session { return missing; } - async calculateNewVersion(major) { - const { cli } = this; - - cli.startSpinner(`Parsing CHANGELOG for most recent release of v${major}.x`); - const data = await fs.readFile( - path.resolve(`doc/changelogs/CHANGELOG_V${major}.md`), - 'utf8' - ); - const [,, minor, patch] = /\1\.\2\.\3<\/a>/.exec(data); - - cli.stopSpinner(`Latest release on ${major}.x line is ${major}.${minor}.${patch}`); - const changelog = this.getChangelog(`v${major}.${minor}.${patch}`); + async calculateNewVersion({ tagName, major, minor, patch }) { + const changelog = this.getChangelog(tagName); const newVersion = { major, minor, patch }; if (changelog.includes('SEMVER-MAJOR')) { @@ -478,7 +467,7 @@ export default class ReleasePreparation extends Session { const data = await fs.readFile(majorChangelogPath, 'utf8'); const arr = data.split('\n'); const allCommits = this.getChangelog(); - const notableChanges = this.getBranchDiff({ onlyNotableChanges: true }); + const notableChanges = await this.getBranchDiff({ onlyNotableChanges: true }); let releaseHeader = `## ${date}, Version ${newVersion}` + ` ${releaseInfo}, @${username}\n`; if (isSecurityRelease) { @@ -532,14 +521,14 @@ export default class ReleasePreparation extends Session { } async createProposalBranch(base = this.stagingBranch) { - const { upstream, newVersion } = this; + const { newVersion } = this; const proposalBranch = `v${newVersion}-proposal`; await runAsync('git', [ 'checkout', '-b', proposalBranch, - `${upstream}/${base}` + base ]); return proposalBranch; } @@ -614,7 +603,7 @@ export default class ReleasePreparation extends Session { messageBody.push('This is a security release.\n\n'); } - const notableChanges = this.getBranchDiff({ + const notableChanges = await this.getBranchDiff({ onlyNotableChanges: true, format: 'plaintext' }); @@ -641,8 +630,9 @@ export default class ReleasePreparation extends Session { return useMessage; } - getBranchDiff(opts) { + async getBranchDiff(opts) { const { + cli, versionComponents = {}, upstream, newVersion, @@ -670,6 +660,10 @@ export default class ReleasePreparation extends Session { 'semver-minor' ]; + await forceRunAsync('git', ['remote', 'set-branches', '--add', upstream, releaseBranch], { + ignoreFailures: false + }); + await forceRunAsync('git', ['fetch', upstream, releaseBranch], { ignoreFailures: false }); branchDiffOptions = [ `${upstream}/${releaseBranch}`, proposalBranch, @@ -688,20 +682,43 @@ export default class ReleasePreparation extends Session { 'baking-for-lts' ]; - let comparisonBranch = 'main'; + let comparisonBranch = this.config.branch || 'main'; const isSemverMinor = versionComponents.patch === 0; if (isLTS) { + const res = await fetch('https://nodejs.org/dist/index.json'); + if (!res.ok) throw new Error('Failed to fetch', { cause: res }); + const [latest] = await res.json(); // Assume Current branch matches tag with highest semver value. - const tags = runSync('git', - ['tag', '-l', '--sort', '-version:refname']).trim(); - const highestVersionTag = tags.split('\n')[0]; - comparisonBranch = `v${semver.coerce(highestVersionTag).major}.x`; + comparisonBranch = `v${semver.coerce(latest.version).major}.x`; if (!isSemverMinor) { excludeLabels.push('semver-minor'); } } + await forceRunAsync('git', ['fetch', upstream, comparisonBranch], { ignoreFailures: false }); + const commits = await forceRunAsync('git', ['rev-parse', 'FETCH_HEAD', comparisonBranch], { + captureStdout: 'lines', + ignoreFailures: true + }); + if (commits == null) { + const shouldCreateCompareBranch = await cli.prompt( + `No local branch ${comparisonBranch}, do you want to create it?`); + if (shouldCreateCompareBranch) { + await forceRunAsync('git', ['branch', comparisonBranch, 'FETCH_HEAD'], { + ignoreFailures: false + }); + } + } else if (commits[0] !== commits[1]) { + const shouldUpBranch = cli.prompt(`Local ${comparisonBranch} branch is not in sync with ${ + upstream}/${comparisonBranch}, do you want to update it?`); + if (shouldUpBranch) { + await forceRunAsync('git', ['branch', '-f', comparisonBranch, 'FETCH_HEAD'], { + ignoreFailures: false + }); + } + } + branchDiffOptions = [ stagingBranch, comparisonBranch, @@ -718,6 +735,27 @@ export default class ReleasePreparation extends Session { return runSync(branchDiff, branchDiffOptions); } + async getLastRelease(major) { + const { cli } = this; + + cli.startSpinner(`Parsing CHANGELOG for most recent release of v${major}.x`); + const data = await fs.readFile( + path.resolve(`doc/changelogs/CHANGELOG_V${major}.md`), + 'utf8' + ); + const [,, minor, patch] = /\1\.\2\.\3<\/a>/.exec(data); + this.isLTS = data.includes('LTS '); + + cli.stopSpinner(`Latest release on ${major}.x line is ${major}.${minor}.${patch}${ + this.isLTS ? ' (LTS)' : '' + }`); + + return { + tagName: await this.getLastRef(`v${major}.${minor}.${patch}`), + major, minor, patch + }; + } + async prepareLocalBranch() { const { cli } = this; if (this.newVersion) { @@ -736,6 +774,7 @@ export default class ReleasePreparation extends Session { this.stagingBranch = `v${newVersion.major}.x-staging`; this.releaseBranch = `v${newVersion.major}.x`; await this.tryResetBranch(); + await this.getLastRelease(newVersion.major); return; } @@ -751,7 +790,7 @@ export default class ReleasePreparation extends Session { } this.stagingBranch = currentBranch; await this.tryResetBranch(); - this.versionComponents = await this.calculateNewVersion(match[1]); + this.versionComponents = await this.calculateNewVersion(await this.getLastRelease(match[1])); const { major, minor, patch } = this.versionComponents; this.newVersion = `${major}.${minor}.${patch}`; this.releaseBranch = `v${major}.x`;