From e6453ae4231f6b63f6bdd44bc0082fd3ff37f0c9 Mon Sep 17 00:00:00 2001 From: Florian Greinacher <fgreinacher@users.noreply.github.com> Date: Tue, 14 Apr 2020 07:05:30 +0200 Subject: [PATCH] feat(presets): add support for local presets (#5814) --- docs/usage/config-presets.md | 4 + lib/config/common.ts | 4 +- lib/config/index.ts | 3 +- .../presets/__snapshots__/index.spec.ts.snap | 18 +++++ .../presets/__snapshots__/local.spec.ts.snap | 75 +++++++++++++++++++ lib/config/presets/github.spec.ts | 22 +++++- lib/config/presets/github.ts | 26 +++++-- lib/config/presets/gitlab.spec.ts | 17 +++++ lib/config/presets/gitlab.ts | 22 ++++-- lib/config/presets/index.spec.ts | 29 ++++--- lib/config/presets/index.ts | 25 ++++++- lib/config/presets/local.spec.ts | 60 +++++++++++++++ lib/config/presets/local.ts | 20 +++++ lib/config/validation.ts | 3 +- lib/platform/azure/index.ts | 3 +- lib/platform/bitbucket-server/index.ts | 3 +- lib/platform/gitea/index.ts | 4 +- lib/platform/github/index.ts | 3 +- lib/platform/gitlab/index.ts | 3 +- lib/util/url.ts | 3 + lib/workers/repository/init/config.ts | 2 +- 21 files changed, 313 insertions(+), 36 deletions(-) create mode 100644 lib/config/presets/__snapshots__/local.spec.ts.snap create mode 100644 lib/config/presets/local.spec.ts create mode 100644 lib/config/presets/local.ts create mode 100644 lib/util/url.ts diff --git a/docs/usage/config-presets.md b/docs/usage/config-presets.md index 6d44b3ae2f..5b8907d5c4 100644 --- a/docs/usage/config-presets.md +++ b/docs/usage/config-presets.md @@ -168,6 +168,10 @@ To host your preset config on GitLab: Note: Unlike npmjs-hosted presets, GitLab-hosted ones can contain only one config. +## Local presets + +Renovate also supports local presets, i.e. presets that are hosted on the same platform as the target repository. This is especially helpful in self-hosted scenarios where public presets cannot be used. Local presets are only supported on GitHub and GitLab. Local presets are specified either by leaving out any prefix, e.g. `owner/name`, or explicitly by adding a `local>` prefix, e.g. `local>owner/name`. Renovate will determine the current platform and look up the preset from there. + ## Presets and Private Modules Using your own preset config along with private npm modules can present a chicken and egg problem. You want to configure the encrypted token just once, which means in the preset. But you also probably want the preset to be private too, so how can the other repos reference it? diff --git a/lib/config/common.ts b/lib/config/common.ts index 59e162a31f..2e6469e29b 100644 --- a/lib/config/common.ts +++ b/lib/config/common.ts @@ -67,6 +67,8 @@ export interface RenovateAdminConfig { configWarningReuseIssue?: boolean; dryRun?: boolean; + endpoint?: string; + global?: GlobalConfig; localDir?: string; @@ -80,6 +82,7 @@ export interface RenovateAdminConfig { onboardingPrTitle?: string; onboardingConfig?: RenovateSharedConfig; + platform?: string; postUpdateOptions?: string[]; privateKey?: string | Buffer; repositories?: RenovateRepository[]; @@ -121,7 +124,6 @@ export interface RenovateConfig branchList?: string[]; description?: string[]; - endpoint?: string; errors?: ValidationMessage[]; extends?: string[]; diff --git a/lib/config/index.ts b/lib/config/index.ts index 047bc94f57..041950ad6c 100644 --- a/lib/config/index.ts +++ b/lib/config/index.ts @@ -8,6 +8,7 @@ import { resolveConfigPresets } from './presets'; import { get, getLanguageList, getManagerList } from '../manager'; import { RenovateConfig, RenovateConfigStage } from './common'; import { mergeChildConfig } from './utils'; +import { ensureTrailingSlash } from '../util/url'; export * from './common'; export { mergeChildConfig }; @@ -102,7 +103,7 @@ export async function parseConfigs( // Massage endpoint to have a trailing slash if (config.endpoint) { logger.debug('Adding trailing slash to endpoint'); - config.endpoint = config.endpoint.replace(/\/?$/, '/'); + config.endpoint = ensureTrailingSlash(config.endpoint); } // Remove log file entries diff --git a/lib/config/presets/__snapshots__/index.spec.ts.snap b/lib/config/presets/__snapshots__/index.spec.ts.snap index 395529ede7..a33b18cacd 100644 --- a/lib/config/presets/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/__snapshots__/index.spec.ts.snap @@ -98,6 +98,24 @@ Object { } `; +exports[`config/presets parsePreset parses local 1`] = ` +Object { + "packageName": "some/repo", + "params": undefined, + "presetName": "default", + "presetSource": "local", +} +`; + +exports[`config/presets parsePreset parses no prefix as local 1`] = ` +Object { + "packageName": "some/repo", + "params": undefined, + "presetName": "default", + "presetSource": "local", +} +`; + exports[`config/presets parsePreset returns default package name 1`] = ` Object { "packageName": "renovate-config-default", diff --git a/lib/config/presets/__snapshots__/local.spec.ts.snap b/lib/config/presets/__snapshots__/local.spec.ts.snap new file mode 100644 index 0000000000..8cbbc16a24 --- /dev/null +++ b/lib/config/presets/__snapshots__/local.spec.ts.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`config/presets/local getPreset() forwards to custom github 1`] = ` +Array [ + Array [ + "some/repo", + "", + Object { + "endpoint": "https://api.github.example.com", + "platform": "GitHub", + }, + ], +] +`; + +exports[`config/presets/local getPreset() forwards to custom github 2`] = ` +Object { + "resolved": "preset", +} +`; + +exports[`config/presets/local getPreset() forwards to custom gitlab 1`] = ` +Array [ + Array [ + "some/repo", + "", + Object { + "endpoint": "https://gitlab.example.com/api/v4", + "platform": "gitlab", + }, + ], +] +`; + +exports[`config/presets/local getPreset() forwards to custom gitlab 2`] = ` +Object { + "resolved": "preset", +} +`; + +exports[`config/presets/local getPreset() forwards to github 1`] = ` +Array [ + Array [ + "some/repo", + "", + Object { + "platform": "github", + }, + ], +] +`; + +exports[`config/presets/local getPreset() forwards to github 2`] = ` +Object { + "resolved": "preset", +} +`; + +exports[`config/presets/local getPreset() forwards to gitlab 1`] = ` +Array [ + Array [ + "some/repo", + "", + Object { + "platform": "GitLab", + }, + ], +] +`; + +exports[`config/presets/local getPreset() forwards to gitlab 2`] = ` +Object { + "resolved": "preset", +} +`; diff --git a/lib/config/presets/github.spec.ts b/lib/config/presets/github.spec.ts index dac3a47c80..04680ba5f0 100644 --- a/lib/config/presets/github.spec.ts +++ b/lib/config/presets/github.spec.ts @@ -11,7 +11,10 @@ const got: any = _got; const hostRules: any = _hostRules; describe('config/presets/github', () => { - beforeEach(() => global.renovateCache.rmAll()); + beforeEach(() => { + got.mockReset(); + return global.renovateCache.rmAll(); + }); describe('getPreset()', () => { it('passes up platform-failure', async () => { got.mockImplementationOnce(() => { @@ -66,5 +69,22 @@ describe('config/presets/github', () => { delete global.appMode; } }); + + it('uses default endpoint', async () => { + await github.getPreset('some/repo', 'default').catch((_) => {}); + expect(got.mock.calls[0][0]).toEqual( + 'https://api.github.com/repos/some/repo/contents/default.json' + ); + }); + it('uses custom endpoint', async () => { + await github + .getPreset('some/repo', 'default', { + endpoint: 'https://api.github.example.org', + }) + .catch((_) => {}); + expect(got.mock.calls[0][0]).toEqual( + 'https://api.github.example.org/repos/some/repo/contents/default.json' + ); + }); }); }); diff --git a/lib/config/presets/github.ts b/lib/config/presets/github.ts index addef24b7a..365294b7c8 100644 --- a/lib/config/presets/github.ts +++ b/lib/config/presets/github.ts @@ -2,12 +2,18 @@ import { logger } from '../../logger'; import { Preset } from './common'; import { Http, HttpOptions } from '../../util/http'; import { PLATFORM_FAILURE } from '../../constants/error-messages'; +import { ensureTrailingSlash } from '../../util/url'; +import { RenovateConfig } from '../common'; const id = 'github'; const http = new Http(id); -async function fetchJSONFile(repo: string, fileName: string): Promise<Preset> { - const url = `https://api.github.com/repos/${repo}/contents/${fileName}`; +async function fetchJSONFile( + repo: string, + fileName: string, + endpoint: string +): Promise<Preset> { + const url = `${endpoint}repos/${repo}/contents/${fileName}`; const opts: HttpOptions = { headers: { accept: global.appMode @@ -39,11 +45,19 @@ async function fetchJSONFile(repo: string, fileName: string): Promise<Preset> { export async function getPreset( pkgName: string, - presetName = 'default' + presetName = 'default', + baseConfig?: RenovateConfig ): Promise<Preset> { + const endpoint = ensureTrailingSlash( + baseConfig?.endpoint ?? 'https://api.github.com/' + ); if (presetName === 'default') { try { - const defaultJson = await fetchJSONFile(pkgName, 'default.json'); + const defaultJson = await fetchJSONFile( + pkgName, + 'default.json', + endpoint + ); return defaultJson; } catch (err) { if (err.message === PLATFORM_FAILURE) { @@ -51,10 +65,10 @@ export async function getPreset( } if (err.message === 'dep not found') { logger.debug('default.json preset not found - trying renovate.json'); - return fetchJSONFile(pkgName, 'renovate.json'); + return fetchJSONFile(pkgName, 'renovate.json', endpoint); } throw err; } } - return fetchJSONFile(pkgName, `${presetName}.json`); + return fetchJSONFile(pkgName, `${presetName}.json`, endpoint); } diff --git a/lib/config/presets/gitlab.spec.ts b/lib/config/presets/gitlab.spec.ts index dc503daddd..f9f3373bb4 100644 --- a/lib/config/presets/gitlab.spec.ts +++ b/lib/config/presets/gitlab.spec.ts @@ -9,6 +9,7 @@ const glGot: jest.Mock<Promise<Partial<GotResponse>>> = api.get as never; describe('config/presets/gitlab', () => { beforeEach(() => { + glGot.mockReset(); global.repoCache = {}; return global.renovateCache.rmAll(); }); @@ -52,5 +53,21 @@ describe('config/presets/gitlab', () => { const content = await gitlab.getPreset('some/repo'); expect(content).toEqual({ foo: 'bar' }); }); + it('uses default endpoint', async () => { + await gitlab.getPreset('some/repo', 'default').catch((_) => {}); + expect(glGot.mock.calls[0][0]).toEqual( + 'https://gitlab.com/api/v4/projects/some%2Frepo/repository/branches' + ); + }); + it('uses custom endpoint', async () => { + await gitlab + .getPreset('some/repo', 'default', { + endpoint: 'https://gitlab.example.org/api/v4', + }) + .catch((_) => {}); + expect(glGot.mock.calls[0][0]).toEqual( + 'https://gitlab.example.org/api/v4/projects/some%2Frepo/repository/branches' + ); + }); }); }); diff --git a/lib/config/presets/gitlab.ts b/lib/config/presets/gitlab.ts index 1b8cc1cc8c..b765c773e6 100644 --- a/lib/config/presets/gitlab.ts +++ b/lib/config/presets/gitlab.ts @@ -1,15 +1,16 @@ import { api } from '../../platform/gitlab/gl-got-wrapper'; import { logger } from '../../logger'; import { Preset } from './common'; +import { ensureTrailingSlash } from '../../util/url'; +import { RenovateConfig } from '../common'; const { get: glGot } = api; -const GitLabApiUrl = 'https://gitlab.com/api/v4/projects'; - async function getDefaultBranchName( - urlEncodedPkgName: string + urlEncodedPkgName: string, + endpoint: string ): Promise<string> { - const branchesUrl = `${GitLabApiUrl}/${urlEncodedPkgName}/repository/branches`; + const branchesUrl = `${endpoint}projects/${urlEncodedPkgName}/repository/branches`; type GlBranch = { default: boolean; name: string; @@ -30,8 +31,12 @@ async function getDefaultBranchName( export async function getPreset( pkgName: string, - presetName = 'default' + presetName = 'default', + baseConfig?: RenovateConfig ): Promise<Preset> { + const endpoint = ensureTrailingSlash( + baseConfig?.endpoint ?? 'https://gitlab.com/api/v4/' + ); if (presetName !== 'default') { // TODO: proper error contructor throw new Error( @@ -42,9 +47,12 @@ export async function getPreset( let res: string; try { const urlEncodedPkgName = encodeURIComponent(pkgName); - const defautlBranchName = await getDefaultBranchName(urlEncodedPkgName); + const defautlBranchName = await getDefaultBranchName( + urlEncodedPkgName, + endpoint + ); - const presetUrl = `${GitLabApiUrl}/${urlEncodedPkgName}/repository/files/renovate.json?ref=${defautlBranchName}`; + const presetUrl = `${endpoint}projects/${urlEncodedPkgName}/repository/files/renovate.json?ref=${defautlBranchName}`; res = Buffer.from( (await glGot(presetUrl)).body.content, 'base64' diff --git a/lib/config/presets/index.spec.ts b/lib/config/presets/index.spec.ts index 70a750a2b2..6e757c213a 100644 --- a/lib/config/presets/index.spec.ts +++ b/lib/config/presets/index.spec.ts @@ -198,7 +198,9 @@ describe('config/presets', () => { it('ignores presets', async () => { config.extends = ['config:base']; - const res = await presets.resolveConfigPresets(config, ['config:base']); + const res = await presets.resolveConfigPresets(config, {}, [ + 'config:base', + ]); expect(config).toMatchObject(res); expect(res).toMatchSnapshot(); }); @@ -257,6 +259,12 @@ describe('config/presets', () => { it('parses gitlab', () => { expect(presets.parsePreset('gitlab>some/repo')).toMatchSnapshot(); }); + it('parses local', () => { + expect(presets.parsePreset('local>some/repo')).toMatchSnapshot(); + }); + it('parses no prefix as local', () => { + expect(presets.parsePreset('some/repo')).toMatchSnapshot(); + }); it('returns default package name with params', () => { expect( presets.parsePreset(':group(packages/eslint, eslint)') @@ -323,27 +331,30 @@ describe('config/presets', () => { }); describe('getPreset', () => { it('gets linters', async () => { - const res = await presets.getPreset('packages:linters'); + const res = await presets.getPreset('packages:linters', {}); expect(res).toMatchSnapshot(); expect(res.packageNames).toHaveLength(1); expect(res.extends).toHaveLength(2); }); it('gets parameterised configs', async () => { - const res = await presets.getPreset(':group(packages:eslint, eslint)'); + const res = await presets.getPreset( + ':group(packages:eslint, eslint)', + {} + ); expect(res).toMatchSnapshot(); }); it('handles missing params', async () => { - const res = await presets.getPreset(':group()'); + const res = await presets.getPreset(':group()', {}); expect(res).toMatchSnapshot(); }); it('ignores irrelevant params', async () => { - const res = await presets.getPreset(':pinVersions(foo, bar)'); + const res = await presets.getPreset(':pinVersions(foo, bar)', {}); expect(res).toMatchSnapshot(); }); it('handles 404 packages', async () => { let e: Error; try { - await presets.getPreset('notfound:foo'); + await presets.getPreset('notfound:foo', {}); } catch (err) { e = err; } @@ -355,7 +366,7 @@ describe('config/presets', () => { it('handles no config', async () => { let e: Error; try { - await presets.getPreset('noconfig:foo'); + await presets.getPreset('noconfig:foo', {}); } catch (err) { e = err; } @@ -367,7 +378,7 @@ describe('config/presets', () => { it('handles throw errors', async () => { let e: Error; try { - await presets.getPreset('throw:foo'); + await presets.getPreset('throw:foo', {}); } catch (err) { e = err; } @@ -379,7 +390,7 @@ describe('config/presets', () => { it('handles preset not found', async () => { let e: Error; try { - await presets.getPreset('wrongpreset:foo'); + await presets.getPreset('wrongpreset:foo', {}); } catch (err) { e = err; } diff --git a/lib/config/presets/index.ts b/lib/config/presets/index.ts index ee2bdc2d51..a84f527ac8 100644 --- a/lib/config/presets/index.ts +++ b/lib/config/presets/index.ts @@ -5,6 +5,7 @@ import * as migration from '../migration'; import * as github from './github'; import * as npm from './npm'; import * as gitlab from './gitlab'; +import * as local from './local'; import { RenovateConfig } from '../common'; import { mergeChildConfig } from '../utils'; import { regEx } from '../../util/regex'; @@ -18,6 +19,7 @@ const presetSources = { github, npm, gitlab, + local, }; export function replaceArgs( @@ -61,6 +63,15 @@ export function parsePreset(input: string): ParsedPreset { } else if (str.startsWith('gitlab>')) { presetSource = 'gitlab'; str = str.substring('gitlab>'.length); + } else if (str.startsWith('local>')) { + presetSource = 'local'; + str = str.substring('local>'.length); + } else if ( + !str.startsWith('@') && + !str.startsWith(':') && + str.includes('/') + ) { + presetSource = 'local'; } str = str.replace(/^npm>/, ''); presetSource = presetSource || 'npm'; @@ -101,12 +112,16 @@ export function parsePreset(input: string): ParsedPreset { return { presetSource, packageName, presetName, params }; } -export async function getPreset(preset: string): Promise<RenovateConfig> { +export async function getPreset( + preset: string, + baseConfig?: RenovateConfig +): Promise<RenovateConfig> { logger.trace(`getPreset(${preset})`); const { presetSource, packageName, presetName, params } = parsePreset(preset); let presetConfig = await presetSources[presetSource].getPreset( packageName, - presetName + presetName, + baseConfig ); logger.trace({ presetConfig }, `Found preset ${preset}`); if (params) { @@ -142,6 +157,7 @@ export async function getPreset(preset: string): Promise<RenovateConfig> { export async function resolveConfigPresets( inputConfig: RenovateConfig, + baseConfig?: RenovateConfig, ignorePresets?: string[], existingPresets: string[] = [] ): Promise<RenovateConfig> { @@ -166,7 +182,7 @@ export async function resolveConfigPresets( logger.trace(`Resolving preset "${preset}"`); let fetchedPreset: RenovateConfig; try { - fetchedPreset = await getPreset(preset); + fetchedPreset = await getPreset(preset, baseConfig); } catch (err) { logger.debug({ err }, 'Preset fetch error'); // istanbul ignore if @@ -194,6 +210,7 @@ export async function resolveConfigPresets( } const presetConfig = await resolveConfigPresets( fetchedPreset, + baseConfig, ignorePresets, existingPresets.concat([preset]) ); @@ -225,6 +242,7 @@ export async function resolveConfigPresets( (config[key] as RenovateConfig[]).push( await resolveConfigPresets( element as RenovateConfig, + baseConfig, ignorePresets, existingPresets ) @@ -238,6 +256,7 @@ export async function resolveConfigPresets( logger.trace(`Resolving object "${key}"`); config[key] = await resolveConfigPresets( val as RenovateConfig, + baseConfig, ignorePresets, existingPresets ); diff --git a/lib/config/presets/local.spec.ts b/lib/config/presets/local.spec.ts new file mode 100644 index 0000000000..0c7a22f59f --- /dev/null +++ b/lib/config/presets/local.spec.ts @@ -0,0 +1,60 @@ +import * as gitlab from './gitlab'; +import * as github from './github'; +import * as local from './local'; + +jest.mock('./gitlab'); +jest.mock('./github'); + +const gitlabGetPreset: jest.Mock<Promise<any>> = gitlab.getPreset as never; +const githubGetPreset: jest.Mock<Promise<any>> = github.getPreset as never; + +describe('config/presets/local', () => { + beforeEach(() => { + gitlabGetPreset.mockReset(); + gitlabGetPreset.mockResolvedValueOnce({ resolved: 'preset' }); + githubGetPreset.mockReset(); + githubGetPreset.mockResolvedValueOnce({ resolved: 'preset' }); + global.repoCache = {}; + return global.renovateCache.rmAll(); + }); + describe('getPreset()', () => { + it('throws for unsupported platform', async () => { + await expect( + local.getPreset('some/repo', 'default', { + platform: 'unsupported-platform', + }) + ).rejects.toThrow(); + }); + it('forwards to gitlab', async () => { + const content = await local.getPreset('some/repo', '', { + platform: 'GitLab', + }); + expect(gitlabGetPreset.mock.calls).toMatchSnapshot(); + expect(content).toMatchSnapshot(); + }); + it('forwards to custom gitlab', async () => { + const content = await local.getPreset('some/repo', '', { + platform: 'gitlab', + endpoint: 'https://gitlab.example.com/api/v4', + }); + expect(gitlabGetPreset.mock.calls).toMatchSnapshot(); + expect(content).toMatchSnapshot(); + }); + + it('forwards to github', async () => { + const content = await local.getPreset('some/repo', '', { + platform: 'github', + }); + expect(githubGetPreset.mock.calls).toMatchSnapshot(); + expect(content).toMatchSnapshot(); + }); + it('forwards to custom github', async () => { + const content = await local.getPreset('some/repo', '', { + platform: 'GitHub', + endpoint: 'https://api.github.example.com', + }); + expect(githubGetPreset.mock.calls).toMatchSnapshot(); + expect(content).toMatchSnapshot(); + }); + }); +}); diff --git a/lib/config/presets/local.ts b/lib/config/presets/local.ts new file mode 100644 index 0000000000..5b466f52f2 --- /dev/null +++ b/lib/config/presets/local.ts @@ -0,0 +1,20 @@ +import { Preset } from './common'; +import * as gitlab from './gitlab'; +import * as github from './github'; +import { RenovateConfig } from '../common'; + +export async function getPreset( + pkgName: string, + presetName = 'default', + baseConfig: RenovateConfig +): Promise<Preset> { + if (baseConfig.platform?.toLowerCase() === 'gitlab') { + return gitlab.getPreset(pkgName, presetName, baseConfig); + } + if (baseConfig.platform?.toLowerCase() === 'github') { + return github.getPreset(pkgName, presetName, baseConfig); + } + throw new Error( + `Unsupported platform '${baseConfig.platform}' for local preset.` + ); +} diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 203a860981..6df821c381 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -174,7 +174,8 @@ export async function validateConfig( let hasSelector = false; if (is.object(packageRule)) { const resolvedRule = await resolveConfigPresets( - packageRule as RenovateConfig + packageRule as RenovateConfig, + config ); errors.push( ...managerValidator.check({ resolvedRule, currentPath }) diff --git a/lib/platform/azure/index.ts b/lib/platform/azure/index.ts index 4c423a67bb..4b7c5abe71 100644 --- a/lib/platform/azure/index.ts +++ b/lib/platform/azure/index.ts @@ -33,6 +33,7 @@ import { } from '../../constants/pull-requests'; import { BranchStatus } from '../../types'; import { RenovateConfig } from '../../config/common'; +import { ensureTrailingSlash } from '../../util/url'; interface Config { storage: GitStorage; @@ -77,7 +78,7 @@ export function initPlatform({ } // TODO: Add a connection check that endpoint/token combination are valid const res = { - endpoint: endpoint.replace(/\/?$/, '/'), // always add a trailing slash + endpoint: ensureTrailingSlash(endpoint), }; defaults.endpoint = res.endpoint; azureApi.setEndpoint(res.endpoint); diff --git a/lib/platform/bitbucket-server/index.ts b/lib/platform/bitbucket-server/index.ts index bac9e13a44..7a785f563b 100644 --- a/lib/platform/bitbucket-server/index.ts +++ b/lib/platform/bitbucket-server/index.ts @@ -33,6 +33,7 @@ import { import { PR_STATE_ALL, PR_STATE_OPEN } from '../../constants/pull-requests'; import { BranchStatus } from '../../types'; import { RenovateConfig } from '../../config/common'; +import { ensureTrailingSlash } from '../../util/url'; /* * Version: 5.3 (EOL Date: 15 Aug 2019) * See following docs for api information: @@ -88,7 +89,7 @@ export function initPlatform({ ); } // TODO: Add a connection check that endpoint/username/password combination are valid - defaults.endpoint = endpoint.replace(/\/?$/, '/'); // always add a trailing slash + defaults.endpoint = ensureTrailingSlash(endpoint); api.setBaseUrl(defaults.endpoint); const platformConfig: PlatformConfig = { endpoint: defaults.endpoint, diff --git a/lib/platform/gitea/index.ts b/lib/platform/gitea/index.ts index a0d401a949..c2c2351963 100644 --- a/lib/platform/gitea/index.ts +++ b/lib/platform/gitea/index.ts @@ -35,6 +35,7 @@ import { sanitize } from '../../util/sanitize'; import { BranchStatus } from '../../types'; import * as helper from './gitea-helper'; import { PR_STATE_ALL, PR_STATE_OPEN } from '../../constants/pull-requests'; +import { ensureTrailingSlash } from '../../util/url'; type GiteaRenovateConfig = { endpoint: string; @@ -194,8 +195,7 @@ const platform: Platform = { } if (endpoint) { - // Ensure endpoint contains trailing slash - defaults.endpoint = endpoint.replace(/\/?$/, '/'); + defaults.endpoint = ensureTrailingSlash(endpoint); } else { logger.debug('Using default Gitea endpoint: ' + defaults.endpoint); } diff --git a/lib/platform/github/index.ts b/lib/platform/github/index.ts index ac5d3985e4..4f17d075ba 100644 --- a/lib/platform/github/index.ts +++ b/lib/platform/github/index.ts @@ -46,6 +46,7 @@ import { PR_STATE_CLOSED, PR_STATE_OPEN, } from '../../constants/pull-requests'; +import { ensureTrailingSlash } from '../../util/url'; const defaultConfigFile = configFileNames[0]; @@ -122,7 +123,7 @@ export async function initPlatform({ } if (endpoint) { - defaults.endpoint = endpoint.replace(/\/?$/, '/'); // always add a trailing slash + defaults.endpoint = ensureTrailingSlash(endpoint); api.setBaseUrl(defaults.endpoint); } else { logger.debug('Using default github endpoint: ' + defaults.endpoint); diff --git a/lib/platform/gitlab/index.ts b/lib/platform/gitlab/index.ts index 9dc3e88476..81ca938ae3 100644 --- a/lib/platform/gitlab/index.ts +++ b/lib/platform/gitlab/index.ts @@ -37,6 +37,7 @@ import { import { PR_STATE_ALL, PR_STATE_OPEN } from '../../constants/pull-requests'; import { PLATFORM_TYPE_GITLAB } from '../../constants/platforms'; import { BranchStatus } from '../../types'; +import { ensureTrailingSlash } from '../../util/url'; type MergeMethod = 'merge' | 'rebase_merge' | 'ff'; const defaultConfigFile = configFileNames[0]; @@ -72,7 +73,7 @@ export async function initPlatform({ throw new Error('Init: You must configure a GitLab personal access token'); } if (endpoint) { - defaults.endpoint = endpoint.replace(/\/?$/, '/'); // always add a trailing slash + defaults.endpoint = ensureTrailingSlash(endpoint); api.setBaseUrl(defaults.endpoint); } else { logger.debug('Using default GitLab endpoint: ' + defaults.endpoint); diff --git a/lib/util/url.ts b/lib/util/url.ts new file mode 100644 index 0000000000..347fbcd9ad --- /dev/null +++ b/lib/util/url.ts @@ -0,0 +1,3 @@ +export function ensureTrailingSlash(url: string): string { + return url.replace(/\/?$/, '/'); +} diff --git a/lib/workers/repository/init/config.ts b/lib/workers/repository/init/config.ts index 66cfcfb844..80706511ac 100644 --- a/lib/workers/repository/init/config.ts +++ b/lib/workers/repository/init/config.ts @@ -146,7 +146,7 @@ export async function mergeRenovateConfig( } // Decrypt after resolving in case the preset contains npm authentication instead const resolvedConfig = decryptConfig( - await presets.resolveConfigPresets(decryptedConfig), + await presets.resolveConfigPresets(decryptedConfig, config), config.privateKey ); delete resolvedConfig.privateKey; -- GitLab