diff --git a/lib/config/migration.ts b/lib/config/migration.ts index 4219e35676bc47..c2c4015e065799 100644 --- a/lib/config/migration.ts +++ b/lib/config/migration.ts @@ -33,7 +33,7 @@ export function migrateConfig( optionTypes[option.name] = option.type; }); } - const newConfig = MigrationsService.run(config); + const newConfig = MigrationsService.run(config).migratedConfig; const migratedConfig = clone(newConfig) as MigratedRenovateConfig; const depTypes = [ 'dependencies', diff --git a/lib/config/migrations/base/abstract-migration.ts b/lib/config/migrations/base/abstract-migration.ts index 56d6fb7548bf22..9d47be887bd54e 100644 --- a/lib/config/migrations/base/abstract-migration.ts +++ b/lib/config/migrations/base/abstract-migration.ts @@ -1,24 +1,49 @@ +import is from '@sindresorhus/is'; import type { RenovateConfig } from '../../types'; import type { Migration } from '../types'; export abstract class AbstractMigration implements Migration { - readonly propertyName: string; + abstract readonly propertyName: string; - protected readonly originalConfig: RenovateConfig; + private readonly originalConfig: RenovateConfig; - protected readonly migratedConfig: RenovateConfig; + private readonly migratedConfig: RenovateConfig; - constructor( - propertyName: string, - originalConfig: RenovateConfig, - migratedConfig: RenovateConfig - ) { - this.propertyName = propertyName; + constructor(originalConfig: RenovateConfig, migratedConfig: RenovateConfig) { this.originalConfig = originalConfig; this.migratedConfig = migratedConfig; } - abstract run(): void; + abstract run(value: unknown): void; + + protected get( + key: Key + ): RenovateConfig[Key] { + return this.migratedConfig[key] ?? this.originalConfig[key]; + } + + protected setSafely( + key: Key, + value: RenovateConfig[Key] + ): void { + if ( + is.nullOrUndefined(this.originalConfig[key]) && + is.nullOrUndefined(this.migratedConfig[key]) + ) { + this.migratedConfig[key] = value; + } + } + + protected setHard( + key: Key, + value: RenovateConfig[Key] + ): void { + this.migratedConfig[key] = value; + } + + protected rewrite(value: unknown): void { + this.setHard(this.propertyName, value); + } protected delete(property: string): void { delete this.migratedConfig[property]; diff --git a/lib/config/migrations/base/remove-property-migration.ts b/lib/config/migrations/base/remove-property-migration.ts index 0b2ad2496963b2..21c8b61e0baf76 100644 --- a/lib/config/migrations/base/remove-property-migration.ts +++ b/lib/config/migrations/base/remove-property-migration.ts @@ -1,6 +1,18 @@ +import type { RenovateConfig } from '../../types'; import { AbstractMigration } from './abstract-migration'; export class RemovePropertyMigration extends AbstractMigration { + readonly propertyName: string; + + constructor( + propertyName: string, + originalConfig: RenovateConfig, + migratedConfig: RenovateConfig + ) { + super(originalConfig, migratedConfig); + this.propertyName = propertyName; + } + override run(): void { this.delete(this.propertyName); } diff --git a/lib/config/migrations/base/rename-property-migration.ts b/lib/config/migrations/base/rename-property-migration.ts index c317634be40ea4..d883dd5069ee3e 100644 --- a/lib/config/migrations/base/rename-property-migration.ts +++ b/lib/config/migrations/base/rename-property-migration.ts @@ -2,7 +2,9 @@ import type { RenovateConfig } from '../../types'; import { AbstractMigration } from './abstract-migration'; export class RenamePropertyMigration extends AbstractMigration { - protected readonly newPropertyName: string; + readonly propertyName: string; + + private readonly newPropertyName: string; constructor( deprecatedPropertyName: string, @@ -10,14 +12,14 @@ export class RenamePropertyMigration extends AbstractMigration { originalConfig: RenovateConfig, migratedConfig: RenovateConfig ) { - super(deprecatedPropertyName, originalConfig, migratedConfig); + super(originalConfig, migratedConfig); + this.propertyName = deprecatedPropertyName; this.newPropertyName = newPropertyName; } - override run(): void { + override run(value): void { this.delete(this.propertyName); - this.migratedConfig[this.newPropertyName] = - this.originalConfig[this.propertyName]; + this.setSafely(this.newPropertyName, value); } } diff --git a/lib/config/migrations/custom/binary-source-migration.spec.ts b/lib/config/migrations/custom/binary-source-migration.spec.ts index 22ef956ba40e2b..5471ca20d0ae62 100644 --- a/lib/config/migrations/custom/binary-source-migration.spec.ts +++ b/lib/config/migrations/custom/binary-source-migration.spec.ts @@ -2,10 +2,13 @@ import { MigrationsService } from '../migrations-service'; describe('config/migrations/custom/binary-source-migration', () => { it('should migrate "auto" to "global"', () => { - const migratedConfig = MigrationsService.run({ + const { isMigrated, migratedConfig } = MigrationsService.run({ binarySource: 'auto', }); - expect(migratedConfig.binarySource).toBe('global'); + expect(isMigrated).toBeTrue(); + expect(migratedConfig).toEqual({ + binarySource: 'global', + }); }); }); diff --git a/lib/config/migrations/custom/binary-source-migration.ts b/lib/config/migrations/custom/binary-source-migration.ts index 1452897133193b..81337609e6b55a 100644 --- a/lib/config/migrations/custom/binary-source-migration.ts +++ b/lib/config/migrations/custom/binary-source-migration.ts @@ -1,14 +1,11 @@ -import type { RenovateConfig } from '../../types'; import { AbstractMigration } from '../base/abstract-migration'; export class BinarySourceMigration extends AbstractMigration { - constructor(originalConfig: RenovateConfig, migratedConfig: RenovateConfig) { - super('binarySource', originalConfig, migratedConfig); - } + readonly propertyName = 'binarySource'; - override run(): void { - if (this.originalConfig.binarySource === 'auto') { - this.migratedConfig.binarySource = 'global'; + override run(value): void { + if (value === 'auto') { + this.rewrite('global'); } } } diff --git a/lib/config/migrations/custom/go-mod-tidy-migration.spec.ts b/lib/config/migrations/custom/go-mod-tidy-migration.spec.ts index f0c7be274fe469..4f342710dcf25c 100644 --- a/lib/config/migrations/custom/go-mod-tidy-migration.spec.ts +++ b/lib/config/migrations/custom/go-mod-tidy-migration.spec.ts @@ -2,30 +2,34 @@ import { MigrationsService } from '../migrations-service'; describe('config/migrations/custom/go-mod-tidy-migration', () => { it('should add postUpdateOptions option when true', () => { - const migratedConfig = MigrationsService.run({ + const { isMigrated, migratedConfig } = MigrationsService.run({ gomodTidy: true, postUpdateOptions: ['test'], }); - expect(migratedConfig).not.toHaveProperty('gomodTidy'); - expect(migratedConfig.postUpdateOptions).toEqual(['test', 'gomodTidy']); + expect(isMigrated).toBeTrue(); + expect(migratedConfig).toEqual({ + postUpdateOptions: ['test', 'gomodTidy'], + }); }); it('should handle case when postUpdateOptions is not defined ', () => { - const migratedConfig = MigrationsService.run({ + const { isMigrated, migratedConfig } = MigrationsService.run({ gomodTidy: true, }); - expect(migratedConfig).not.toHaveProperty('gomodTidy'); - expect(migratedConfig.postUpdateOptions).toEqual(['gomodTidy']); + expect(isMigrated).toBeTrue(); + expect(migratedConfig).toEqual({ + postUpdateOptions: ['gomodTidy'], + }); }); it('should only remove when false', () => { - const migratedConfig = MigrationsService.run({ + const { isMigrated, migratedConfig } = MigrationsService.run({ gomodTidy: false, }); - expect(migratedConfig).not.toHaveProperty('gomodTidy'); - expect(migratedConfig).not.toHaveProperty('postUpdateOptions'); + expect(isMigrated).toBeTrue(); + expect(migratedConfig).toEqual({}); }); }); diff --git a/lib/config/migrations/custom/go-mod-tidy-migration.ts b/lib/config/migrations/custom/go-mod-tidy-migration.ts index b655489da2116a..9f3c984e363c86 100644 --- a/lib/config/migrations/custom/go-mod-tidy-migration.ts +++ b/lib/config/migrations/custom/go-mod-tidy-migration.ts @@ -1,19 +1,18 @@ -import type { RenovateConfig } from '../../types'; import { AbstractMigration } from '../base/abstract-migration'; export class GoModTidyMigration extends AbstractMigration { - constructor(originalConfig: RenovateConfig, migratedConfig: RenovateConfig) { - super('gomodTidy', originalConfig, migratedConfig); - } + readonly propertyName = 'gomodTidy'; - override run(): void { - const { gomodTidy, postUpdateOptions } = this.originalConfig; + override run(value): void { + const postUpdateOptions = this.get('postUpdateOptions'); this.delete(this.propertyName); - if (gomodTidy) { - this.migratedConfig.postUpdateOptions ??= postUpdateOptions ?? []; - this.migratedConfig.postUpdateOptions.push('gomodTidy'); + if (value) { + const newPostUpdateOptions = Array.isArray(postUpdateOptions) + ? postUpdateOptions.concat(['gomodTidy']) + : ['gomodTidy']; + this.setHard('postUpdateOptions', newPostUpdateOptions); } } } diff --git a/lib/config/migrations/custom/ignore-node-modules-migration.spec.ts b/lib/config/migrations/custom/ignore-node-modules-migration.spec.ts index 4f2d4d000a8129..2be6ae247a70b6 100644 --- a/lib/config/migrations/custom/ignore-node-modules-migration.spec.ts +++ b/lib/config/migrations/custom/ignore-node-modules-migration.spec.ts @@ -2,10 +2,11 @@ import { MigrationsService } from '../migrations-service'; describe('config/migrations/custom/ignore-node-modules-migration', () => { it('should migrate to ignorePaths', () => { - const migratedConfig = MigrationsService.run({ + const { isMigrated, migratedConfig } = MigrationsService.run({ ignoreNodeModules: true, }); - expect(migratedConfig.ignorePaths).toEqual(['node_modules/']); + expect(isMigrated).toBeTrue(); + expect(migratedConfig).toEqual({ ignorePaths: ['node_modules/'] }); }); }); diff --git a/lib/config/migrations/custom/ignore-node-modules-migration.ts b/lib/config/migrations/custom/ignore-node-modules-migration.ts index 976b53df3cd0e0..79a1c77e750940 100644 --- a/lib/config/migrations/custom/ignore-node-modules-migration.ts +++ b/lib/config/migrations/custom/ignore-node-modules-migration.ts @@ -1,16 +1,11 @@ -import type { RenovateConfig } from '../../types'; import { AbstractMigration } from '../base/abstract-migration'; export class IgnoreNodeModulesMigration extends AbstractMigration { - constructor(originalConfig: RenovateConfig, migratedConfig: RenovateConfig) { - super('ignoreNodeModules', originalConfig, migratedConfig); - } + readonly propertyName = 'ignoreNodeModules'; - override run(): void { + override run(value): void { this.delete(this.propertyName); - this.migratedConfig.ignorePaths = this.originalConfig.ignoreNodeModules - ? ['node_modules/'] - : []; + this.setSafely('ignorePaths', value ? ['node_modules/'] : []); } } diff --git a/lib/config/migrations/custom/required-status-checks-migration.spec.ts b/lib/config/migrations/custom/required-status-checks-migration.spec.ts index 2fdd67820869e0..5aba3b883ceffd 100644 --- a/lib/config/migrations/custom/required-status-checks-migration.spec.ts +++ b/lib/config/migrations/custom/required-status-checks-migration.spec.ts @@ -2,11 +2,13 @@ import { MigrationsService } from '../migrations-service'; describe('config/migrations/custom/required-status-checks-migration', () => { it('should migrate requiredStatusChecks=null to ignoreTests=true', () => { - const migratedConfig = MigrationsService.run({ + const { isMigrated, migratedConfig } = MigrationsService.run({ requiredStatusChecks: null, }); - expect(migratedConfig).not.toHaveProperty('requiredStatusChecks'); - expect(migratedConfig.ignoreTests).toBeTrue(); + expect(isMigrated).toBeTrue(); + expect(migratedConfig).toEqual({ + ignoreTests: true, + }); }); }); diff --git a/lib/config/migrations/custom/required-status-checks-migration.ts b/lib/config/migrations/custom/required-status-checks-migration.ts index cd06869c20e18c..a6fb5c303432e3 100644 --- a/lib/config/migrations/custom/required-status-checks-migration.ts +++ b/lib/config/migrations/custom/required-status-checks-migration.ts @@ -1,16 +1,13 @@ -import type { RenovateConfig } from '../../types'; import { AbstractMigration } from '../base/abstract-migration'; export class RequiredStatusChecksMigration extends AbstractMigration { - constructor(originalConfig: RenovateConfig, migratedConfig: RenovateConfig) { - super('requiredStatusChecks', originalConfig, migratedConfig); - } + readonly propertyName = 'requiredStatusChecks'; - override run(): void { + override run(value): void { this.delete(this.propertyName); - if (this.originalConfig.requiredStatusChecks === null) { - this.migratedConfig.ignoreTests = true; + if (value === null) { + this.setSafely('ignoreTests', true); } } } diff --git a/lib/config/migrations/custom/trust-level-migration.spec.ts b/lib/config/migrations/custom/trust-level-migration.spec.ts index 5fac9753ea1fba..51ac5e070e7a0c 100644 --- a/lib/config/migrations/custom/trust-level-migration.spec.ts +++ b/lib/config/migrations/custom/trust-level-migration.spec.ts @@ -2,25 +2,31 @@ import { MigrationsService } from '../migrations-service'; describe('config/migrations/custom/trust-level-migration', () => { it('should handle hight level', () => { - const migratedConfig = MigrationsService.run({ + const { isMigrated, migratedConfig } = MigrationsService.run({ trustLevel: 'high', }); - expect(migratedConfig.allowCustomCrateRegistries).toBeTrue(); - expect(migratedConfig.allowScripts).toBeTrue(); - expect(migratedConfig.exposeAllEnv).toBeTrue(); + expect(isMigrated).toBeTrue(); + expect(migratedConfig).toEqual({ + allowCustomCrateRegistries: true, + allowScripts: true, + exposeAllEnv: true, + }); }); it('should not rewrite provided properties', () => { - const migratedConfig = MigrationsService.run({ + const { isMigrated, migratedConfig } = MigrationsService.run({ allowCustomCrateRegistries: false, allowScripts: false, exposeAllEnv: false, trustLevel: 'high', }); - expect(migratedConfig.allowCustomCrateRegistries).toBeFalse(); - expect(migratedConfig.allowScripts).toBeFalse(); - expect(migratedConfig.exposeAllEnv).toBeFalse(); + expect(isMigrated).toBeTrue(); + expect(migratedConfig).toEqual({ + allowCustomCrateRegistries: false, + allowScripts: false, + exposeAllEnv: false, + }); }); }); diff --git a/lib/config/migrations/custom/trust-level-migration.ts b/lib/config/migrations/custom/trust-level-migration.ts index b9e09077f45739..95948940cf88d1 100644 --- a/lib/config/migrations/custom/trust-level-migration.ts +++ b/lib/config/migrations/custom/trust-level-migration.ts @@ -1,21 +1,15 @@ -import type { RenovateConfig } from '../../types'; import { AbstractMigration } from '../base/abstract-migration'; export class TrustLevelMigration extends AbstractMigration { - constructor(originalConfig: RenovateConfig, migratedConfig: RenovateConfig) { - super('trustLevel', originalConfig, migratedConfig); - } + readonly propertyName = 'trustLevel'; - override run(): void { + override run(value): void { this.delete(this.propertyName); - if (this.originalConfig.trustLevel === 'high') { - this.migratedConfig.allowCustomCrateRegistries = - this.originalConfig.allowCustomCrateRegistries ?? true; - this.migratedConfig.allowScripts = - this.originalConfig.allowScripts ?? true; - this.migratedConfig.exposeAllEnv = - this.originalConfig.exposeAllEnv ?? true; + if (value === 'high') { + this.setSafely('allowCustomCrateRegistries', true); + this.setSafely('allowScripts', true); + this.setSafely('exposeAllEnv', true); } } } diff --git a/lib/config/migrations/migrations-service.spec.ts b/lib/config/migrations/migrations-service.spec.ts index 222da9a9ca05ae..ae7ba803c0dbcc 100644 --- a/lib/config/migrations/migrations-service.spec.ts +++ b/lib/config/migrations/migrations-service.spec.ts @@ -8,8 +8,10 @@ describe('config/migrations/migrations-service', () => { [property]: 'test', }; - const migratedConfig = MigrationsService.run(originalConfig); - expect(migratedConfig).not.toHaveProperty(property); + const { isMigrated, migratedConfig } = + MigrationsService.run(originalConfig); + expect(isMigrated).toBeTrue(); + expect(migratedConfig).toEqual({}); } }); @@ -22,9 +24,12 @@ describe('config/migrations/migrations-service', () => { [oldPropertyName]: 'test', }; - const migratedConfig = MigrationsService.run(originalConfig); - expect(migratedConfig).not.toHaveProperty(oldPropertyName); - expect(migratedConfig[newPropertyName]).toBe('test'); + const { isMigrated, migratedConfig } = + MigrationsService.run(originalConfig); + expect(isMigrated).toBeTrue(); + expect(migratedConfig).toEqual({ + [newPropertyName]: 'test', + }); } }); @@ -34,12 +39,14 @@ describe('config/migrations/migrations-service', () => { versionScheme: 'test', excludedPackageNames: ['test'], }; - const migratedConfig = MigrationsService.run(originalConfig); + const { isMigrated, migratedConfig } = + MigrationsService.run(originalConfig); const mappedProperties = Object.keys(originalConfig).map((property) => MigrationsService.renamedProperties.get(property) ); + expect(isMigrated).toBeTrue(); expect(mappedProperties).toEqual(Object.keys(migratedConfig)); }); }); diff --git a/lib/config/migrations/migrations-service.ts b/lib/config/migrations/migrations-service.ts index d1cee89acb423a..a0d67ed42daea5 100644 --- a/lib/config/migrations/migrations-service.ts +++ b/lib/config/migrations/migrations-service.ts @@ -1,4 +1,5 @@ -import type { RenovateConfig } from '../types'; +import { dequal } from 'dequal'; +import type { MigratedConfig, RenovateConfig } from '../types'; import { RemovePropertyMigration } from './base/remove-property-migration'; import { RenamePropertyMigration } from './base/rename-property-migration'; import { BinarySourceMigration } from './custom/binary-source-migration'; @@ -6,7 +7,7 @@ import { GoModTidyMigration } from './custom/go-mod-tidy-migration'; import { IgnoreNodeModulesMigration } from './custom/ignore-node-modules-migration'; import { RequiredStatusChecksMigration } from './custom/required-status-checks-migration'; import { TrustLevelMigration } from './custom/trust-level-migration'; -import type { Migration } from './types'; +import type { Migration, MigrationConstructor } from './types'; export class MigrationsService { static readonly removedProperties: ReadonlySet = new Set([ @@ -34,7 +35,15 @@ export class MigrationsService { ['versionScheme', 'versioning'], ]); - static run(originalConfig: RenovateConfig): RenovateConfig { + static readonly customMigrations: ReadonlyArray = [ + BinarySourceMigration, + GoModTidyMigration, + IgnoreNodeModulesMigration, + RequiredStatusChecksMigration, + TrustLevelMigration, + ]; + + static run(originalConfig: RenovateConfig): MigratedConfig { const migratedConfig: RenovateConfig = {}; const migrations = MigrationsService.getMigrations( originalConfig, @@ -44,16 +53,19 @@ export class MigrationsService { for (const [key, value] of Object.entries(originalConfig)) { migratedConfig[key] ??= value; const migration = migrations.find((item) => item.propertyName === key); - migration?.run(); + migration?.run(value); } - return migratedConfig; + return { + isMigrated: !dequal(originalConfig, migratedConfig), + migratedConfig, + }; } private static getMigrations( originalConfig: RenovateConfig, migratedConfig: RenovateConfig - ): Migration[] { + ): ReadonlyArray { const migrations: Migration[] = []; for (const propertyName of MigrationsService.removedProperties) { @@ -80,15 +92,9 @@ export class MigrationsService { ); } - migrations.push(new BinarySourceMigration(originalConfig, migratedConfig)); - migrations.push( - new IgnoreNodeModulesMigration(originalConfig, migratedConfig) - ); - migrations.push( - new RequiredStatusChecksMigration(originalConfig, migratedConfig) - ); - migrations.push(new TrustLevelMigration(originalConfig, migratedConfig)); - migrations.push(new GoModTidyMigration(originalConfig, migratedConfig)); + for (const CustomMigration of this.customMigrations) { + migrations.push(new CustomMigration(originalConfig, migratedConfig)); + } return migrations; } diff --git a/lib/config/migrations/types.ts b/lib/config/migrations/types.ts index 93f2035ed60a60..206b294863df1e 100644 --- a/lib/config/migrations/types.ts +++ b/lib/config/migrations/types.ts @@ -1,4 +1,12 @@ +import type { RenovateConfig } from './../types'; +export interface MigrationConstructor { + new ( + originalConfig: RenovateConfig, + migratedConfig: RenovateConfig + ): Migration; +} + export interface Migration { readonly propertyName: string; - run(): void; + run(value: unknown): void; }