From 66820cfe277ccf84bad1b47721c768c307e72c99 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Sat, 22 May 2021 23:47:28 +0200
Subject: [PATCH] feat: migratePresets (#10111)

---
 docs/usage/self-hosted-configuration.md        | 18 ++++++++++++++++++
 .../__snapshots__/migration.spec.ts.snap       |  8 ++++++++
 lib/config/admin.ts                            |  1 +
 lib/config/definitions.ts                      | 11 +++++++++++
 lib/config/migration.spec.ts                   | 18 ++++++++++++++++++
 lib/config/migration.ts                        |  8 +++++++-
 lib/config/types.ts                            |  1 +
 lib/config/validation.ts                       |  2 +-
 8 files changed, 65 insertions(+), 2 deletions(-)

diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index bdb2999b39..e842e566a4 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -247,6 +247,24 @@ If left as default (null), a random short ID will be selected.
 
 ## logFileLevel
 
+## migratePresets
+
+Use this if you have repositories that extend from a particular preset, which has now been renamed or removed.
+This is handy if you have a large number of repositories that all extend from a particular preset which you want to rename, without the hassle of manually updating every repository individually.
+Use an empty string to indicate that the preset should be ignored rather than replaced.
+
+Example:
+
+```js
+modules.exports = {
+  migratePresets: {
+    '@company': 'local>org/renovate-config',
+  },
+};
+```
+
+In the above example any reference to the `@company` preset will be replaced with `local>org/renovate-config`.
+
 ## onboarding
 
 Set this to `false` only if all three statements are true:
diff --git a/lib/config/__snapshots__/migration.spec.ts.snap b/lib/config/__snapshots__/migration.spec.ts.snap
index 1b0c191d80..6245413a4a 100644
--- a/lib/config/__snapshots__/migration.spec.ts.snap
+++ b/lib/config/__snapshots__/migration.spec.ts.snap
@@ -51,6 +51,14 @@ Object {
 }
 `;
 
+exports[`config/migration it migrates presets 1`] = `
+Object {
+  "extends": Array [
+    "local>org/renovate-config",
+  ],
+}
+`;
+
 exports[`config/migration migrateConfig(config, parentConfig) does not migrate multi days 1`] = `
 Object {
   "schedule": "after 5:00pm on wednesday and thursday",
diff --git a/lib/config/admin.ts b/lib/config/admin.ts
index 8596085786..73f289d209 100644
--- a/lib/config/admin.ts
+++ b/lib/config/admin.ts
@@ -14,6 +14,7 @@ const repoAdminOptions = [
   'dockerUser',
   'dryRun',
   'exposeAllEnv',
+  'migratePresets',
   'privateKey',
   'localDir',
   'cacheDir',
diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts
index 70d2a78d2c..8cff7fab64 100644
--- a/lib/config/definitions.ts
+++ b/lib/config/definitions.ts
@@ -148,6 +148,17 @@ const options: RenovateOptions[] = [
     allowString: true,
     cli: false,
   },
+  {
+    name: 'migratePresets',
+    description:
+      'Define presets here which have been removed or renamed and should be migrated automatically.',
+    type: 'object',
+    admin: true,
+    default: {},
+    additionalProperties: {
+      type: 'string',
+    },
+  },
   {
     name: 'description',
     description: 'Plain text description for a config or preset.',
diff --git a/lib/config/migration.spec.ts b/lib/config/migration.spec.ts
index 8c386d4486..3bbf422c0a 100644
--- a/lib/config/migration.spec.ts
+++ b/lib/config/migration.spec.ts
@@ -1,5 +1,6 @@
 import { getName } from '../../test/util';
 import { PLATFORM_TYPE_GITHUB } from '../constants/platforms';
+import { setAdminConfig } from './admin';
 import { getConfig } from './defaults';
 import * as configMigration from './migration';
 import type {
@@ -682,4 +683,21 @@ describe(getName(), () => {
     expect(isMigrated).toBe(true);
     expect(migratedConfig).toMatchSnapshot();
   });
+  it('it migrates presets', () => {
+    setAdminConfig({
+      migratePresets: {
+        '@org': 'local>org/renovate-config',
+        '@org2/foo': '',
+      },
+    });
+    const config: RenovateConfig = {
+      extends: ['@org', '@org2/foo'],
+    } as any;
+    const { isMigrated, migratedConfig } = configMigration.migrateConfig(
+      config,
+      defaultConfig
+    );
+    expect(isMigrated).toBe(true);
+    expect(migratedConfig).toMatchSnapshot();
+  });
 });
diff --git a/lib/config/migration.ts b/lib/config/migration.ts
index 3e866f252f..4affb3a91b 100644
--- a/lib/config/migration.ts
+++ b/lib/config/migration.ts
@@ -3,6 +3,7 @@ import is from '@sindresorhus/is';
 import { dequal } from 'dequal';
 import { logger } from '../logger';
 import { clone } from '../util/clone';
+import { getAdminConfig } from './admin';
 import { getOptions } from './definitions';
 import { removedPresets } from './presets/common';
 import type {
@@ -54,6 +55,7 @@ export function migrateConfig(
       'optionalDependencies',
       'peerDependencies',
     ];
+    const { migratePresets } = getAdminConfig();
     for (const [key, val] of Object.entries(config)) {
       if (removedOptions.includes(key)) {
         delete migratedConfig[key];
@@ -260,7 +262,11 @@ export function migrateConfig(
         for (let i = 0; i < presets.length; i += 1) {
           const preset = presets[i];
           if (is.string(preset)) {
-            const newPreset = removedPresets[preset];
+            let newPreset = removedPresets[preset];
+            if (newPreset !== undefined) {
+              presets[i] = newPreset;
+            }
+            newPreset = migratePresets?.[preset];
             if (newPreset !== undefined) {
               presets[i] = newPreset;
             }
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 5b00e51439..bd56a58505 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -96,6 +96,7 @@ export interface RepoAdminConfig {
   dockerUser?: string;
   dryRun?: boolean;
   exposeAllEnv?: boolean;
+  migratePresets?: Record<string, string>;
   privateKey?: string | Buffer;
   localDir?: string;
   cacheDir?: string;
diff --git a/lib/config/validation.ts b/lib/config/validation.ts
index 4893046903..9e876c1b9a 100644
--- a/lib/config/validation.ts
+++ b/lib/config/validation.ts
@@ -514,7 +514,7 @@ export async function validateConfig(
                   message: `Invalid \`${currentPath}.${key}.${res}\` configuration: value is not a url`,
                 });
               }
-            } else if (key === 'customEnvVariables') {
+            } else if (['customEnvVariables', 'migratePresets'].includes(key)) {
               const res = validatePlainObject(val);
               if (res !== true) {
                 errors.push({
-- 
GitLab