From 3bdd530ae55ea0132ac4e16e5bcc53e548f838bd Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Sat, 2 Mar 2024 11:42:43 +0100
Subject: [PATCH] feat: forkCreation (#27686)

---
 docs/usage/self-hosted-configuration.md   |  6 ++++++
 lib/config/options/index.ts               | 11 ++++++++++
 lib/constants/error-messages.ts           |  1 +
 lib/modules/platform/github/index.spec.ts | 25 +++++++++++++++++++++++
 lib/modules/platform/github/index.ts      |  7 ++++++-
 lib/modules/platform/github/types.ts      |  1 +
 lib/modules/platform/types.ts             |  1 +
 lib/workers/repository/error.spec.ts      |  2 ++
 lib/workers/repository/error.ts           |  5 +++++
 9 files changed, 58 insertions(+), 1 deletion(-)

diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index 57e7e33536..b38bfcd446 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -555,6 +555,12 @@ In practice, it is implemented by converting the `force` configuration into a `p
 This is set to `true` by default, meaning that any settings (such as `schedule`) take maximum priority even against custom settings existing inside individual repositories.
 It will also override any settings in `packageRules`.
 
+## forkCreation
+
+This configuration lets you disable the runtime forking of repositories when running in "fork mode".
+
+Usually you will need to keep this as the default `true`, and only set to `false` if you have some out of band process to handle the creation of forks.
+
 ## forkOrg
 
 This configuration option lets you choose an organization you want repositories forked into when "fork mode" is enabled.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 00459b4fa2..cdf1f8cb18 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -530,6 +530,17 @@ const options: RenovateOptions[] = [
     supportedPlatforms: ['gitlab'],
     globalOnly: true,
   },
+  {
+    name: 'forkCreation',
+    description:
+      'Whether to create forks as needed at runtime when running in "fork mode".',
+    stage: 'repository',
+    type: 'boolean',
+    globalOnly: true,
+    supportedPlatforms: ['github'],
+    experimental: true,
+    default: true,
+  },
   {
     name: 'forkToken',
     description: 'Set a personal access token here to enable "fork mode".',
diff --git a/lib/constants/error-messages.ts b/lib/constants/error-messages.ts
index 490e963733..aa7da4b79e 100644
--- a/lib/constants/error-messages.ts
+++ b/lib/constants/error-messages.ts
@@ -28,6 +28,7 @@ export const REPOSITORY_CLOSED_ONBOARDING = 'disabled-closed-onboarding';
 export const REPOSITORY_DISABLED_BY_CONFIG = 'disabled-by-config';
 export const REPOSITORY_NO_CONFIG = 'disabled-no-config';
 export const REPOSITORY_EMPTY = 'empty';
+export const REPOSITORY_FORK_MISSING = 'fork-missing';
 export const REPOSITORY_FORK_MODE_FORKED = 'fork-mode-forked';
 export const REPOSITORY_FORKED = 'fork';
 export const REPOSITORY_MIRRORED = 'mirror';
diff --git a/lib/modules/platform/github/index.spec.ts b/lib/modules/platform/github/index.spec.ts
index 1f7e15ef8f..d887628eb9 100644
--- a/lib/modules/platform/github/index.spec.ts
+++ b/lib/modules/platform/github/index.spec.ts
@@ -8,6 +8,7 @@ import {
   PLATFORM_UNKNOWN_ERROR,
   REPOSITORY_CANNOT_FORK,
   REPOSITORY_FORKED,
+  REPOSITORY_FORK_MISSING,
   REPOSITORY_NOT_FOUND,
   REPOSITORY_RENAMED,
 } from '../../../constants/error-messages';
@@ -546,10 +547,26 @@ describe('modules/platform/github/index', () => {
       const config = await github.initRepo({
         repository: 'some/repo',
         forkToken: 'true',
+        forkCreation: true,
       });
       expect(config).toMatchSnapshot();
     });
 
+    it('should throw if fork needed but forkCreation=false', async () => {
+      const scope = httpMock.scope(githubApiHost);
+      forkInitRepoMock(scope, 'some/repo', false);
+      scope.get('/user').reply(200, {
+        login: 'forked',
+      });
+      await expect(
+        github.initRepo({
+          repository: 'some/repo',
+          forkToken: 'true',
+          forkCreation: false,
+        }),
+      ).rejects.toThrow(REPOSITORY_FORK_MISSING);
+    });
+
     it('throws if the repo is a fork', async () => {
       const repo = 'some/repo';
       const branch = 'master';
@@ -559,6 +576,7 @@ describe('modules/platform/github/index', () => {
         github.initRepo({
           repository: 'some/repo',
           forkToken: 'true',
+          forkCreation: true,
         }),
       ).rejects.toThrow(REPOSITORY_FORKED);
     });
@@ -573,6 +591,7 @@ describe('modules/platform/github/index', () => {
         github.initRepo({
           repository: 'some/repo',
           forkToken: 'true',
+          forkCreation: true,
         }),
       ).rejects.toThrow(REPOSITORY_CANNOT_FORK);
     });
@@ -585,6 +604,7 @@ describe('modules/platform/github/index', () => {
         github.initRepo({
           repository: 'some/repo',
           forkToken: 'true',
+          forkCreation: true,
         }),
       ).rejects.toThrow(REPOSITORY_CANNOT_FORK);
     });
@@ -597,6 +617,7 @@ describe('modules/platform/github/index', () => {
         github.initRepo({
           repository: 'some/repo',
           forkToken: 'true',
+          forkCreation: true,
         }),
       ).rejects.toThrow(REPOSITORY_CANNOT_FORK);
     });
@@ -613,6 +634,7 @@ describe('modules/platform/github/index', () => {
         github.initRepo({
           repository: 'some/repo',
           forkToken: 'true',
+          forkCreation: true,
           forkOrg: 'forked',
         }),
       ).rejects.toThrow(REPOSITORY_CANNOT_FORK);
@@ -625,6 +647,7 @@ describe('modules/platform/github/index', () => {
       const config = await github.initRepo({
         repository: 'some/repo',
         forkToken: 'true',
+        forkCreation: true,
         forkOrg: 'forked',
       });
       expect(config).toMatchSnapshot();
@@ -642,6 +665,7 @@ describe('modules/platform/github/index', () => {
       const config = await github.initRepo({
         repository: 'some/repo',
         forkToken: 'true',
+        forkCreation: true,
       });
       expect(config).toMatchSnapshot();
     });
@@ -2568,6 +2592,7 @@ describe('modules/platform/github/index', () => {
         await github.initRepo({
           repository: 'some/repo',
           forkToken: 'true',
+          forkCreation: true,
         });
       });
 
diff --git a/lib/modules/platform/github/index.ts b/lib/modules/platform/github/index.ts
index a5894572cd..b5c6948fe4 100644
--- a/lib/modules/platform/github/index.ts
+++ b/lib/modules/platform/github/index.ts
@@ -16,6 +16,7 @@ import {
   REPOSITORY_DISABLED,
   REPOSITORY_EMPTY,
   REPOSITORY_FORKED,
+  REPOSITORY_FORK_MISSING,
   REPOSITORY_FORK_MODE_FORKED,
   REPOSITORY_NOT_FOUND,
   REPOSITORY_RENAMED,
@@ -434,6 +435,7 @@ export async function createFork(
 export async function initRepo({
   endpoint,
   repository,
+  forkCreation,
   forkOrg,
   forkToken,
   renovateUsername,
@@ -682,10 +684,13 @@ export async function initRepo({
         }
         throw new ExternalHostError(err);
       }
-    } else {
+    } else if (forkCreation) {
       logger.debug('Forked repo is not found - attempting to create it');
       forkedRepo = await createFork(forkToken, repository, forkOrg);
       config.repository = forkedRepo.full_name;
+    } else {
+      logger.debug('Forked repo is not found and forkCreation is disabled');
+      throw new Error(REPOSITORY_FORK_MISSING);
     }
   }
 
diff --git a/lib/modules/platform/github/types.ts b/lib/modules/platform/github/types.ts
index f3c0e06661..48c41ef007 100644
--- a/lib/modules/platform/github/types.ts
+++ b/lib/modules/platform/github/types.ts
@@ -95,6 +95,7 @@ export interface LocalRepoConfig {
   parentRepo: string | null;
   forkOrg?: string;
   forkToken?: string;
+  forkCreation?: boolean;
   prList: GhPr[] | null;
   issueList: any[] | null;
   mergeMethod: 'rebase' | 'squash' | 'merge';
diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts
index 8a27564666..eff914fb7d 100644
--- a/lib/modules/platform/types.ts
+++ b/lib/modules/platform/types.ts
@@ -41,6 +41,7 @@ export interface RepoParams {
   repository: string;
   endpoint?: string;
   gitUrl?: GitUrlOption;
+  forkCreation?: boolean;
   forkOrg?: string;
   forkToken?: string;
   forkProcessing?: 'enabled' | 'disabled';
diff --git a/lib/workers/repository/error.spec.ts b/lib/workers/repository/error.spec.ts
index 1c2a662370..021836c21c 100644
--- a/lib/workers/repository/error.spec.ts
+++ b/lib/workers/repository/error.spec.ts
@@ -18,6 +18,7 @@ import {
   REPOSITORY_DISABLED,
   REPOSITORY_EMPTY,
   REPOSITORY_FORKED,
+  REPOSITORY_FORK_MISSING,
   REPOSITORY_FORK_MODE_FORKED,
   REPOSITORY_MIRRORED,
   REPOSITORY_NOT_FOUND,
@@ -48,6 +49,7 @@ describe('workers/repository/error', () => {
       REPOSITORY_DISABLED,
       REPOSITORY_CHANGED,
       REPOSITORY_FORKED,
+      REPOSITORY_FORK_MISSING,
       REPOSITORY_FORK_MODE_FORKED,
       REPOSITORY_NO_PACKAGE_FILES,
       CONFIG_SECRETS_EXPOSED,
diff --git a/lib/workers/repository/error.ts b/lib/workers/repository/error.ts
index 6e02b09315..04a7515950 100644
--- a/lib/workers/repository/error.ts
+++ b/lib/workers/repository/error.ts
@@ -21,6 +21,7 @@ import {
   REPOSITORY_DISABLED_BY_CONFIG,
   REPOSITORY_EMPTY,
   REPOSITORY_FORKED,
+  REPOSITORY_FORK_MISSING,
   REPOSITORY_FORK_MODE_FORKED,
   REPOSITORY_MIRRORED,
   REPOSITORY_NOT_FOUND,
@@ -110,6 +111,10 @@ export default async function handleError(
     logger.info('Cannot fork repository - skipping');
     return err.message;
   }
+  if (err.message === REPOSITORY_FORK_MISSING) {
+    logger.info('Cannot find fork required for fork mode - skipping');
+    return err.message;
+  }
   if (err.message === REPOSITORY_NO_PACKAGE_FILES) {
     logger.info('Repository has no package files - skipping');
     return err.message;
-- 
GitLab