From e06c6ba5c3ce29689275e495934d4a6785962d5b Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Wed, 22 Dec 2021 12:13:49 -0800 Subject: [PATCH] fix: fallback to look at tags when looking for latest release (#1160) Co-authored-by: Benjamin E. Coe --- src/github.ts | 42 +++++++++++++++++- src/manifest.ts | 34 +++++++++++++- test/manifest.ts | 112 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 184 insertions(+), 4 deletions(-) diff --git a/src/github.ts b/src/github.ts index 7f0fba921..e999bdf90 100644 --- a/src/github.ts +++ b/src/github.ts @@ -144,6 +144,10 @@ interface ReleaseIteratorOptions { maxResults?: number; } +interface TagIteratorOptions { + maxResults?: number; +} + export interface GitHubRelease { name?: string; tagName: string; @@ -153,6 +157,11 @@ export interface GitHubRelease { draft?: boolean; } +export interface GitHubTag { + name: string; + sha: string; +} + export class GitHub { readonly repository: Repository; private octokit: OctokitType; @@ -583,7 +592,7 @@ export class GitHub { } /** - * Iterate through merged pull requests with a max number of results scanned. + * Iterate through releases with a max number of results scanned. * * @param {ReleaseIteratorOptions} options Query options * @param {number} options.maxResults Limit the number of results searched. @@ -670,6 +679,37 @@ export class GitHub { }; } + /** + * Iterate through tags with a max number of results scanned. + * + * @param {TagIteratorOptions} options Query options + * @param {number} options.maxResults Limit the number of results searched. + * Defaults to unlimited. + * @yields {GitHubTag} + * @throws {GitHubAPIError} on an API error + */ + async *tagIterator(options: TagIteratorOptions = {}) { + const maxResults = options.maxResults || Number.MAX_SAFE_INTEGER; + let results = 0; + for await (const response of this.octokit.paginate.iterator( + this.octokit.rest.repos.listTags, + { + owner: this.repository.owner, + repo: this.repository.repo, + } + )) { + for (const tag of response.data) { + if ((results += 1) > maxResults) { + break; + } + yield { + name: tag.name, + sha: tag.commit.sha, + }; + } + } + } + /** * Fetch the contents of a file from the configured branch * diff --git a/src/manifest.ts b/src/manifest.ts index 53626e893..2484a05c0 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -995,7 +995,7 @@ async function latestReleaseVersion( continue; } - if (tagName.component === prefix) { + if (tagName.component === branchPrefix) { logger.debug(`found release for ${prefix}`, tagName.version); if (!commitShas.has(release.sha)) { logger.debug( @@ -1011,8 +1011,38 @@ async function latestReleaseVersion( candidateReleaseVersions ); + if (candidateReleaseVersions.length > 0) { + // Find largest release number (sort descending then return first) + return candidateReleaseVersions.sort((a, b) => b.compare(a))[0]; + } + + // If not found from recent pull requests or releases, look at tags. Iterate + // through tags and cross reference against SHAs in this branch + const tagGenerator = github.tagIterator(); + const candidateTagVersion: Version[] = []; + for await (const tag of tagGenerator) { + const tagName = TagName.parse(tag.name); + if (!tagName) { + continue; + } + + if (tagName.component === branchPrefix) { + if (!commitShas.has(tag.sha)) { + logger.debug( + `SHA not found in recent commits to branch ${targetBranch}, skipping` + ); + continue; + } + candidateTagVersion.push(tagName.version); + } + } + logger.debug( + `found ${candidateTagVersion.length} possible tags.`, + candidateTagVersion + ); + // Find largest release number (sort descending then return first) - return candidateReleaseVersions.sort((a, b) => b.compare(a))[0]; + return candidateTagVersion.sort((a, b) => b.compare(a))[0]; } function mergeReleaserConfig( diff --git a/test/manifest.ts b/test/manifest.ts index 2d2633e5a..170156c70 100644 --- a/test/manifest.ts +++ b/test/manifest.ts @@ -14,7 +14,7 @@ import {describe, it, beforeEach, afterEach} from 'mocha'; import {Manifest} from '../src/manifest'; -import {GitHub, GitHubRelease} from '../src/github'; +import {GitHub, GitHubRelease, GitHubTag} from '../src/github'; import * as sinon from 'sinon'; import {Commit} from '../src/commit'; import { @@ -59,6 +59,15 @@ function mockReleases(github: GitHub, releases: GitHubRelease[]) { sandbox.stub(github, 'releaseIterator').returns(fakeGenerator()); } +function mockTags(github: GitHub, tags: GitHubTag[]) { + async function* fakeGenerator() { + for (const tag of tags) { + yield tag; + } + } + sandbox.stub(github, 'tagIterator').returns(fakeGenerator()); +} + function mockPullRequests( github: GitHub, pullRequests: PullRequest[], @@ -335,6 +344,47 @@ describe('Manifest', () => { '3.3.3' ); }); + it('finds legacy tags', async () => { + mockCommits(github, [ + { + sha: 'abc123', + message: 'some commit message', + files: [], + pullRequest: { + headBranchName: 'release-please/branches/main/components/foobar', + baseBranchName: 'main', + number: 123, + title: 'chore: release foobar 1.2.3', + body: '', + labels: [], + files: [], + }, + }, + ]); + mockReleases(github, []); + mockTags(github, [ + { + name: 'other-v3.3.3', + sha: 'abc123', + }, + ]); + + const manifest = await Manifest.fromConfig(github, 'target-branch', { + releaseType: 'simple', + bumpMinorPreMajor: true, + bumpPatchForMinorPreMajor: true, + component: 'other', + includeComponentInTag: true, + }); + expect(Object.keys(manifest.repositoryConfig)).lengthOf(1); + expect( + Object.keys(manifest.releasedVersions), + 'found release versions' + ).lengthOf(1); + expect(Object.values(manifest.releasedVersions)[0].toString()).to.eql( + '3.3.3' + ); + }); it('ignores manually tagged release if commit not found', async () => { mockCommits(github, [ { @@ -359,6 +409,7 @@ describe('Manifest', () => { url: 'http://path/to/release', }, ]); + mockTags(github, []); const manifest = await Manifest.fromConfig(github, 'target-branch', { releaseType: 'simple', @@ -415,6 +466,65 @@ describe('Manifest', () => { }, ]); + const manifest = await Manifest.fromConfig(github, 'target-branch', { + releaseType: 'simple', + bumpMinorPreMajor: true, + bumpPatchForMinorPreMajor: true, + component: 'other', + includeComponentInTag: true, + }); + expect(Object.keys(manifest.repositoryConfig)).lengthOf(1); + expect( + Object.keys(manifest.releasedVersions), + 'found release versions' + ).lengthOf(1); + expect(Object.values(manifest.releasedVersions)[0].toString()).to.eql( + '3.3.3' + ); + }); + it('finds largest found tagged', async () => { + mockCommits(github, [ + { + sha: 'abc123', + message: 'some commit message', + files: [], + pullRequest: { + headBranchName: 'release-please/branches/main/components/foobar', + baseBranchName: 'main', + number: 123, + title: 'chore: release foobar 1.2.3', + body: '', + labels: [], + files: [], + }, + }, + { + sha: 'def234', + message: 'some commit message', + files: [], + pullRequest: { + headBranchName: 'release-please/branches/main/components/foobar', + baseBranchName: 'main', + number: 123, + title: 'chore: release foobar 1.2.3', + body: '', + labels: [], + files: [], + }, + }, + ]); + mockReleases(github, []); + mockTags(github, [ + { + name: 'other-v3.3.3', + sha: 'abc123', + }, + { + name: 'other-v3.3.2', + sha: 'def234', + }, + ]); + const manifest = await Manifest.fromConfig(github, 'target-branch', { releaseType: 'simple', bumpMinorPreMajor: true,