Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(git-node): auto-fetch latest release tag when preparing release #842

Merged
merged 3 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions components/git/release.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
135 changes: 78 additions & 57 deletions lib/prepare_release.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -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] = /<a href="#(\d+)\.(\d+)\.(\d+)">\1\.\2\.\3<\/a><br\/>/.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;
Expand All @@ -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
Expand All @@ -411,7 +393,7 @@ export default class ReleasePreparation {
'--markdown',
'--filter-release',
'--start-ref',
this.getLastRef()
this.getLastRef(tagName)
]).trim();
}

Expand Down Expand Up @@ -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,
Expand Down
Loading