Skip to content
114 changes: 114 additions & 0 deletions src/release-pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,118 @@ export class ReleasePR {

return this.defaultBranch;
}

/**
* Find the most recent matching release tag on the branch we're
* configured for.
*
* @param {string} prefix - Limit the release to a specific component.
* @param {boolean} preRelease - Whether or not to return pre-release
* versions. Defaults to false.
*/
async latestTag(
prefix?: string,
preRelease = false
): Promise<GitHubTag | undefined> {
// only look at the last 250 or so commits to find the latest tag - we
// don't want to scan the entire repository history if this repo has never
// been released
const pull = await this.findMergedReleasePR([], prefix, preRelease, 250);
if (!pull) return await this.gh.latestTagFallback(prefix, preRelease);

// TODO: shouldn't need to do this twice
const branchName = BranchName.parse(pull.headRefName)!;
const version = await this.detectReleaseVersion(pull, branchName);
const normalizedVersion = semver.valid(version)!;

return {
name: `v${normalizedVersion}`,
sha: pull.sha,
version: normalizedVersion,
};
}

// The default matcher will rule out pre-releases.
/**
* Find the last merged pull request that targeted the default
* branch and looks like a release PR.
*
* @param {string[]} labels - If provided, ensure that the pull
* request has all of the specified labels
* @param {string|undefined} branchPrefix - If provided, limit
* release pull requests that contain the specified component
* @param {boolean} preRelease - Whether to include pre-release
* versions in the response. Defaults to true.
* @param {number} maxResults - Limit the number of results searched.
* Defaults to unlimited.
* @returns {MergedGitHubPR|undefined}
*/
protected async findMergedReleasePR(
labels: string[],
branchPrefix: string | undefined = undefined,
preRelease = true,
maxResults: number = Number.MAX_SAFE_INTEGER
): Promise<MergedGitHubPR | undefined> {
branchPrefix = branchPrefix?.endsWith('-')
? branchPrefix.replace(/-$/, '')
: branchPrefix;

const generator = this.gh.mergeCommitIterator(maxResults);
for await (const commitWithPullRequest of generator) {
const mergedPullRequest = commitWithPullRequest.pullRequest;
if (!mergedPullRequest) {
continue;
}

// If labels specified, ensure the pull request has all the specified labels
if (
labels.length > 0 &&
!this.hasAllLabels(labels, mergedPullRequest.labels)
) {
continue;
}

const branchName = BranchName.parse(mergedPullRequest.headRefName);
if (!branchName) {
continue;
}

// If branchPrefix is specified, ensure it is found in the branch name.
// If branchPrefix is not specified, component should also be undefined.
if (branchName.getComponent() !== branchPrefix) {
continue;
}

const version = await this.detectReleaseVersion(
mergedPullRequest,
branchName
);
if (!version) {
continue;
}

// What's left by now should just be the version string.
// Check for pre-releases if needed.
if (!preRelease && version.indexOf('-') >= 0) {
continue;
}

// Make sure we did get a valid semver.
const normalizedVersion = semver.valid(version);
if (!normalizedVersion) {
continue;
}
return mergedPullRequest;
}

return undefined;
}

private hasAllLabels(labelsA: string[], labelsB: string[]) {
let hasAll = true;
labelsA.forEach(label => {
if (labelsB.indexOf(label) === -1) hasAll = false;
});
return hasAll;
}
}
2 changes: 1 addition & 1 deletion src/releasers/go-yoshi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const REGEN_PR_REGEX = /.*auto-regenerate.*/;

export class GoYoshi extends ReleasePR {
protected async _run(): Promise<number | undefined> {
const latestTag = await this.gh.latestTag(
const latestTag = await this.latestTag(
this.monorepoTags ? `${this.packageName}-` : undefined,
false
);
Expand Down
2 changes: 1 addition & 1 deletion src/releasers/java-bom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class JavaBom extends ReleasePR {
this.labels = ['type: process'];
}

const latestTag: GitHubTag | undefined = await this.gh.latestTag();
const latestTag: GitHubTag | undefined = await this.latestTag();

const commits = await this.commits({
sha: latestTag ? latestTag.sha : undefined,
Expand Down
4 changes: 2 additions & 2 deletions src/releasers/java-yoshi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class JavaYoshi extends ReleasePR {
this.labels = ['type: process'];
}

const latestTag: GitHubTag | undefined = await this.gh.latestTag();
const latestTag: GitHubTag | undefined = await this.latestTag();
const commits: Commit[] = this.snapshot
? [
{
Expand Down Expand Up @@ -321,7 +321,7 @@ export class JavaYoshi extends ReleasePR {
// Override this method to detect the release version from code (if it cannot be
// inferred from the release PR head branch)
protected detectReleaseVersionFromTitle(title: string): string | undefined {
const pattern = /^chore\((?<branch>[^(]+)\): release ?(?<component>.*) (?<version>\d+\.\d+\.\d+)$/;
const pattern = /^chore\((?<branch>[^(]+)\): release ?(?<component>.*) (?<version>\d+\.\d+\.\d+(-\w+)?)$/;
const match = title.match(pattern);
if (match?.groups) {
return match.groups['version'];
Expand Down
2 changes: 1 addition & 1 deletion src/releasers/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class Node extends ReleasePR {
}

protected async _run(): Promise<number | undefined> {
const latestTag: GitHubTag | undefined = await this.gh.latestTag(
const latestTag: GitHubTag | undefined = await this.latestTag(
this.monorepoTags ? `${this.packagePrefix}-` : undefined
);
const commits: Commit[] = await this.commits({
Expand Down
2 changes: 1 addition & 1 deletion src/releasers/ocaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const CHANGELOG_SECTIONS = [

export class OCaml extends ReleasePR {
protected async _run(): Promise<number | undefined> {
const latestTag: GitHubTag | undefined = await this.gh.latestTag(
const latestTag: GitHubTag | undefined = await this.latestTag(
this.monorepoTags ? `${this.packageName}-` : undefined
);
const commits: Commit[] = await this.commits({
Expand Down
2 changes: 1 addition & 1 deletion src/releasers/php-yoshi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ interface PHPYoshiBulkUpdate {

export class PHPYoshi extends ReleasePR {
protected async _run(): Promise<number | undefined> {
const latestTag: GitHubTag | undefined = await this.gh.latestTag();
const latestTag: GitHubTag | undefined = await this.latestTag();
const commits: Commit[] = await this.commits({
sha: latestTag ? latestTag.sha : undefined,
});
Expand Down
2 changes: 1 addition & 1 deletion src/releasers/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class Python extends ReleasePR {
}

protected async _run(): Promise<number | undefined> {
const latestTag: GitHubTag | undefined = await this.gh.latestTag(
const latestTag: GitHubTag | undefined = await this.latestTag(
this.monorepoTags ? `${this.packageName}-` : undefined
);
const commits: Commit[] = await this.commits({
Expand Down
2 changes: 1 addition & 1 deletion src/releasers/ruby.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class Ruby extends ReleasePR {
this.versionFile = options.versionFile;
}
protected async _run(): Promise<number | undefined> {
const latestTag: GitHubTag | undefined = await this.gh.latestTag(
const latestTag: GitHubTag | undefined = await this.latestTag(
this.monorepoTags ? `${this.packageName}-` : undefined,
false
);
Expand Down
2 changes: 1 addition & 1 deletion src/releasers/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class Rust extends ReleasePR {
return tagOrBranch;
};

const latestTag: GitHubTag | undefined = await this.gh.latestTag(prefix);
const latestTag: GitHubTag | undefined = await this.latestTag(prefix);
const commits: Commit[] = await this.commits({
sha: latestTag ? latestTag.sha : undefined,
path: this.path,
Expand Down
2 changes: 1 addition & 1 deletion src/releasers/simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {VersionTxt} from '../updaters/version-txt';

export class Simple extends ReleasePR {
protected async _run(): Promise<number | undefined> {
const latestTag: GitHubTag | undefined = await this.gh.latestTag(
const latestTag: GitHubTag | undefined = await this.latestTag(
this.monorepoTags ? `${this.packageName}-` : undefined
);
const commits: Commit[] = await this.commits({
Expand Down
2 changes: 1 addition & 1 deletion src/releasers/terraform-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {ModuleVersion} from '../updaters/terraform/module-version';

export class TerraformModule extends ReleasePR {
protected async _run(): Promise<number | undefined> {
const latestTag: GitHubTag | undefined = await this.gh.latestTag(
const latestTag: GitHubTag | undefined = await this.latestTag(
this.monorepoTags ? `${this.packageName}-` : undefined
);
const commits: Commit[] = await this.commits({
Expand Down
275 changes: 275 additions & 0 deletions test/fixtures/latest-tag-stable-branch.json

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions test/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ describe('GitHub', () => {
req.done();
});
});

describe('getFileContentsWithSimpleAPI', () => {
const setupReq = (ref: string) => {
req
Expand Down Expand Up @@ -928,4 +929,102 @@ describe('GitHub', () => {
expect(thrown).to.be.true;
});
});

describe('latestTagFallback', () => {
it('falls back to using tags, for simple case', async () => {
req.get('/repos/fake/fake/tags?per_page=100').reply(200, [
{
name: 'v1.0.0',
commit: {sha: 'abc123'},
},
{
name: 'v1.1.0',
commit: {sha: 'deadbeef'},
},
]);
const latestTag = await github.latestTagFallback();
expect(latestTag!.version).to.equal('1.1.0');
req.done();
});

it('falls back to using tags, when prefix is provided', async () => {
req.get('/repos/fake/fake/tags?per_page=100').reply(200, [
{
name: 'v1.0.0',
commit: {sha: 'abc123'},
},
{
name: 'v1.1.0',
commit: {sha: 'deadbeef'},
},
{
name: 'foo-v1.9.0',
commit: {sha: 'deadbeef'},
},
{
name: 'v1.2.0',
commit: {sha: 'deadbeef'},
},
]);
const latestTag = await github.latestTagFallback('foo-');
expect(latestTag!.version).to.equal('1.9.0');
req.done();
});

it('allows for "@" rather than "-" when fallback used', async () => {
req.get('/repos/fake/fake/tags?per_page=100').reply(200, [
{
name: 'v1.0.0',
commit: {sha: 'abc123'},
},
{
name: 'v1.1.0',
commit: {sha: 'deadbeef'},
},
{
name: '[email protected]',
commit: {sha: 'dead'},
},
{
name: 'v1.2.0',
commit: {sha: 'beef'},
},
{
name: '[email protected]',
commit: {sha: '123abc'},
},
]);
const latestTag = await github.latestTagFallback('foo-');
expect(latestTag!.version).to.equal('2.1.0');
req.done();
});

it('allows for "/" rather than "-" when fallback used', async () => {
req.get('/repos/fake/fake/tags?per_page=100').reply(200, [
{
name: 'v1.0.0',
commit: {sha: 'abc123'},
},
{
name: 'v1.1.0',
commit: {sha: 'deadbeef'},
},
{
name: 'foo/v2.3.0',
commit: {sha: 'dead'},
},
{
name: 'v1.2.0',
commit: {sha: 'beef'},
},
{
name: 'foo/v2.1.0',
commit: {sha: '123abc'},
},
]);
const latestTag = await github.latestTagFallback('foo-');
expect(latestTag!.version).to.equal('2.3.0');
req.done();
});
});
});
Loading