diff --git a/lib/config/migration.spec.ts b/lib/config/migration.spec.ts index 960ca990c15b49..fcb40a6609d2d6 100644 --- a/lib/config/migration.spec.ts +++ b/lib/config/migration.spec.ts @@ -20,7 +20,6 @@ describe('config/migration', () => { { platform: 'docker', endpoint: 'https://docker.io', - host: 'docker.io', username: 'some-username', password: 'some-password', }, diff --git a/lib/config/migrations/custom/host-rules-migration.spec.ts b/lib/config/migrations/custom/host-rules-migration.spec.ts index 30aed0c47dfdef..8a14eb0d743a71 100644 --- a/lib/config/migrations/custom/host-rules-migration.spec.ts +++ b/lib/config/migrations/custom/host-rules-migration.spec.ts @@ -1,3 +1,4 @@ +import { CONFIG_VALIDATION } from '../../../constants/error-messages'; import { HostRulesMigration } from './host-rules-migration'; describe('config/migrations/custom/host-rules-migration', () => { @@ -10,6 +11,12 @@ describe('config/migrations/custom/host-rules-migration', () => { baseUrl: 'https://some.domain.com', token: '123test', }, + { + hostType: 'dotnet', + baseUrl: 'https://some.domain.com', + matchHost: 'https://some.domain.com', + token: '123test', + }, { hostType: 'adoptium-java', domainName: 'domain.com', @@ -18,10 +25,17 @@ describe('config/migrations/custom/host-rules-migration', () => { { domainName: 'domain.com/', token: '123test' }, { hostType: 'docker', matchHost: 'domain.com/', token: '123test' }, { hostName: 'some.domain.com', token: '123test' }, + { endpoint: 'domain.com/', token: '123test' }, + { host: 'some.domain.com', token: '123test' }, ], } as any, { hostRules: [ + { + hostType: 'dotnet-version', + matchHost: 'https://some.domain.com', + token: '123test', + }, { hostType: 'dotnet-version', matchHost: 'https://some.domain.com', @@ -42,8 +56,33 @@ describe('config/migrations/custom/host-rules-migration', () => { token: '123test', }, { matchHost: 'some.domain.com', token: '123test' }, + { matchHost: 'https://domain.com/', token: '123test' }, + { matchHost: 'some.domain.com', token: '123test' }, ], } ); }); + + it('throws when multiple hosts are present', () => { + expect(() => + new HostRulesMigration( + { + hostRules: [ + { + matchHost: 'https://some-diff.domain.com', + baseUrl: 'https://some.domain.com', + token: '123test', + }, + ], + } as any, + {} + ).run([ + { + matchHost: 'https://some-diff.domain.com', + baseUrl: 'https://some.domain.com', + token: '123test', + }, + ]) + ).toThrow(CONFIG_VALIDATION); + }); }); diff --git a/lib/config/migrations/custom/host-rules-migration.ts b/lib/config/migrations/custom/host-rules-migration.ts index d2c368f7aa0e94..b773828813cb92 100644 --- a/lib/config/migrations/custom/host-rules-migration.ts +++ b/lib/config/migrations/custom/host-rules-migration.ts @@ -1,14 +1,18 @@ import is from '@sindresorhus/is'; +import { CONFIG_VALIDATION } from '../../../constants/error-messages'; +import { logger } from '../../../logger'; import type { HostRule } from '../../../types'; +import type { LegacyHostRule } from '../../../util/host-rules'; import { AbstractMigration } from '../base/abstract-migration'; import { migrateDatasource } from './datasource-migration'; export class HostRulesMigration extends AbstractMigration { override readonly propertyName = 'hostRules'; - override run(value: Record[]): void { + override run(value: (LegacyHostRule & HostRule)[]): void { const newHostRules: HostRule[] = []; for (const hostRule of value) { + validateHostRule(hostRule); const newRule: any = {}; for (const [key, value] of Object.entries(hostRule)) { @@ -56,6 +60,36 @@ export class HostRulesMigration extends AbstractMigration { } } +function validateHostRule(rule: LegacyHostRule & HostRule): void { + const { matchHost, hostName, domainName, baseUrl, endpoint, host } = rule; + const hosts: Record = removeUndefinedFields({ + matchHost, + hostName, + domainName, + baseUrl, + endpoint, + host, + }); + + if (Object.keys(hosts).length > 1) { + const distinctHostValues = new Set(Object.values(hosts)); + // check if the host values are duplicated + if (distinctHostValues.size > 1) { + const error = new Error(CONFIG_VALIDATION); + error.validationSource = 'config'; + error.validationMessage = `hostRules cannot contain more than one host-matching field - use "matchHost" only.`; + error.validationError = + 'The renovate configuration file contains some invalid settings'; + throw error; + } else { + logger.warn( + { hosts }, + 'Duplicate host values found, please only use `matchHost` to specify the host' + ); + } + } +} + function massageUrl(url: string): string { if (!url.includes('://') && url.includes('/')) { return 'https://' + url; @@ -63,3 +97,15 @@ function massageUrl(url: string): string { return url; } } + +function removeUndefinedFields( + obj: Record +): Record { + const result: Record = {}; + for (const key of Object.keys(obj)) { + if (is.string(obj[key])) { + result[key] = obj[key]; + } + } + return result; +} diff --git a/lib/util/host-rules.ts b/lib/util/host-rules.ts index 9ee45b797cb6af..1ff39a50df6d41 100644 --- a/lib/util/host-rules.ts +++ b/lib/util/host-rules.ts @@ -8,13 +8,15 @@ import { parseUrl, validateUrl } from './url'; let hostRules: HostRule[] = []; -interface LegacyHostRule { +export interface LegacyHostRule { hostName?: string; domainName?: string; baseUrl?: string; + host?: string; + endpoint?: string; } -function migrateRule(rule: LegacyHostRule & HostRule): HostRule { +export function migrateRule(rule: LegacyHostRule & HostRule): HostRule { const cloned: LegacyHostRule & HostRule = structuredClone(rule); delete cloned.hostName; delete cloned.domainName;