Skip to content

Commit 15ae401

Browse files
authored
feat(git-node): auto-fetch latest release tag when preparing release (#842)
And suggest fetching the upstream staging branch at the start of the session.
1 parent 4e8ec9c commit 15ae401

File tree

2 files changed

+80
-57
lines changed

2 files changed

+80
-57
lines changed

components/git/release.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ async function main(state, argv, cli, dir) {
7878
if (state === PREPARE) {
7979
const prep = new ReleasePreparation(argv, cli, dir);
8080

81+
await prep.prepareLocalBranch();
82+
8183
if (prep.warnForWrongBranch()) return;
8284

8385
// If the new version was automatically calculated, confirm it.

lib/prepare_release.js

Lines changed: 78 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { promises as fs } from 'node:fs';
44
import semver from 'semver';
55
import { replaceInFile } from 'replace-in-file';
66

7-
import { getMergedConfig } from './config.js';
87
import { runAsync, runSync } from './run.js';
98
import { writeJson, readJson } from './file.js';
109
import Request from './request.js';
@@ -15,58 +14,25 @@ import {
1514
updateTestProcessRelease
1615
} from './release/utils.js';
1716
import CherryPick from './cherry_pick.js';
17+
import Session from './session.js';
1818

1919
const isWindows = process.platform === 'win32';
2020

21-
export default class ReleasePreparation {
21+
export default class ReleasePreparation extends Session {
2222
constructor(argv, cli, dir) {
23-
this.cli = cli;
24-
this.dir = dir;
23+
super(cli, dir);
2524
this.isSecurityRelease = argv.security;
2625
this.isLTS = false;
2726
this.isLTSTransition = argv.startLTS;
2827
this.runBranchDiff = !argv.skipBranchDiff;
2928
this.ltsCodename = '';
3029
this.date = '';
31-
this.config = getMergedConfig(this.dir);
3230
this.filterLabels = argv.filterLabel && argv.filterLabel.split(',');
31+
this.newVersion = argv.newVersion;
32+
}
3333

34-
// Ensure the preparer has set an upstream and username.
35-
if (this.warnForMissing()) {
36-
cli.error('Failed to begin the release preparation process.');
37-
return;
38-
}
39-
40-
// Allow passing optional new version.
41-
if (argv.newVersion) {
42-
const newVersion = semver.clean(argv.newVersion);
43-
if (!semver.valid(newVersion)) {
44-
cli.warn(`${newVersion} is not a valid semantic version.`);
45-
return;
46-
}
47-
this.newVersion = newVersion;
48-
} else {
49-
this.newVersion = this.calculateNewVersion();
50-
}
51-
52-
const { upstream, owner, repo, newVersion } = this;
53-
54-
this.versionComponents = {
55-
major: semver.major(newVersion),
56-
minor: semver.minor(newVersion),
57-
patch: semver.patch(newVersion)
58-
};
59-
60-
this.stagingBranch = `v${this.versionComponents.major}.x-staging`;
61-
this.releaseBranch = `v${this.versionComponents.major}.x`;
62-
63-
const upstreamHref = runSync('git', [
64-
'config', '--get',
65-
`remote.${upstream}.url`]).trim();
66-
if (!new RegExp(`${owner}/${repo}(?:.git)?$`).test(upstreamHref)) {
67-
cli.warn('Remote repository URL does not point to the expected ' +
68-
`repository ${owner}/${repo}`);
69-
}
34+
get branch() {
35+
return this.stagingBranch;
7036
}
7137

7238
warnForNonMergeablePR(pr) {
@@ -369,24 +335,29 @@ export default class ReleasePreparation {
369335
return missing;
370336
}
371337

372-
calculateNewVersion() {
373-
let newVersion;
338+
async calculateNewVersion(major) {
339+
const { cli } = this;
374340

375-
const lastTagVersion = semver.clean(this.getLastRef());
376-
const lastTag = {
377-
major: semver.major(lastTagVersion),
378-
minor: semver.minor(lastTagVersion),
379-
patch: semver.patch(lastTagVersion)
380-
};
341+
cli.startSpinner(`Parsing CHANGELOG for most recent release of v${major}.x`);
342+
const data = await fs.readFile(
343+
path.resolve(`doc/changelogs/CHANGELOG_V${major}.md`),
344+
'utf8'
345+
);
346+
const [,, minor, patch] = /<a href="#(\d+)\.(\d+)\.(\d+)">\1\.\2\.\3<\/a><br\/>/.exec(data);
381347

382-
const changelog = this.getChangelog();
348+
cli.stopSpinner(`Latest release on ${major}.x line is ${major}.${minor}.${patch}`);
349+
const changelog = this.getChangelog(`v${major}.${minor}.${patch}`);
383350

351+
const newVersion = { major, minor, patch };
384352
if (changelog.includes('SEMVER-MAJOR')) {
385-
newVersion = `${lastTag.major + 1}.0.0`;
353+
newVersion.major++;
354+
newVersion.minor = 0;
355+
newVersion.patch = 0;
386356
} else if (changelog.includes('SEMVER-MINOR') || this.isLTSTransition) {
387-
newVersion = `${lastTag.major}.${lastTag.minor + 1}.0`;
357+
newVersion.minor++;
358+
newVersion.patch = 0;
388359
} else {
389-
newVersion = `${lastTag.major}.${lastTag.minor}.${lastTag.patch + 1}`;
360+
newVersion.patch++;
390361
}
391362

392363
return newVersion;
@@ -396,11 +367,22 @@ export default class ReleasePreparation {
396367
return runSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
397368
}
398369

399-
getLastRef() {
400-
return runSync('git', ['describe', '--abbrev=0', '--tags']).trim();
370+
getLastRef(tagName) {
371+
if (!tagName) {
372+
return runSync('git', ['describe', '--abbrev=0', '--tags']).trim();
373+
}
374+
375+
try {
376+
runSync('git', ['rev-parse', tagName]);
377+
} catch {
378+
this.cli.startSpinner(`Error parsing git ref ${tagName}, attempting fetching it as a tag`);
379+
runSync('git', ['fetch', this.upstream, 'tag', '-n', tagName]);
380+
this.cli.stopSpinner(`Tag fetched: ${tagName}`);
381+
}
382+
return tagName;
401383
}
402384

403-
getChangelog() {
385+
getChangelog(tagName) {
404386
const changelogMaker = new URL(
405387
'../node_modules/.bin/changelog-maker' + (isWindows ? '.cmd' : ''),
406388
import.meta.url
@@ -411,7 +393,7 @@ export default class ReleasePreparation {
411393
'--markdown',
412394
'--filter-release',
413395
'--start-ref',
414-
this.getLastRef()
396+
this.getLastRef(tagName)
415397
]).trim();
416398
}
417399

@@ -736,6 +718,45 @@ export default class ReleasePreparation {
736718
return runSync(branchDiff, branchDiffOptions);
737719
}
738720

721+
async prepareLocalBranch() {
722+
const { cli } = this;
723+
if (this.newVersion) {
724+
// If the CLI asked for a specific version:
725+
const newVersion = semver.parse(this.newVersion);
726+
if (!newVersion) {
727+
cli.warn(`${this.newVersion} is not a valid semantic version.`);
728+
return;
729+
}
730+
this.newVersion = newVersion.version;
731+
this.versionComponents = {
732+
major: newVersion.major,
733+
minor: newVersion.minor,
734+
patch: newVersion.patch
735+
};
736+
this.stagingBranch = `v${newVersion.major}.x-staging`;
737+
this.releaseBranch = `v${newVersion.major}.x`;
738+
await this.tryResetBranch();
739+
return;
740+
}
741+
742+
// Otherwise, we need to figure out what's the next version number for the
743+
// release line of the branch that's currently checked out.
744+
const currentBranch = this.getCurrentBranch();
745+
const match = /^v(\d+)\.x-staging$/.exec(currentBranch);
746+
747+
if (!match) {
748+
cli.warn(`Cannot prepare a release from ${currentBranch
749+
}. Switch to a staging branch before proceeding.`);
750+
return;
751+
}
752+
this.stagingBranch = currentBranch;
753+
await this.tryResetBranch();
754+
this.versionComponents = await this.calculateNewVersion(match[1]);
755+
const { major, minor, patch } = this.versionComponents;
756+
this.newVersion = `${major}.${minor}.${patch}`;
757+
this.releaseBranch = `v${major}.x`;
758+
}
759+
739760
warnForWrongBranch() {
740761
const {
741762
cli,

0 commit comments

Comments
 (0)