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

Try extra release pages without Link header #23

Merged
merged 2 commits into from
Jan 24, 2021
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
173 changes: 114 additions & 59 deletions __tests__/version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const dataPath = path.join(__dirname, 'data');

import * as version from '../src/version';

describe('When a version is needed', () => {
describe('Pulling from multipage results with Link header', () => {
beforeEach(() => {
nock.disableNetConnect();
// Releases file contains version info for:
Expand All @@ -14,11 +14,10 @@ describe('When a version is needed', () => {
// 3.13.5
nock('https://api.github.com')
.get('/repos/Kitware/CMake/releases')
.query({ page: 1 })
.replyWithFile(200, path.join(dataPath, 'releases.json'), {
'Content-Type': 'application/json',
Link:
'<...releases?page=2>; rel="next", <...releases?page=2>; rel="last"',
'<https://api.github.com/repos/Kitware/CMake/releases?page=2>; rel="next", <https://api.github.com/repos/Kitware/CMake/releases?page=2>; rel="last"',
});
// Releases file 2 contains version info for:
// 2.4.8, 2.6.4, 2.8.10.2, 2.8.12.2
Expand All @@ -30,62 +29,106 @@ describe('When a version is needed', () => {
.replyWithFile(200, path.join(dataPath, 'releases2.json'), {
'Content-Type': 'application/json',
Link:
'<...releases?page=1>; rel="prev", <...releases?page=2>; rel="last"',
'<https://api.github.com/repos/Kitware/CMake/releases?page=1>; rel="first", <https://api.github.com/repos/Kitware/CMake/releases?page=1>; rel="prev"',
});
});
afterEach(() => {
nock.cleanAll();
nock.enableNetConnect();
});
it('latest version is correctly parsed', async () => {
it('parses the latest version', async () => {
const version_info = await version.getAllVersionInfo();
const latest = await version.getLatestMatching('', version_info);
const latest = version.getLatestMatching('', version_info);
expect(latest.name).toMatch(/3.16.2/);
});
it('exact version is selected', async () => {
it('selects an exact version for full release', async () => {
const version_info = await version.getAllVersionInfo();
const selected = await version.getLatestMatching('3.15.5', version_info);
const selected = version.getLatestMatching('3.15.5', version_info);
expect(selected.name).toMatch(/3.15.5/);
});
it('latest version is selected for provided minor release', async () => {
it('selects the latest version for provided minor release', async () => {
const version_info = await version.getAllVersionInfo();
const selected = await version.getLatestMatching('3.15', version_info);
const selected = version.getLatestMatching('3.15', version_info);
expect(selected.name).toMatch(/3.15.6/);
});
it('latest version is selected for provided minor release with x', async () => {
it('selects the latest version for provided minor release with x', async () => {
const version_info = await version.getAllVersionInfo();
const selected = await version.getLatestMatching('3.15.x', version_info);
const selected = version.getLatestMatching('3.15.x', version_info);
expect(selected.name).toMatch(/3.15.6/);
});
it('latest version is selected for provided major release with x', async () => {
it('selects the latest version for provided major release with x', async () => {
const version_info = await version.getAllVersionInfo();
const selected = await version.getLatestMatching('3.x', version_info);
const selected = version.getLatestMatching('3.x', version_info);
expect(selected.name).toMatch(/3.16.2/);
});
it('non-existent full version throws', async () => {
it('throws on non-existent full version', async () => {
const version_info = await version.getAllVersionInfo();
expect(() => {
version.getLatestMatching('100.0.0', version_info);
}).toThrow('Unable to find version matching 100.0.0');
});
it('non-existent part version throws', async () => {
it('throws on non-existent part version', async () => {
const version_info = await version.getAllVersionInfo();
expect(() => {
version.getLatestMatching('100.0.x', version_info);
}).toThrow('Unable to find version matching 100.0');
});
it('versions on second page get chosen', async () => {
it('select versions on second page', async () => {
const version_info = await version.getAllVersionInfo();
const selected = await version.getLatestMatching('2.x', version_info);
const selected = version.getLatestMatching('2.x', version_info);
expect(selected.name).toMatch(/2.8.12/);
});
it('extra numbers in version get ignored', async () => {
it('ignores extra numbers in version', async () => {
const version_info = await version.getAllVersionInfo();
const selected = await version.getLatestMatching('2.8.10', version_info);
const selected = version.getLatestMatching('2.8.10', version_info);
expect(selected.name).toMatch(/2.8.10/);
});
});

describe('Pulling from multipage results without Link header', () => {
// When the Link header is not available, we still want to be able to parse
// all pages. This could be done by iterating over all possible pages until
// we get no further results.
beforeEach(() => {
nock.disableNetConnect();
nock('https://api.github.com')
.get('/repos/Kitware/CMake/releases')
.replyWithFile(200, path.join(dataPath, 'releases.json'), {
'Content-Type': 'application/json',
});
nock('https://api.github.com')
.get('/repos/Kitware/CMake/releases')
.query({ page: 2 })
.replyWithFile(200, path.join(dataPath, 'releases2.json'), {
'Content-Type': 'application/json',
});
nock('https://api.github.com')
.get('/repos/Kitware/CMake/releases')
.query({ page: 3 })
.reply(200, []);
});
afterEach(() => {
nock.cleanAll();
nock.enableNetConnect();
});
it('selects exact version from first page', async () => {
const version_info = await version.getAllVersionInfo();
const selected = version.getLatestMatching('3.15.5', version_info);
expect(selected.name).toMatch(/3.15.5/);
});
it('throws on non-existent full version', async () => {
const version_info = await version.getAllVersionInfo();
expect(() => {
version.getLatestMatching('100.0.0', version_info);
}).toThrow('Unable to find version matching 100.0.0');
});
it('selects versions on second page', async () => {
const version_info = await version.getAllVersionInfo();
const selected = version.getLatestMatching('2.x', version_info);
expect(selected.name).toMatch(/2.8.12/);
});
});

describe('When api token is required', () => {
beforeEach(() => {
nock('https://api.github.com', {
Expand All @@ -94,14 +137,16 @@ describe('When api token is required', () => {
},
})
.get('/repos/Kitware/CMake/releases')
.query({ page: 1 })
.replyWithFile(200, path.join(dataPath, 'releases.json'), {
'Content-Type': 'application/json',
});
nock('https://api.github.com')
.get('/repos/Kitware/CMake/releases')
.query({ page: 1 })
.replyWithError('Invalid API token');
nock('https://api.github.com')
.get('/repos/Kitware/CMake/releases')
.query({ page: 2 })
.reply(200, []);
});
afterEach(() => {
nock.cleanAll();
Expand All @@ -124,33 +169,38 @@ describe('When api token is required', () => {
});

describe('When using macos 3.19.2 release', () => {
const releases = {
tag_name: 'v3.19.2',
assets: [
{
name: 'cmake-3.19.2-Linux-x86_64.tar.gz',
browser_download_url:
'https://fakeaddress/cmake-3.19.2-Linux-x86_64.tar.gz',
},
{
name: 'cmake-3.19.2-macos-universal.dmg',
browser_download_url:
'https://fakeaddress.com/cmake-3.19.2-macos-universal.dmg',
},
{
name: 'cmake-3.19.2-macos-universal.tar.gz',
browser_download_url:
'https://fakeaddress.com/cmake-3.19.2-macos-universal.tar.gz',
},
],
};
const releases = [
{
tag_name: 'v3.19.2',
assets: [
{
name: 'cmake-3.19.2-Linux-x86_64.tar.gz',
browser_download_url:
'https://fakeaddress/cmake-3.19.2-Linux-x86_64.tar.gz',
},
{
name: 'cmake-3.19.2-macos-universal.dmg',
browser_download_url:
'https://fakeaddress.com/cmake-3.19.2-macos-universal.dmg',
},
{
name: 'cmake-3.19.2-macos-universal.tar.gz',
browser_download_url:
'https://fakeaddress.com/cmake-3.19.2-macos-universal.tar.gz',
},
],
},
];

beforeEach(() => {
nock.disableNetConnect();
nock('https://api.github.com')
.get('/repos/Kitware/CMake/releases')
.query({ page: 1 })
.reply(200, releases);
nock('https://api.github.com')
.get('/repos/Kitware/CMake/releases')
.query({ page: 2 })
.reply(200, []);
});

afterEach(() => {
Expand Down Expand Up @@ -184,28 +234,33 @@ describe('When using macos 3.19.2 release', () => {
});

describe('When providing multiple different archs', () => {
const releases = {
tag_name: 'v3.19.3',
assets: [
{
name: 'cmake-3.19.3-Linux-aarch64.tar.gz',
browser_download_url:
'https://fakeaddress.com/cmake-3.19.3-Linux-aarch64.tar.gz',
},
{
name: 'cmake-3.19.3-Linux-x86_64.tar.gz',
browser_download_url:
'https://fakeaddress.com/cmake-3.19.3-Linux-x86_64.tar.gz',
},
],
};
const releases = [
{
tag_name: 'v3.19.3',
assets: [
{
name: 'cmake-3.19.3-Linux-aarch64.tar.gz',
browser_download_url:
'https://fakeaddress.com/cmake-3.19.3-Linux-aarch64.tar.gz',
},
{
name: 'cmake-3.19.3-Linux-x86_64.tar.gz',
browser_download_url:
'https://fakeaddress.com/cmake-3.19.3-Linux-x86_64.tar.gz',
},
],
},
];

beforeEach(() => {
nock.disableNetConnect();
nock('https://api.github.com')
.get('/repos/Kitware/CMake/releases')
.query({ page: 1 })
.reply(200, releases);
nock('https://api.github.com')
.get('/repos/Kitware/CMake/releases')
.query({ page: 2 })
.reply(200, []);
});

afterEach(() => {
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

87 changes: 65 additions & 22 deletions src/version.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as rest from 'typed-rest-client/RestClient';
import * as core from '@actions/core';
import * as semver from 'semver';
import * as vi from './version-info';

Expand Down Expand Up @@ -94,44 +95,86 @@ function convertToVersionInfo(versions: GitHubVersion[]): vi.VersionInfo[] {

function getHttpOptions(
api_token: string,
page_number: number
page_number: number = 1
): rest.IRequestOptions {
let options: rest.IRequestOptions = {
queryParameters: {
params: { page: page_number },
},
};
let options: rest.IRequestOptions = {};
options.additionalHeaders = { Accept: 'application/vnd.github.v3+json' };
if (page_number > 1) {
options.queryParameters = { params: { page: page_number } };
}
if (api_token) {
options.additionalHeaders.Authorization = 'token ' + api_token;
}
return options;
}

// Parse the pagination Link header to get the next url.
// The header has the form <...url...>; rel="...", <...>; rel="..."
function getNextFromLink(link: string): string | undefined {
const rLink = /<(?<url>[A-Za-z0-9_?=.\/:-]*?)>; rel="(?<rel>\w*?)"/g;
let match;
while ((match = rLink.exec(link)) != null) {
if (match.groups && /next/.test(match.groups.rel)) {
return match.groups.url;
}
}
return;
}

export async function getAllVersionInfo(
api_token: string = ''
): Promise<vi.VersionInfo[]> {
const client = new rest.RestClient(USER_AGENT);
let cur_page = 1;
let raw_versions: GitHubVersion[] = [];
let has_next_page = true;
while (has_next_page) {
const options = getHttpOptions(api_token, cur_page);
const version_response = await client.get<GitHubVersion[]>(
VERSION_URL,
options
);
const headers: { link?: string } = version_response.headers;
if (headers.link && headers.link.match(/rel="next"/)) {
has_next_page = true;
} else {
has_next_page = false;

// Fetch the first page of releases and use that to select the pagination
// method to use for all other pages.
const options = getHttpOptions(api_token);
const version_response = await client.get<GitHubVersion[]>(
VERSION_URL,
options
);
if (version_response.statusCode != 200 || !version_response.result) {
return [];
}
let raw_versions = version_response.result;
// Try to use the Link headers for pagination. If these are not available,
// then fall back to checking all pages until an empty page of results is
// returned.
let headers: { link?: string } = version_response.headers;
if (headers.link) {
core.debug(`Using link headers for pagination`);
let next = getNextFromLink(headers.link);
while (next) {
const options = getHttpOptions(api_token);
const version_response = await client.get<GitHubVersion[]>(next, options);
if (version_response.statusCode != 200 || !version_response.result) {
break;
}
raw_versions = raw_versions.concat(version_response.result);
headers = version_response.headers;
if (!headers.link) {
break;
}
next = getNextFromLink(headers.link);
}
if (version_response.result) {
} else {
core.debug(`Using page count for pagination`);
const max_pages = 20;
let cur_page = 2;
while (cur_page <= max_pages) {
const options = getHttpOptions(api_token, cur_page);
const version_response = await client.get<GitHubVersion[]>(
VERSION_URL,
options
);
if (!version_response.result || version_response.result.length == 0) {
break;
}
raw_versions = raw_versions.concat(version_response.result);
cur_page++;
}
cur_page++;
}
core.debug(`overall got ${raw_versions.length} versions`);
const versions: vi.VersionInfo[] = convertToVersionInfo(raw_versions);
return versions;
}
Expand Down