Skip to content

Commit

Permalink
feat(platform/github): Support rewrite pagination links with the serv…
Browse files Browse the repository at this point in the history
…er's base URL (#19888)

Co-authored-by: Sergei Zharinov <zharinov@users.noreply.github.com>
Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
  • Loading branch information
4 people authored Feb 16, 2023
1 parent 45dd234 commit 0e47a10
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 2 deletions.
8 changes: 8 additions & 0 deletions docs/usage/self-hosted-experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ Allowed values:

Default value: `asc`.

## `RENOVATE_X_REBASE_PAGINATION_LINKS`

If set, Renovate will rewrite GitHub Enterprise Server's pagination responses to use the `endpoint` URL from the Renovate config.

<!-- prettier-ignore -->
!!! note
For the GitHub Enterprise Server platform only.

## `OTEL_EXPORTER_OTLP_ENDPOINT`

If set, Renovate will export OpenTelemetry data to the supplied endpoint.
Expand Down
70 changes: 70 additions & 0 deletions lib/util/http/github.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('util/http/github', () => {
let repoCache: RepoCacheData = {};

beforeEach(() => {
delete process.env.RENOVATE_X_REBASE_PAGINATION_LINKS;
githubApi = new GithubHttp();
setBaseUrl(githubApiHost);
jest.resetAllMocks();
Expand Down Expand Up @@ -229,6 +230,75 @@ describe('util/http/github', () => {
expect(res.body).toEqual(['a']);
});

it('rebases GHE Server pagination links', async () => {
process.env.RENOVATE_X_REBASE_PAGINATION_LINKS = '1';
// The origin and base URL which Renovate uses (from its config) to reach GHE:
const baseUrl = 'http://ghe.alternative.domain.com/api/v3';
setBaseUrl(baseUrl);
// The hostname from GHE settings, which users use through their browsers to reach GHE:
// https://docs.github.com/en/enterprise-server@3.5/admin/configuration/configuring-network-settings/configuring-a-hostname
const gheHostname = 'ghe.mycompany.com';
// GHE replies to paginated requests with a Link response header whose URLs have this base
const gheBaseUrl = `https://${gheHostname}/api/v3`;
const apiUrl = '/some-url?per_page=2';
httpMock
.scope(baseUrl)
.get(apiUrl)
.reply(200, ['a', 'b'], {
link: `<${gheBaseUrl}${apiUrl}&page=2>; rel="next", <${gheBaseUrl}${apiUrl}&page=3>; rel="last"`,
})
.get(`${apiUrl}&page=2`)
.reply(200, ['c', 'd'], {
link: `<${gheBaseUrl}${apiUrl}&page=3>; rel="next", <${gheBaseUrl}${apiUrl}&page=3>; rel="last"`,
})
.get(`${apiUrl}&page=3`)
.reply(200, ['e']);
const res = await githubApi.getJson(apiUrl, { paginate: true });
expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']);
});

it('preserves pagination links by default', async () => {
const baseUrl = 'http://ghe.alternative.domain.com/api/v3';
setBaseUrl(baseUrl);
const apiUrl = '/some-url?per_page=2';
httpMock
.scope(baseUrl)
.get(apiUrl)
.reply(200, ['a', 'b'], {
link: `<${baseUrl}${apiUrl}&page=2>; rel="next", <${baseUrl}${apiUrl}&page=3>; rel="last"`,
})
.get(`${apiUrl}&page=2`)
.reply(200, ['c', 'd'], {
link: `<${baseUrl}${apiUrl}&page=3>; rel="next", <${baseUrl}${apiUrl}&page=3>; rel="last"`,
})
.get(`${apiUrl}&page=3`)
.reply(200, ['e']);
const res = await githubApi.getJson(apiUrl, { paginate: true });
expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']);
});

it('preserves pagination links for github.com', async () => {
process.env.RENOVATE_X_REBASE_PAGINATION_LINKS = '1';
const baseUrl = 'https://api.github.com/';

setBaseUrl(baseUrl);
const apiUrl = 'some-url?per_page=2';
httpMock
.scope(baseUrl)
.get('/' + apiUrl)
.reply(200, ['a', 'b'], {
link: `<${baseUrl}${apiUrl}&page=2>; rel="next", <${baseUrl}${apiUrl}&page=3>; rel="last"`,
})
.get(`/${apiUrl}&page=2`)
.reply(200, ['c', 'd'], {
link: `<${baseUrl}${apiUrl}&page=3>; rel="next", <${baseUrl}${apiUrl}&page=3>; rel="last"`,
})
.get(`/${apiUrl}&page=3`)
.reply(200, ['e']);
const res = await githubApi.getJson(apiUrl, { paginate: true });
expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']);
});

describe('handleGotError', () => {
async function fail(
code: number,
Expand Down
21 changes: 19 additions & 2 deletions lib/util/http/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ function setGraphqlPageSize(fieldName: string, newPageSize: number): void {
}
}

function replaceUrlBase(url: URL, baseUrl: string): URL {
const relativeUrl = `${url.pathname}${url.search}`;
return new URL(relativeUrl, baseUrl);
}

export class GithubHttp extends Http<GithubHttpOptions> {
constructor(hostType = 'github', options?: GithubHttpOptions) {
super(hostType, options);
Expand Down Expand Up @@ -309,15 +314,27 @@ export class GithubHttp extends Http<GithubHttpOptions> {
// Check if result is paginated
const pageLimit = opts.pageLimit ?? 10;
const linkHeader = parseLinkHeader(result?.headers?.link);
if (linkHeader?.next?.url && linkHeader?.last?.page) {
const next = linkHeader?.next;
if (next?.url && linkHeader?.last?.page) {
let lastPage = parseInt(linkHeader.last.page, 10);
// istanbul ignore else: needs a test
if (!process.env.RENOVATE_PAGINATE_ALL && opts.paginate !== 'all') {
lastPage = Math.min(pageLimit, lastPage);
}
const baseUrl = opts.baseUrl;
const parsedUrl = new URL(next.url, baseUrl);
const rebasePagination =
!!baseUrl &&
!!process.env.RENOVATE_X_REBASE_PAGINATION_LINKS &&
// Preserve github.com URLs for use cases like release notes
parsedUrl.origin !== 'https://api.github.com';
const firstPageUrl = rebasePagination
? replaceUrlBase(parsedUrl, baseUrl)
: parsedUrl;
const queue = [...range(2, lastPage)].map(
(pageNumber) => (): Promise<HttpResponse<T>> => {
const nextUrl = new URL(linkHeader.next!.url, opts.baseUrl);
// copy before modifying searchParams
const nextUrl = new URL(firstPageUrl);
nextUrl.searchParams.set('page', String(pageNumber));
return this.request<T>(
nextUrl,
Expand Down

0 comments on commit 0e47a10

Please sign in to comment.