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 comparison branch when preparing release #846

Merged
merged 2 commits into from
Aug 28, 2024
Merged
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
93 changes: 66 additions & 27 deletions lib/prepare_release.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
});
Expand All @@ -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) {
Expand Down Expand Up @@ -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] = /<a href="#(\d+)\.(\d+)\.(\d+)">\1\.\2\.\3<\/a><br\/>/.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')) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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'
});
Expand All @@ -641,8 +630,9 @@ export default class ReleasePreparation extends Session {
return useMessage;
}

getBranchDiff(opts) {
async getBranchDiff(opts) {
const {
cli,
versionComponents = {},
upstream,
newVersion,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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] = /<a href="#(\d+)\.(\d+)\.(\d+)">\1\.\2\.\3<\/a><br\/>/.exec(data);
this.isLTS = data.includes('<th>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) {
Expand All @@ -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;
}

Expand All @@ -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`;
Expand Down
Loading