diff --git a/components/git/release.js b/components/git/release.js index c74ba5ab..ae82e0ae 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -78,6 +78,8 @@ async function main(state, argv, cli, dir) { if (state === PREPARE) { const prep = new ReleasePreparation(argv, cli, dir); + await prep.prepareLocalBranch(); + if (prep.warnForWrongBranch()) return; // If the new version was automatically calculated, confirm it. diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 5b64d79f..9f3c3866 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -4,7 +4,6 @@ import { promises as fs } from 'node:fs'; import semver from 'semver'; import { replaceInFile } from 'replace-in-file'; -import { getMergedConfig } from './config.js'; import { runAsync, runSync } from './run.js'; import { writeJson, readJson } from './file.js'; import Request from './request.js'; @@ -15,58 +14,25 @@ import { updateTestProcessRelease } from './release/utils.js'; import CherryPick from './cherry_pick.js'; +import Session from './session.js'; const isWindows = process.platform === 'win32'; -export default class ReleasePreparation { +export default class ReleasePreparation extends Session { constructor(argv, cli, dir) { - this.cli = cli; - this.dir = dir; + super(cli, dir); this.isSecurityRelease = argv.security; this.isLTS = false; this.isLTSTransition = argv.startLTS; this.runBranchDiff = !argv.skipBranchDiff; this.ltsCodename = ''; this.date = ''; - this.config = getMergedConfig(this.dir); this.filterLabels = argv.filterLabel && argv.filterLabel.split(','); + this.newVersion = argv.newVersion; + } - // Ensure the preparer has set an upstream and username. - if (this.warnForMissing()) { - cli.error('Failed to begin the release preparation process.'); - return; - } - - // Allow passing optional new version. - if (argv.newVersion) { - const newVersion = semver.clean(argv.newVersion); - if (!semver.valid(newVersion)) { - cli.warn(`${newVersion} is not a valid semantic version.`); - return; - } - this.newVersion = newVersion; - } else { - this.newVersion = this.calculateNewVersion(); - } - - const { upstream, owner, repo, newVersion } = this; - - this.versionComponents = { - major: semver.major(newVersion), - minor: semver.minor(newVersion), - patch: semver.patch(newVersion) - }; - - this.stagingBranch = `v${this.versionComponents.major}.x-staging`; - this.releaseBranch = `v${this.versionComponents.major}.x`; - - const upstreamHref = runSync('git', [ - 'config', '--get', - `remote.${upstream}.url`]).trim(); - if (!new RegExp(`${owner}/${repo}(?:.git)?$`).test(upstreamHref)) { - cli.warn('Remote repository URL does not point to the expected ' + - `repository ${owner}/${repo}`); - } + get branch() { + return this.stagingBranch; } warnForNonMergeablePR(pr) { @@ -369,24 +335,29 @@ export default class ReleasePreparation { return missing; } - calculateNewVersion() { - let newVersion; + async calculateNewVersion(major) { + const { cli } = this; - const lastTagVersion = semver.clean(this.getLastRef()); - const lastTag = { - major: semver.major(lastTagVersion), - minor: semver.minor(lastTagVersion), - patch: semver.patch(lastTagVersion) - }; + 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); - const changelog = this.getChangelog(); + cli.stopSpinner(`Latest release on ${major}.x line is ${major}.${minor}.${patch}`); + const changelog = this.getChangelog(`v${major}.${minor}.${patch}`); + const newVersion = { major, minor, patch }; if (changelog.includes('SEMVER-MAJOR')) { - newVersion = `${lastTag.major + 1}.0.0`; + newVersion.major++; + newVersion.minor = 0; + newVersion.patch = 0; } else if (changelog.includes('SEMVER-MINOR') || this.isLTSTransition) { - newVersion = `${lastTag.major}.${lastTag.minor + 1}.0`; + newVersion.minor++; + newVersion.patch = 0; } else { - newVersion = `${lastTag.major}.${lastTag.minor}.${lastTag.patch + 1}`; + newVersion.patch++; } return newVersion; @@ -396,11 +367,22 @@ export default class ReleasePreparation { return runSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).trim(); } - getLastRef() { - return runSync('git', ['describe', '--abbrev=0', '--tags']).trim(); + getLastRef(tagName) { + if (!tagName) { + return runSync('git', ['describe', '--abbrev=0', '--tags']).trim(); + } + + try { + runSync('git', ['rev-parse', tagName]); + } catch { + this.cli.startSpinner(`Error parsing git ref ${tagName}, attempting fetching it as a tag`); + runSync('git', ['fetch', this.upstream, 'tag', '-n', tagName]); + this.cli.stopSpinner(`Tag fetched: ${tagName}`); + } + return tagName; } - getChangelog() { + getChangelog(tagName) { const changelogMaker = new URL( '../node_modules/.bin/changelog-maker' + (isWindows ? '.cmd' : ''), import.meta.url @@ -411,7 +393,7 @@ export default class ReleasePreparation { '--markdown', '--filter-release', '--start-ref', - this.getLastRef() + this.getLastRef(tagName) ]).trim(); } @@ -736,6 +718,45 @@ export default class ReleasePreparation { return runSync(branchDiff, branchDiffOptions); } + async prepareLocalBranch() { + const { cli } = this; + if (this.newVersion) { + // If the CLI asked for a specific version: + const newVersion = semver.parse(this.newVersion); + if (!newVersion) { + cli.warn(`${this.newVersion} is not a valid semantic version.`); + return; + } + this.newVersion = newVersion.version; + this.versionComponents = { + major: newVersion.major, + minor: newVersion.minor, + patch: newVersion.patch + }; + this.stagingBranch = `v${newVersion.major}.x-staging`; + this.releaseBranch = `v${newVersion.major}.x`; + await this.tryResetBranch(); + return; + } + + // Otherwise, we need to figure out what's the next version number for the + // release line of the branch that's currently checked out. + const currentBranch = this.getCurrentBranch(); + const match = /^v(\d+)\.x-staging$/.exec(currentBranch); + + if (!match) { + cli.warn(`Cannot prepare a release from ${currentBranch + }. Switch to a staging branch before proceeding.`); + return; + } + this.stagingBranch = currentBranch; + await this.tryResetBranch(); + this.versionComponents = await this.calculateNewVersion(match[1]); + const { major, minor, patch } = this.versionComponents; + this.newVersion = `${major}.${minor}.${patch}`; + this.releaseBranch = `v${major}.x`; + } + warnForWrongBranch() { const { cli,