diff --git a/docs/usage/getting-started/private-packages.md b/docs/usage/getting-started/private-packages.md index 0dfa4e59d7afd85f2deb7f05250266fe52d730cf..451cb54b4ecb64846ed2c31f77b2f8d34ac24f43 100644 --- a/docs/usage/getting-started/private-packages.md +++ b/docs/usage/getting-started/private-packages.md @@ -88,6 +88,62 @@ Here is an example of some host rules: Renovate applies theses `hostRules` to every HTTP(s) request which is sent, so they are largely independent of any platform or datasource logic. With `hostRules` in place, private package lookups should all work. +### GitHub (and Enterprise) repo scoped credentials + +If you need to use different credentials for a specific GitHub repo, then you can configure `hostRules` like one of the following: + +```json +{ + "hostRules": [ + { + "matchHost": "https://api.github.com/repos/org/repo", + "token": "abc123" + }, + { + "matchHost": "https://github.domain.com/api/v3/repos/org/repo", + "token": "abc123" + } + ] +} +``` + +Renovate will use those credentials for all requests to `org/repo`. + +#### Example for gomod + +Here's an example for `gomod` with private github.com repos. +Assume this config is used on the `github.com/some-other-org` repo: + +```json +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "dependencyDashboard": true, + "hostRules": [ + { + "matchHost": "https://gitlab.com", + "token": "glpat-token_for_different_git_platform", + "hostType": "gitlab" + }, + { + "matchHost": "https://github.com/some-org", + "token": "ghp_token_for_different_org", + "hostType": "go" + }, + { + "matchHost": "https://api.github.com/repos/some-org", + "token": "ghp_token_for_different_org", + "hostType": "github" + } + ], + "customEnvVariables": { + "GOPRIVATE": "github.com/some-org,github.com/some-other-org,gitlab.com/some-org", + "GONOSUMDB": "github.com/some-org,github.com/some-other-org,gitlab.com/some-org", + "GONOPROXY": "github.com/some-org,github.com/some-other-org,gitlab.com/some-org" + }, + "postUpdateOptions": ["gomodTidy"] +} +``` + ## Looking up Release Notes When Renovate creates Pull Requests, its default behavior is to locate and embed release notes/changelogs of packages. diff --git a/lib/modules/datasource/github-releases/cache/cache-base.ts b/lib/modules/datasource/github-releases/cache/cache-base.ts index c9cb5081f4df5d4fe809110846ffb30560ef63f2..998c3164d83d9c9345a958812149f3009f8dd507 100644 --- a/lib/modules/datasource/github-releases/cache/cache-base.ts +++ b/lib/modules/datasource/github-releases/cache/cache-base.ts @@ -4,6 +4,7 @@ import * as packageCache from '../../../../util/cache/package'; import type { GithubGraphqlResponse, GithubHttp, + GithubHttpOptions, } from '../../../../util/http/github'; import type { GetReleasesConfig } from '../../types'; import { getApiBaseUrl } from '../common'; @@ -164,12 +165,14 @@ export abstract class AbstractGithubDatasourceCache< private async query( baseUrl: string, - variables: GithubQueryParams + variables: GithubQueryParams, + options: GithubHttpOptions ): Promise<QueryResponse<FetchedItem> | Error> { try { const graphqlRes = await this.http.postJson< GithubGraphqlResponse<QueryResponse<FetchedItem>> >('/graphql', { + ...options, baseUrl, body: { query: this.graphqlQuery, variables }, }); @@ -264,7 +267,9 @@ export abstract class AbstractGithubDatasourceCache< : this.maxPrefetchPages; let stopIteration = false; while (pagesRemained > 0 && !stopIteration) { - const res = await this.query(baseUrl, variables); + const res = await this.query(baseUrl, variables, { + repository: packageName, + }); if (res instanceof Error) { if ( res.message.startsWith( diff --git a/lib/util/http/github.spec.ts b/lib/util/http/github.spec.ts index 1e57db28ecab3e960552b42335ac0ea7f8ea4c37..6096fc01d18d441decaeca187d1afe757350e400 100644 --- a/lib/util/http/github.spec.ts +++ b/lib/util/http/github.spec.ts @@ -138,6 +138,83 @@ describe('util/http/github', () => { expect(res.body.the_field).toEqual(['a', 'b', 'c', 'd']); }); + it('paginates with auth and repo', async () => { + const url = '/some-url?per_page=2'; + hostRules.add({ + hostType: 'github', + token: 'test', + matchHost: 'github.com', + }); + hostRules.add({ + hostType: 'github', + token: 'abc', + matchHost: 'https://api.github.com/repos/some/repo', + }); + httpMock + .scope(githubApiHost, { + reqheaders: { + authorization: 'token abc', + accept: 'application/vnd.github.v3+json', + }, + }) + .get(url) + .reply(200, ['a', 'b'], { + link: `<${url}&page=2>; rel="next", <${url}&page=3>; rel="last"`, + }) + .get(`${url}&page=2`) + .reply(200, ['c', 'd'], { + link: `<${url}&page=3>; rel="next", <${url}&page=3>; rel="last"`, + }) + .get(`${url}&page=3`) + .reply(200, ['e']); + const res = await githubApi.getJson(url, { + paginate: true, + repository: 'some/repo', + }); + expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']); + }); + + it('paginates with auth and repo on GHE', async () => { + const url = '/api/v3/some-url?per_page=2'; + hostRules.add({ + hostType: 'github', + token: 'test', + matchHost: 'github.domain.com', + }); + hostRules.add({ + hostType: 'github', + token: 'abc', + matchHost: 'https://github.domain.com/api/v3/repos/some/repo', + }); + httpMock + .scope('https://github.domain.com', { + reqheaders: { + authorization: 'token abc', + accept: + 'application/vnd.github.antiope-preview+json, application/vnd.github.v3+json', + }, + }) + .get(url) + .reply(200, ['a', 'b'], { + link: `<${url}&page=2>; rel="next", <${url}&page=3>; rel="last"`, + }) + .get(`${url}&page=2`) + .reply(200, ['c', 'd'], { + link: `<${url}&page=3>; rel="next", <${url}&page=3>; rel="last"`, + }) + .get(`${url}&page=3`) + .reply(200, ['e']); + const res = await githubApi.getJson(url, { + paginate: true, + repository: 'some/repo', + baseUrl: 'https://github.domain.com', + headers: { + accept: 'application/vnd.github.antiope-preview+json', + }, + }); + expect(res.body).toEqual(['a', 'b', 'c', 'd', 'e']); + }); + it('attempts to paginate', async () => { const url = '/some-url'; httpMock diff --git a/lib/util/http/github.ts b/lib/util/http/github.ts index 45021b32d48aa73af2df45b3cb3b5e58040f5c83..1350088968a18aa210b360c902436084a1dbad2b 100644 --- a/lib/util/http/github.ts +++ b/lib/util/http/github.ts @@ -14,7 +14,8 @@ import { getCache } from '../cache/repository'; import { maskToken } from '../mask'; import { range } from '../range'; import { regEx } from '../regex'; -import { parseLinkHeader } from '../url'; +import { joinUrlParts, parseLinkHeader, resolveBaseUrl } from '../url'; +import { findMatchingRules } from './host-rules'; import type { GotLegacyError } from './legacy'; import type { GraphqlOptions, @@ -35,6 +36,7 @@ export interface GithubHttpOptions extends HttpOptions { paginate?: boolean | string; paginationField?: string; pageLimit?: number; + repository?: string; } interface GithubGraphqlRepoData<T = unknown> { @@ -170,9 +172,12 @@ function constructAcceptString(input?: any): string { const defaultAccept = 'application/vnd.github.v3+json'; const acceptStrings = typeof input === 'string' ? input.split(regEx(/\s*,\s*/)) : []; + + // TODO: regression of #6736 if ( - !acceptStrings.some((x) => x.startsWith('application/vnd.github.')) || - acceptStrings.length < 2 + !acceptStrings.some((x) => x === defaultAccept) && + (!acceptStrings.some((x) => x.startsWith('application/vnd.github.')) || + acceptStrings.length < 2) ) { acceptStrings.push(defaultAccept); } @@ -273,12 +278,33 @@ export class GithubHttp extends Http<GithubHttpOptions, GithubHttpOptions> { options?: InternalHttpOptions & GithubHttpOptions, okToRetry = true ): Promise<HttpResponse<T>> { - const opts = { + const opts: GithubHttpOptions = { baseUrl, ...options, throwHttpErrors: true, }; + if (!opts.token) { + const authUrl = new URL(resolveBaseUrl(opts.baseUrl!, url)); + + if (opts.repository) { + // set authUrl to https://api.github.com/repos/org/repo or https://gihub.domain.com/api/v3/repos/org/repo + authUrl.hash = ''; + authUrl.search = ''; + authUrl.pathname = joinUrlParts( + authUrl.pathname.startsWith('/api/v3') ? '/api/v3' : '', + 'repos', + `${opts.repository}` + ); + } + + const { token } = findMatchingRules( + { hostType: this.hostType }, + authUrl.toString() + ); + opts.token = token; + } + const accept = constructAcceptString(opts.headers?.accept); opts.headers = { @@ -300,7 +326,7 @@ export class GithubHttp extends Http<GithubHttpOptions, GithubHttpOptions> { } const queue = [...range(2, lastPage)].map( (pageNumber) => (): Promise<HttpResponse<T>> => { - const nextUrl = new URL(linkHeader.next.url, baseUrl); + const nextUrl = new URL(linkHeader.next.url, opts.baseUrl); nextUrl.searchParams.set('page', String(pageNumber)); return this.request<T>( nextUrl, diff --git a/lib/util/http/host-rules.ts b/lib/util/http/host-rules.ts index 92d23a7df83ec7ce7134a26c1b0572958afe7656..2e829499732671d2a88281bf87565daeab893807 100644 --- a/lib/util/http/host-rules.ts +++ b/lib/util/http/host-rules.ts @@ -10,7 +10,7 @@ import type { HostRule } from '../../types'; import * as hostRules from '../host-rules'; import type { GotOptions } from './types'; -function findMatchingRules(options: GotOptions, url: string): HostRule { +export function findMatchingRules(options: GotOptions, url: string): HostRule { const { hostType } = options; let res = hostRules.find({ hostType, url }); diff --git a/lib/util/http/index.ts b/lib/util/http/index.ts index ce83378947e75d024ef9ad03189e8ff31f59b659..21bb432567af5196a8ed90fd3614a82f58c009ca 100644 --- a/lib/util/http/index.ts +++ b/lib/util/http/index.ts @@ -95,7 +95,7 @@ async function gotRoutine<T>( export class Http<GetOptions = HttpOptions, PostOptions = HttpPostOptions> { private options?: GotOptions; - constructor(private hostType: string, options: HttpOptions = {}) { + constructor(protected hostType: string, options: HttpOptions = {}) { this.options = merge<GotOptions>(options, { context: { hostType } }); }