Skip to content

Commit

Permalink
feat: search matching releases on GitHub before falling back to tags (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
maribethb authored Jun 27, 2022
1 parent 8af4e14 commit 8deacf2
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 21 deletions.
71 changes: 66 additions & 5 deletions packages/server/src/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,80 @@ export const getRepo = (name: string, token: string): Promise<GhRepo> => {
};

/**
* https://developer.github.com/v3/repos/releases/#list-releases-for-a-repository
* Checks if there is a matching release on GitHub for the given tag(s). If there is no release, we'll check the list of tags.
* Checking tags is less reliable as a limited number of tags are checked and they are not in chronological order.
* @param name repository name including username. ex: node/node or bcoe/yargs
* @param token
* @param tags list of possible tag names to fetch. The first one to match will be returned.
*
* @returns string first given tag that matches a tag or release on GitHub, or undefined if none match.
*/
export const getRelease = async (
name: string,
token: string,
tags: string[]
): Promise<string | undefined> => {
const matchingRelease = await getReleaseForTags(name, token, tags);
if (matchingRelease) {
return matchingRelease;
}
return getMatchingTags(name, token, tags);
};

/**
* Calls GitHub's API to get a release by tag name.
* https://docs.github.com/en/rest/releases/releases#get-a-release-by-tag-name
* @param name repository name including username. ex: node/node or bcoe/yargs
* @param token
* @param tags list of possible tag names to fetch. The first one to match will be returned.
*
* @returns string first given tag that matches a release on GitHub, or undefined if none match.
*/
const getReleaseForTags = async (
name: string,
token: string,
tags: string[]
): Promise<string | undefined> => {
const client = gh.client(token, clientOptions);
for (const tag of tags) {
const release = await new Promise<string | undefined>((resolve, reject) => {
client.get(
`/repos/${name}/releases/tags/${tag}`,
{},
(err: Error, code: number, resp: {name: string}) => {
if (err) {
return reject(
Error(`getReleaseForTags: tag = ${tag}, err = ${err}`)
);
}
resolve(resp.name || undefined);
}
);
});
if (release) {
return release;
}
}
return undefined;
};

/**
* Calls GitHub's API to list tags for a repository.
* https://docs.github.com/en/rest/repos/repos#list-repository-tags
* @param name repository name including username. ex: node/node or bcoe/yargs
* @param token
* @param matchingTags list of possible tag names to fetch. The first one to match will be returned.
*
* @returns string first given tag that matches a tag on GitHub, or undefined if none match.
*/
export const getRelease = async (
const getMatchingTags = async (
name: string,
token: string,
matchingTags: string[]
): Promise<string | undefined> => {
const client = gh.client(token, clientOptions);
// We check up to 1200 of the most recent tags for a matching release,
// we use a large page size to allow for monorepos with 100s of tags:
// We check up to 1100 of the most recent tags for a match,
// using a large page size to allow for monorepos with 100s of tags:
const maxPagination = 12;
for (let page = 1; page < maxPagination; page++) {
const tags: [{name: string}] = await new Promise((resolve, reject) => {
Expand All @@ -77,7 +136,9 @@ export const getRelease = async (
(err: Error, code: number, resp: [{name: string}]) => {
if (err) {
return reject(
Error(`getRelease: matchingTags = ${matchingTags.toString()}`)
Error(
`getMatchingTags: tags = ${matchingTags.toString()}, err = ${err}`
)
);
} else if (code !== 200) {
return reject(
Expand Down
63 changes: 57 additions & 6 deletions packages/server/test/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,49 @@ import * as github from '../../src/lib/github';
nock.disableNetConnect();

describe('github', () => {
describe('getLatestRelease', () => {
it('returns latest release from GitHub', async () => {
describe('getRelease', () => {
it('returns matching release from GitHub', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/releases/tags/v1.0.2')
.reply(200, {name: 'v1.0.2'});

const latest = await github.getRelease('bcoe/test', 'abc123', ['v1.0.2']);
expect(latest).to.equal('v1.0.2');
request.done();
});

it('returns release matching monorepo-style tag', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/releases/tags/test-v1.0.2')
.reply(200, {name: 'test-v1.0.2'});

const latest = await github.getRelease('bcoe/test', 'abc123', [
'test-v1.0.2',
'@bcoe/test@1.0.2',
]);
expect(latest).to.equal('test-v1.0.2');
request.done();
});

it('returns release matching lerna-style tag', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/releases/tags/test-v1.0.2')
.reply(404, {})
.get('/repos/bcoe/test/releases/tags/@bcoe/test@1.0.2')
.reply(200, {name: '@bcoe/test@1.0.2'});

const latest = await github.getRelease('bcoe/test', 'abc123', [
'test-v1.0.2',
'@bcoe/test@1.0.2',
]);
expect(latest).to.equal('@bcoe/test@1.0.2');
request.done();
});

it('returns matching tag from GitHub if no release found', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/releases/tags/v1.0.2')
.reply(404, {})
.get('/repos/bcoe/test/tags?per_page=100&page=1')
.reply(200, [{name: 'v1.0.2'}]);

Expand All @@ -35,6 +75,8 @@ describe('github', () => {

it('bubbles error appropriately', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/releases/tags/v1.0.2')
.reply(404, {})
.get('/repos/bcoe/test/tags?per_page=100&page=1')
.reply(404);
let err: Error | undefined = undefined;
Expand All @@ -50,8 +92,12 @@ describe('github', () => {
request.done();
});

it('does not return latest release without prefix, when monorepo-style used', async () => {
it('does not return latest tag without prefix, when monorepo-style used', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/releases/tags/foo-v1.0.2')
.reply(404, {})
.get('/repos/bcoe/test/releases/tags/@scope/foo@1.0.2')
.reply(404, {})
.get('/repos/bcoe/test/tags?per_page=100&page=1')
.reply(200, [{name: 'v1.0.2'}])
.get('/repos/bcoe/test/tags?per_page=100&page=2')
Expand All @@ -77,14 +123,17 @@ describe('github', () => {

expect(
await github.getRelease('bcoe/test', 'abc123', [
'foo-v1.0.2, @scope/foo@1.0.2',
'foo-v1.0.2',
'@scope/foo@1.0.2',
])
).to.equal(undefined);
request.done();
});

it('returns latest release matching monorepo style tag', async () => {
it('returns latest tag matching monorepo style tag', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/releases/tags/foo-v1.0.2')
.reply(404, {})
.get('/repos/bcoe/test/tags?per_page=100&page=1')
.reply(200, [{name: 'v1.0.3'}])
.get('/repos/bcoe/test/tags?per_page=100&page=2')
Expand All @@ -99,8 +148,10 @@ describe('github', () => {
request.done();
});

it('returns latest release matching lerna style tag', async () => {
it('returns latest tag matching lerna style tag', async () => {
const request = nock('https://api.github.com')
.get('/repos/bcoe/test/releases/tags/@scope/foo@1.0.2')
.reply(404, {})
.get('/repos/bcoe/test/tags?per_page=100&page=1')
.reply(200, [{name: 'v1.0.3'}])
.get('/repos/bcoe/test/tags?per_page=100&page=2')
Expand Down
Loading

0 comments on commit 8deacf2

Please sign in to comment.