From a4ab4523f8de62ffecec7c713aa82bfb7bf18bbe Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Sun, 5 Mar 2023 17:23:41 +0100
Subject: [PATCH] feat(config)!: forkProcessing (#20759)

Removes "includeForks" option and replaces with "forkProcessing". New default behavior is to process forks if automerge=false.

Closes #20752

BREAKING CHANGE: Forked repos will now be processed automatically if autodiscover=false. includeForks is removed and replaced by new option forkProcessing.
---
 docs/development/configuration.md             |  2 +-
 docs/development/local-development.md         | 11 ------
 docs/usage/configuration-options.md           | 18 +++++-----
 .../__snapshots__/migration.spec.ts.snap      |  2 +-
 .../custom/include-forks-migration.spec.ts    | 34 +++++++++++++++++++
 .../custom/include-forks-migration.ts         | 13 +++++++
 .../custom/renovate-fork-migration.spec.ts    |  4 +--
 .../custom/renovate-fork-migration.ts         |  2 +-
 lib/config/options/index.ts                   |  9 ++---
 lib/config/types.ts                           |  2 +-
 lib/modules/platform/types.ts                 |  2 +-
 lib/workers/global/config/parse/cli.ts        |  2 ++
 lib/workers/global/config/parse/index.ts      |  6 ++++
 lib/workers/repository/configured.ts          |  2 +-
 lib/workers/repository/init/apis.spec.ts      | 33 ++++++++++++++----
 lib/workers/repository/init/apis.ts           |  9 +++--
 .../repository/onboarding/branch/index.ts     |  2 +-
 17 files changed, 112 insertions(+), 41 deletions(-)
 create mode 100644 lib/config/migrations/custom/include-forks-migration.spec.ts
 create mode 100644 lib/config/migrations/custom/include-forks-migration.ts

diff --git a/docs/development/configuration.md b/docs/development/configuration.md
index b77fdfd6e3..54b007df01 100644
--- a/docs/development/configuration.md
+++ b/docs/development/configuration.md
@@ -32,7 +32,7 @@ e.g. apply one set of labels for `backend/package.json` and a different set of l
 module.exports = {
   npmrc: '//registry.npmjs.org/:_authToken=abc123',
   baseDir: '/tmp/renovate',
-  includeForks: true,
+  forkProcessing: 'enabled',
   gradle: { enabled: false },
 };
 ```
diff --git a/docs/development/local-development.md b/docs/development/local-development.md
index ef07cd0a55..a226fba39b 100644
--- a/docs/development/local-development.md
+++ b/docs/development/local-development.md
@@ -171,17 +171,6 @@ To do this, see these GitHub guides:
 
 ## Tips and tricks
 
-### Running Renovate against forked repositories
-
-Quite often, the quickest way for you to test or fix something is to fork an existing repository.
-But by default Renovate skips over repositories that are forked.
-To override this default, you need to specify the setting `includeForks` as `true`.
-
-Tell Renovate to run on your forked repository by doing one of the following:
-
-1. Add `"includeForks": true` to the `renovate.json` file in your forked repository
-1. Run Renovate with the CLI flag `--renovate-fork=true`
-
 ### Log files
 
 Usually, `debug` is good enough to troubleshoot most problems or verify functionality.
diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index afa89177db..6912e64938 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -923,6 +923,16 @@ If this option is enabled, reviewers will need to create a new PR if additional
 !!! note
     This option is only relevant if you set `forkToken`.
 
+## forkProcessing
+
+By default, Renovate will skip over any repositories that are forked if Renovate is using `autodiscover` mode.
+This includes if the forked repository has a Renovate config file, because Renovate can't tell if that file was added by the original repository or not.
+If you wish to enable processing of a forked repository by Renovate when autodiscovering, you need to add `"forkProcessing": "enabled"` to your repository config or run the CLI command with `--fork-processing=enabled`.
+
+If you are running in non-autodiscover mode (e.g. supplying a list of repositories to Renovate) but wish to skip forked repositories, you need to configure `"forkProcessing": "disabled"` in your global config.
+
+If you are using the hosted Mend Renovate then this option will be configured to `"enabled"` automatically if you "Selected" repositories individually but `"disabled"` if you installed for "All" repositories. If you have installed Renovate into "All" repositories but have a fork you want to use, then add `"forkProcessing": "enabled"` to the repository's `renovate.json` file.
+
 ## gitAuthor
 
 You can customize the Git author that's used whenever Renovate creates a commit.
@@ -1417,14 +1427,6 @@ If you need to force permanent unstable updates for a package, you can add a pac
 
 Also check out the `followTag` configuration option above if you wish Renovate to keep you pinned to a particular release tag.
 
-## includeForks
-
-By default, Renovate will skip over any repositories that are forked.
-This includes if the forked repository has a Renovate config file, because Renovate can't tell if that file was added by the original repository or not.
-If you wish to enable processing of a forked repository by Renovate, you need to add `"includeForks": true` to your repository config or run the CLI command with `--include-forks=true`.
-
-If you are using the hosted Mend Renovate then this option will be configured to `true` automatically if you "Selected" repositories individually but remain as `false` if you installed for "All" repositories.
-
 ## includePaths
 
 If you wish for Renovate to process only select paths in the repository, use `includePaths`.
diff --git a/lib/config/__snapshots__/migration.spec.ts.snap b/lib/config/__snapshots__/migration.spec.ts.snap
index fdf32c597a..572cc3bbdb 100644
--- a/lib/config/__snapshots__/migration.spec.ts.snap
+++ b/lib/config/__snapshots__/migration.spec.ts.snap
@@ -131,6 +131,7 @@ exports[`config/migration migrateConfig(config, parentConfig) migrates config 1`
     "config:js-lib",
     ":dependencyDashboard",
   ],
+  "forkProcessing": "enabled",
   "hostRules": [
     {
       "hostType": "docker",
@@ -142,7 +143,6 @@ exports[`config/migration migrateConfig(config, parentConfig) migrates config 1`
   "ignorePaths": [
     "node_modules/",
   ],
-  "includeForks": true,
   "lockFileMaintenance": {
     "automerge": true,
     "exposeAllEnv": false,
diff --git a/lib/config/migrations/custom/include-forks-migration.spec.ts b/lib/config/migrations/custom/include-forks-migration.spec.ts
new file mode 100644
index 0000000000..ca4ce0b9b2
--- /dev/null
+++ b/lib/config/migrations/custom/include-forks-migration.spec.ts
@@ -0,0 +1,34 @@
+import { RenovateForkMigration } from './include-forks-migration';
+
+describe('config/migrations/custom/include-forks-migration', () => {
+  it('should migrate true', () => {
+    expect(RenovateForkMigration).toMigrate(
+      {
+        includeForks: true,
+      },
+      {
+        forkProcessing: 'enabled',
+      }
+    );
+  });
+
+  it('should migrate false', () => {
+    expect(RenovateForkMigration).toMigrate(
+      {
+        includeForks: false,
+      },
+      {
+        forkProcessing: 'disabled',
+      }
+    );
+  });
+
+  it('should not migrate non boolean value', () => {
+    expect(RenovateForkMigration).toMigrate(
+      {
+        includeForks: 'test',
+      },
+      {}
+    );
+  });
+});
diff --git a/lib/config/migrations/custom/include-forks-migration.ts b/lib/config/migrations/custom/include-forks-migration.ts
new file mode 100644
index 0000000000..5afede3db6
--- /dev/null
+++ b/lib/config/migrations/custom/include-forks-migration.ts
@@ -0,0 +1,13 @@
+import is from '@sindresorhus/is';
+import { AbstractMigration } from '../base/abstract-migration';
+
+export class RenovateForkMigration extends AbstractMigration {
+  override readonly deprecated = true;
+  override readonly propertyName = 'includeForks';
+
+  override run(value: unknown): void {
+    if (is.boolean(value)) {
+      this.setSafely('forkProcessing', value ? 'enabled' : 'disabled');
+    }
+  }
+}
diff --git a/lib/config/migrations/custom/renovate-fork-migration.spec.ts b/lib/config/migrations/custom/renovate-fork-migration.spec.ts
index 3e0841ebbc..daa11f1885 100644
--- a/lib/config/migrations/custom/renovate-fork-migration.spec.ts
+++ b/lib/config/migrations/custom/renovate-fork-migration.spec.ts
@@ -7,7 +7,7 @@ describe('config/migrations/custom/renovate-fork-migration', () => {
         renovateFork: true,
       },
       {
-        includeForks: true,
+        forkProcessing: 'enabled',
       }
     );
   });
@@ -18,7 +18,7 @@ describe('config/migrations/custom/renovate-fork-migration', () => {
         renovateFork: false,
       },
       {
-        includeForks: false,
+        forkProcessing: 'disabled',
       }
     );
   });
diff --git a/lib/config/migrations/custom/renovate-fork-migration.ts b/lib/config/migrations/custom/renovate-fork-migration.ts
index 4a2b0d61d9..071ffdac5f 100644
--- a/lib/config/migrations/custom/renovate-fork-migration.ts
+++ b/lib/config/migrations/custom/renovate-fork-migration.ts
@@ -7,7 +7,7 @@ export class RenovateForkMigration extends AbstractMigration {
 
   override run(value: unknown): void {
     if (is.boolean(value)) {
-      this.setSafely('includeForks', value);
+      this.setSafely('forkProcessing', value ? 'enabled' : 'disabled');
     }
   }
 }
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 0772761624..1ed247e661 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -419,12 +419,13 @@ const options: RenovateOptions[] = [
     experimentalIssues: [17633],
   },
   {
-    name: 'includeForks',
+    name: 'forkProcessing',
     description:
-      'Whether to process forked repositories. By default, all forked repositories are skipped.',
+      'Whether to process forked repositories. By default, all forked repositories are skipped when in autodiscover mode.',
     stage: 'repository',
-    type: 'boolean',
-    default: false,
+    type: 'string',
+    allowedValues: ['auto', 'enabled', 'disabled'],
+    default: 'auto',
   },
   {
     name: 'forkToken',
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 9891748ea5..0a48bb829b 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -213,7 +213,7 @@ export interface RenovateConfig
   hostRules?: HostRule[];
 
   ignorePresets?: string[];
-  includeForks?: boolean;
+  forkProcessing?: 'auto' | 'enabled' | 'disabled';
   isFork?: boolean;
 
   fileList?: string[];
diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts
index f5f8a15285..dab52e1aa8 100644
--- a/lib/modules/platform/types.ts
+++ b/lib/modules/platform/types.ts
@@ -38,7 +38,7 @@ export interface RepoParams {
   endpoint?: string;
   gitUrl?: GitUrlOption;
   forkToken?: string;
-  includeForks?: boolean;
+  forkProcessing?: 'enabled' | 'disabled';
   renovateUsername?: string;
   cloneSubmodules?: boolean;
   ignorePrAuthor?: boolean;
diff --git a/lib/workers/global/config/parse/cli.ts b/lib/workers/global/config/parse/cli.ts
index 019dc343e9..f12305fa3e 100644
--- a/lib/workers/global/config/parse/cli.ts
+++ b/lib/workers/global/config/parse/cli.ts
@@ -32,6 +32,8 @@ export function getConfig(input: string[]): AllConfig {
         .replace(/^--dry-run$/, '--dry-run=true')
         .replace(/^--require-config$/, '--require-config=true')
         .replace('--aliases', '--registry-aliases')
+        .replace('--include-forks=true', '--fork-processing=enabled')
+        .replace('--include-forks', '--fork-processing=enabled')
     )
     .filter((a) => !a.startsWith('--git-fs'));
   const options = getOptions();
diff --git a/lib/workers/global/config/parse/index.ts b/lib/workers/global/config/parse/index.ts
index 5bfed02cbf..0823d06c6f 100644
--- a/lib/workers/global/config/parse/index.ts
+++ b/lib/workers/global/config/parse/index.ts
@@ -99,6 +99,12 @@ export async function parseConfigs(
     config.endpoint = ensureTrailingSlash(config.endpoint);
   }
 
+  // Massage forkProcessing
+  if (!config.autodiscover && config.forkProcessing !== 'disabled') {
+    logger.debug('Enabling forkProcessing while in non-autodiscover mode');
+    config.forkProcessing = 'enabled';
+  }
+
   // Remove log file entries
   delete config.logFile;
   delete config.logFileLevel;
diff --git a/lib/workers/repository/configured.ts b/lib/workers/repository/configured.ts
index 80e88016f5..d6b57ee7a3 100644
--- a/lib/workers/repository/configured.ts
+++ b/lib/workers/repository/configured.ts
@@ -8,7 +8,7 @@ export function checkIfConfigured(config: RenovateConfig): void {
   if (config.enabled === false) {
     throw new Error(REPOSITORY_DISABLED_BY_CONFIG);
   }
-  if (config.isFork && !config.includeForks) {
+  if (config.isFork && config.forkProcessing !== 'enabled') {
     throw new Error(REPOSITORY_FORKED);
   }
 }
diff --git a/lib/workers/repository/init/apis.spec.ts b/lib/workers/repository/init/apis.spec.ts
index 2daa89b7d1..b87e59d401 100644
--- a/lib/workers/repository/init/apis.spec.ts
+++ b/lib/workers/repository/init/apis.spec.ts
@@ -15,7 +15,7 @@ describe('workers/repository/init/apis', () => {
       config.warnings = [];
       config.token = 'some-token';
       delete config.optimizeForDisabled;
-      delete config.includeForks;
+      delete config.forkProcessing;
     });
 
     afterEach(() => {
@@ -53,15 +53,30 @@ describe('workers/repository/init/apis', () => {
         isFork: true,
         repoFingerprint: '123',
       });
-      platform.getJsonFile.mockResolvedValueOnce({ includeForks: false });
+      platform.getJsonFile.mockResolvedValueOnce({
+        forkProcessing: 'disabled',
+      });
       await expect(
         initApis({
           ...config,
-          includeForks: false,
+          forkProcessing: 'disabled',
         })
       ).rejects.toThrow(REPOSITORY_FORKED);
     });
 
+    it('does not throw for includeForks=true', async () => {
+      platform.initRepo.mockResolvedValueOnce({
+        defaultBranch: 'master',
+        isFork: true,
+        repoFingerprint: '123',
+      });
+      platform.getJsonFile.mockResolvedValueOnce({
+        includeForks: true,
+      });
+      const workerPlatformConfig = await initApis(config);
+      expect(workerPlatformConfig).toBeTruthy();
+    });
+
     it('ignores platform.getJsonFile() failures', async () => {
       platform.initRepo.mockResolvedValueOnce({
         defaultBranch: 'master',
@@ -73,7 +88,7 @@ describe('workers/repository/init/apis', () => {
         initApis({
           ...config,
           optimizeForDisabled: true,
-          includeForks: false,
+          forkProcessing: 'disabled',
           isFork: true,
         })
       ).resolves.not.toThrow();
@@ -85,7 +100,9 @@ describe('workers/repository/init/apis', () => {
         isFork: false,
         repoFingerprint: '123',
       });
-      platform.getJsonFile.mockResolvedValueOnce({ includeForks: false });
+      platform.getJsonFile.mockResolvedValueOnce({
+        forkProcessing: 'disabled',
+      });
       const workerPlatformConfig = await initApis({
         ...config,
         optimizeForDisabled: true,
@@ -107,7 +124,9 @@ describe('workers/repository/init/apis', () => {
         isFork: false,
         repoFingerprint: '123',
       });
-      platform.getJsonFile.mockResolvedValueOnce({ includeForks: false });
+      platform.getJsonFile.mockResolvedValueOnce({
+        forkProcessing: 'disabled',
+      });
       const workerPlatformConfig = await initApis({
         ...config,
         optimizeForDisabled: true,
@@ -124,7 +143,7 @@ describe('workers/repository/init/apis', () => {
         isFork: false,
         repoFingerprint: '123',
       });
-      platform.getJsonFile.mockResolvedValueOnce({ includeForks: false });
+      platform.getJsonFile.mockResolvedValueOnce({ forkProcessing: false });
       const workerPlatformConfig = await initApis({
         ...config,
         optimizeForDisabled: true,
diff --git a/lib/workers/repository/init/apis.ts b/lib/workers/repository/init/apis.ts
index d1d425ad33..1eec1212a1 100644
--- a/lib/workers/repository/init/apis.ts
+++ b/lib/workers/repository/init/apis.ts
@@ -4,6 +4,7 @@ import {
   REPOSITORY_DISABLED_BY_CONFIG,
   REPOSITORY_FORKED,
 } from '../../../constants/error-messages';
+import { logger } from '../../../logger';
 import { RepoParams, RepoResult, platform } from '../../../modules/platform';
 
 // TODO: fix types (#7154)
@@ -37,11 +38,15 @@ async function validateOptimizeForDisabled(
 }
 
 async function validateIncludeForks(config: RenovateConfig): Promise<void> {
-  if (!config.includeForks && config.isFork) {
+  if (config.forkProcessing !== 'enabled' && config.isFork) {
     const renovateConfig = await getJsonFile(defaultConfigFile(config));
-    if (!renovateConfig?.includeForks) {
+    if (
+      renovateConfig?.includeForks !== true &&
+      renovateConfig?.forkProcessing !== 'enabled'
+    ) {
       throw new Error(REPOSITORY_FORKED);
     }
+    logger.debug('Repository config enables forks - continuing');
   }
 }
 
diff --git a/lib/workers/repository/onboarding/branch/index.ts b/lib/workers/repository/onboarding/branch/index.ts
index e2b07ce4be..2add4def73 100644
--- a/lib/workers/repository/onboarding/branch/index.ts
+++ b/lib/workers/repository/onboarding/branch/index.ts
@@ -28,7 +28,7 @@ export async function checkOnboardingBranch(
     logger.debug('Repo is onboarded');
     return { ...config, repoIsOnboarded };
   }
-  if (config.isFork && !config.includeForks) {
+  if (config.isFork && config.forkProcessing !== 'enabled') {
     throw new Error(REPOSITORY_FORKED);
   }
   logger.debug('Repo is not onboarded');
-- 
GitLab