diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 8d5e276092b6bb36d82d9c5b96c7bab9353b1242..45fce42d3fb445bbca8d3f77e3c986278f30e395 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -484,6 +484,12 @@ Use the `extends` field instead of this if, for example, you need the ability fo When Renovate resolves `globalExtends` it does not fully process the configuration. This means that Renovate does not have the authentication it needs to fetch private things. +## includeMirrors + +By default, Renovate does not autodiscover repositories that are mirrors. + +Change this setting to `true` to include repositories that are mirrors as Renovate targets. + ## logContext `logContext` is included with each log entry only if `logFormat="json"` - it is not included in the pretty log output. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index a33a491375e8d4edb7271d560a8134d81f54230b..d975d386f2f57d89ad4fe7bd83f90b01ba5d2242 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -441,6 +441,15 @@ const options: RenovateOptions[] = [ allowedValues: ['auto', 'enabled', 'disabled'], default: 'auto', }, + { + name: 'includeMirrors', + description: + 'Whether to process repositories that are mirrors. By default, repositories that are mirrors are skipped.', + type: 'boolean', + default: false, + supportedPlatforms: ['gitlab'], + globalOnly: true, + }, { name: 'forkToken', description: 'Set a personal access token here to enable "fork mode".', diff --git a/lib/config/types.ts b/lib/config/types.ts index e597c7217d1b22064fc4ffe6dc18e991fef84f33..100bfb96aef8a3d960e700d9d0c3b133c641cf00 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -143,6 +143,7 @@ export interface RepoGlobalConfig { containerbaseDir?: string; platform?: PlatformId; endpoint?: string; + includeMirrors?: boolean; } export interface LegacyAdminConfig { diff --git a/lib/modules/platform/gitlab/index.spec.ts b/lib/modules/platform/gitlab/index.spec.ts index 462978f26c6c5753c5201da0cf056e916cb75110..55d72e2778dc0e224b7893c29b3c8afc90bc5f79 100644 --- a/lib/modules/platform/gitlab/index.spec.ts +++ b/lib/modules/platform/gitlab/index.spec.ts @@ -168,6 +168,28 @@ describe('modules/platform/gitlab/index', () => { expect(repos).toEqual(['a/b', 'c/d']); }); + it('should return an array of repos including mirrors', async () => { + httpMock + .scope(gitlabApiHost) + .get( + '/api/v4/projects?membership=true&per_page=100&with_merge_requests_enabled=true&min_access_level=30&archived=false' + ) + .reply(200, [ + { + path_with_namespace: 'a/b', + }, + { + path_with_namespace: 'c/d', + }, + { + path_with_namespace: 'c/f', + mirror: true, + }, + ]); + const repos = await gitlab.getRepos({ includeMirrors: true }); + expect(repos).toEqual(['a/b', 'c/d', 'c/f']); + }); + it('should encode the requested topics into the URL', async () => { httpMock .scope(gitlabApiHost) @@ -262,6 +284,26 @@ describe('modules/platform/gitlab/index', () => { ).rejects.toThrow(REPOSITORY_MIRRORED); }); + it('should not throw an error if repository is a mirror when includeMirrors option is set', async () => { + httpMock + .scope(gitlabApiHost) + .get('/api/v4/projects/some%2Frepo') + .reply(200, { + default_branch: 'master', + mirror: true, + }); + expect( + await gitlab.initRepo({ + repository: 'some/repo', + includeMirrors: true, + }) + ).toEqual({ + defaultBranch: 'master', + isFork: false, + repoFingerprint: expect.any(String), + }); + }); + it('should throw an error if repository access is disabled', async () => { httpMock .scope(gitlabApiHost) diff --git a/lib/modules/platform/gitlab/index.ts b/lib/modules/platform/gitlab/index.ts index 523581a22e95b0fc1ec6cd38ae3b1a818f5295cf..866b5d0f61ec75499e65799d13126cbe3d1fb37e 100644 --- a/lib/modules/platform/gitlab/index.ts +++ b/lib/modules/platform/gitlab/index.ts @@ -169,7 +169,7 @@ export async function getRepos(config?: AutodiscoverConfig): Promise<string[]> { }); logger.debug(`Discovered ${res.body.length} project(s)`); return res.body - .filter((repo) => !repo.mirror) + .filter((repo) => !repo.mirror || config?.includeMirrors) .map((repo) => repo.path_with_namespace); } catch (err) { logger.error({ err }, `GitLab getRepos error`); @@ -267,6 +267,7 @@ export async function initRepo({ ignorePrAuthor, gitUrl, endpoint, + includeMirrors, }: RepoParams): Promise<RepoResult> { config = {} as any; config.repository = urlEscape(repository); @@ -284,7 +285,8 @@ export async function initRepo({ ); throw new Error(REPOSITORY_ARCHIVED); } - if (res.body.mirror) { + + if (res.body.mirror && includeMirrors !== true) { logger.debug( 'Repository is a mirror - throwing error to abort renovation' ); diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index e96f96286a3640867552f17b0a199637bacf32dc..0316f8ff03591af8377260c3d71e25ea5cd8f2f0 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -44,6 +44,7 @@ export interface RepoParams { cloneSubmodules?: boolean; ignorePrAuthor?: boolean; bbUseDevelopmentBranch?: boolean; + includeMirrors?: boolean; } export interface PrDebugData { @@ -166,6 +167,7 @@ export type EnsureIssueResult = 'updated' | 'created'; export interface AutodiscoverConfig { topics?: string[]; + includeMirrors?: boolean; } export interface Platform { diff --git a/lib/workers/global/autodiscover.ts b/lib/workers/global/autodiscover.ts index ed6e92dc817e29d572ed504cca4c12ec604492ca..5d1963ffaaa9b11e92f352f0f1ce38bc3985ff70 100644 --- a/lib/workers/global/autodiscover.ts +++ b/lib/workers/global/autodiscover.ts @@ -37,6 +37,7 @@ export async function autodiscoverRepositories( // Autodiscover list of repositories let discovered = await platform.getRepos({ topics: config.autodiscoverTopics, + includeMirrors: config.includeMirrors, }); if (!discovered?.length) { // Soft fail (no error thrown) if no accessible repositories