From 32d4110fea2abd35abd5a406732e55a7fba7b4e1 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 21 Aug 2024 01:09:16 +0200 Subject: [PATCH 1/3] feat(git-node): auto-fetch latest release tag when preparing release --- lib/prepare_release.js | 85 ++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 5b64d79f..5341d2a0 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -1,5 +1,5 @@ import path from 'node:path'; -import { promises as fs } from 'node:fs'; +import { readFileSync, promises as fs } from 'node:fs'; import semver from 'semver'; import { replaceInFile } from 'replace-in-file'; @@ -39,23 +39,28 @@ export default class ReleasePreparation { // 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.`); + const newVersion = semver.parse(argv.newVersion); + if (!newVersion) { + cli.warn(`${argv.newVersion} is not a valid semantic version.`); return; } - this.newVersion = newVersion; + this.newVersion = newVersion.version; + this.versionComponents = { + major: newVersion.major, + minor: newVersion.minor, + patch: newVersion.patch + }; + this.getLastRef(this.findMostRecentReleaseFromChangelog( + path.resolve(`doc/changelogs/CHANGELOG_V${newVersion.major}.md`) + ).tag); } else { - this.newVersion = this.calculateNewVersion(); + this.versionComponents = this.calculateNewVersion(); + const { major, minor, patch } = this.versionComponents; + this.newVersion = `${major}.${minor}.${patch}`; + cli.info(`Attempt at preparing ${this.newVersion}`); } - const { upstream, owner, repo, newVersion } = this; - - this.versionComponents = { - major: semver.major(newVersion), - minor: semver.minor(newVersion), - patch: semver.patch(newVersion) - }; + const { upstream, owner, repo } = this; this.stagingBranch = `v${this.versionComponents.major}.x-staging`; this.releaseBranch = `v${this.versionComponents.major}.x`; @@ -369,24 +374,35 @@ export default class ReleasePreparation { return missing; } - calculateNewVersion() { - let newVersion; - - const lastTagVersion = semver.clean(this.getLastRef()); - const lastTag = { - major: semver.major(lastTagVersion), - minor: semver.minor(lastTagVersion), - patch: semver.patch(lastTagVersion) + findMostRecentReleaseFromChangelog(pathToChangelog, isMainChangelog) { + const data = readFileSync(pathToChangelog, 'utf8'); + const [, major, minor, patch] = (isMainChangelog + ? /\1\.\2\.\3<\/a><\/b>/ + : /\1\.\2\.\3<\/a>/).exec(data); + + return { + major, + minor, + patch, + tag: `v${major}.${minor}.${patch}` }; + } - const changelog = this.getChangelog(); + calculateNewVersion() { + const lastTag = this.findMostRecentReleaseFromChangelog(path.resolve('CHANGELOG.md'), true); + const { tag, ...newVersion } = lastTag; + + const changelog = this.getChangelog(lastTag.tag); 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 +412,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 +438,7 @@ export default class ReleasePreparation { '--markdown', '--filter-release', '--start-ref', - this.getLastRef() + this.getLastRef(tagName) ]).trim(); } From 9f4d62451f68bbcc6daf0591fbbb48daa7778791 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 27 Aug 2024 13:47:23 +0200 Subject: [PATCH 2/3] feat: suggest to fetch staging branch before preparing --- components/git/release.js | 2 + lib/prepare_release.js | 112 ++++++++++++++++++++------------------ 2 files changed, 61 insertions(+), 53 deletions(-) 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 5341d2a0..7db483b2 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -1,5 +1,5 @@ import path from 'node:path'; -import { readFileSync, promises as fs } from 'node:fs'; +import { promises as fs } from 'node:fs'; import semver from 'semver'; import { replaceInFile } from 'replace-in-file'; @@ -15,13 +15,13 @@ 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; @@ -30,41 +30,10 @@ export default class ReleasePreparation { this.date = ''; this.config = getMergedConfig(this.dir); this.filterLabels = argv.filterLabel && argv.filterLabel.split(','); - - // 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.parse(argv.newVersion); - if (!newVersion) { - cli.warn(`${argv.newVersion} is not a valid semantic version.`); - return; - } - this.newVersion = newVersion.version; - this.versionComponents = { - major: newVersion.major, - minor: newVersion.minor, - patch: newVersion.patch - }; - this.getLastRef(this.findMostRecentReleaseFromChangelog( - path.resolve(`doc/changelogs/CHANGELOG_V${newVersion.major}.md`) - ).tag); - } else { - this.versionComponents = this.calculateNewVersion(); - const { major, minor, patch } = this.versionComponents; - this.newVersion = `${major}.${minor}.${patch}`; - cli.info(`Attempt at preparing ${this.newVersion}`); - } + this.newVersion = argv.newVersion; const { upstream, owner, repo } = this; - 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(); @@ -74,6 +43,10 @@ export default class ReleasePreparation { } } + get branch() { + return this.stagingBranch; + } + warnForNonMergeablePR(pr) { const { cli } = this; @@ -374,26 +347,20 @@ export default class ReleasePreparation { return missing; } - findMostRecentReleaseFromChangelog(pathToChangelog, isMainChangelog) { - const data = readFileSync(pathToChangelog, 'utf8'); - const [, major, minor, patch] = (isMainChangelog - ? /\1\.\2\.\3<\/a><\/b>/ - : /\1\.\2\.\3<\/a>/).exec(data); - - return { - major, - minor, - patch, - tag: `v${major}.${minor}.${patch}` - }; - } + async calculateNewVersion(major) { + const { cli } = this; - calculateNewVersion() { - const lastTag = this.findMostRecentReleaseFromChangelog(path.resolve('CHANGELOG.md'), true); - const { tag, ...newVersion } = lastTag; + 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(lastTag.tag); + 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.major++; newVersion.minor = 0; @@ -763,6 +730,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, From a8da54a642a03c7790c0cf593a7b42af4272b84e Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 27 Aug 2024 13:50:30 +0200 Subject: [PATCH 3/3] fixup! feat: suggest to fetch staging branch before preparing remove duplicated code from Session --- lib/prepare_release.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 7db483b2..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'; @@ -28,19 +27,8 @@ export default class ReleasePreparation extends Session { this.runBranchDiff = !argv.skipBranchDiff; this.ltsCodename = ''; this.date = ''; - this.config = getMergedConfig(this.dir); this.filterLabels = argv.filterLabel && argv.filterLabel.split(','); this.newVersion = argv.newVersion; - - const { upstream, owner, repo } = this; - - 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() {