From 2caf0b304bc9cd9b9ff2dd913e08558c4071efe6 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Sat, 2 Mar 2024 08:16:07 +0100
Subject: [PATCH] fix(github): skip forked repos when in fork mode (#27684)

---
 docs/usage/self-hosted-configuration.md   |  4 ++++
 lib/constants/error-messages.ts           |  1 +
 lib/modules/platform/github/graphql.ts    |  3 +++
 lib/modules/platform/github/index.spec.ts | 26 ++++++++++++++++++-----
 lib/modules/platform/github/index.ts      | 13 ++++++++++++
 lib/modules/platform/github/types.ts      |  3 +++
 lib/util/git/index.ts                     |  2 +-
 lib/workers/repository/error.spec.ts      |  2 ++
 lib/workers/repository/error.ts           |  7 ++++++
 lib/workers/repository/result.ts          |  2 ++
 10 files changed, 57 insertions(+), 6 deletions(-)

diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index f4e388e6f0..57e7e33536 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -577,6 +577,10 @@ If this value is configured then Renovate:
 
 Renovate will then create branches on the fork and opens Pull Requests on the parent repository.
 
+<!-- prettier-ignore -->
+!!! note
+    Forked repositories will always be skipped when `forkToken` is set, even if `includeForks` is true.
+
 ## gitNoVerify
 
 Controls when Renovate passes the `--no-verify` flag to `git`.
diff --git a/lib/constants/error-messages.ts b/lib/constants/error-messages.ts
index 95daa453ee..490e963733 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_MODE_FORKED = 'fork-mode-forked';
 export const REPOSITORY_FORKED = 'fork';
 export const REPOSITORY_MIRRORED = 'mirror';
 export const REPOSITORY_NOT_FOUND = 'not-found';
diff --git a/lib/modules/platform/github/graphql.ts b/lib/modules/platform/github/graphql.ts
index 09f0addcc0..8f96b4f4fb 100644
--- a/lib/modules/platform/github/graphql.ts
+++ b/lib/modules/platform/github/graphql.ts
@@ -3,6 +3,9 @@ query($owner: String!, $name: String!) {
   repository(owner: $owner, name: $name) {
     id
     isFork
+    parent {
+      nameWithOwner
+    }
     isArchived
     nameWithOwner
     hasIssuesEnabled
diff --git a/lib/modules/platform/github/index.spec.ts b/lib/modules/platform/github/index.spec.ts
index 11b8efe42f..1f7e15ef8f 100644
--- a/lib/modules/platform/github/index.spec.ts
+++ b/lib/modules/platform/github/index.spec.ts
@@ -7,6 +7,7 @@ import {
   PLATFORM_RATE_LIMIT_EXCEEDED,
   PLATFORM_UNKNOWN_ERROR,
   REPOSITORY_CANNOT_FORK,
+  REPOSITORY_FORKED,
   REPOSITORY_NOT_FOUND,
   REPOSITORY_RENAMED,
 } from '../../../constants/error-messages';
@@ -483,6 +484,7 @@ describe('modules/platform/github/index', () => {
     forkExisted: boolean,
     forkResult = 200,
     forkDefaultBranch = 'master',
+    isFork = false,
   ): void {
     scope
       // repo info
@@ -490,7 +492,7 @@ describe('modules/platform/github/index', () => {
       .reply(200, {
         data: {
           repository: {
-            isFork: false,
+            isFork,
             isArchived: false,
             nameWithOwner: repository,
             hasIssuesEnabled: true,
@@ -505,10 +507,10 @@ describe('modules/platform/github/index', () => {
             },
           },
         },
-      })
-      // getForks
-      .get(`/repos/${repository}/forks?per_page=100`)
-      .reply(
+      });
+
+    if (!isFork) {
+      scope.get(`/repos/${repository}/forks?per_page=100`).reply(
         forkResult,
         forkExisted
           ? [
@@ -520,6 +522,7 @@ describe('modules/platform/github/index', () => {
             ]
           : [],
       );
+    }
   }
 
   describe('initRepo', () => {
@@ -547,6 +550,19 @@ describe('modules/platform/github/index', () => {
       expect(config).toMatchSnapshot();
     });
 
+    it('throws if the repo is a fork', async () => {
+      const repo = 'some/repo';
+      const branch = 'master';
+      const scope = httpMock.scope(githubApiHost);
+      forkInitRepoMock(scope, repo, false, 200, branch, true);
+      await expect(
+        github.initRepo({
+          repository: 'some/repo',
+          forkToken: 'true',
+        }),
+      ).rejects.toThrow(REPOSITORY_FORKED);
+    });
+
     it('throws when cannot fork due to username error', async () => {
       const repo = 'some/repo';
       const branch = 'master';
diff --git a/lib/modules/platform/github/index.ts b/lib/modules/platform/github/index.ts
index c34abf4abc..a5894572cd 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_MODE_FORKED,
   REPOSITORY_NOT_FOUND,
   REPOSITORY_RENAMED,
 } from '../../../constants/error-messages';
@@ -570,6 +571,9 @@ export async function initRepo({
     if (err.message.startsWith('Repository access blocked')) {
       throw new Error(REPOSITORY_BLOCKED);
     }
+    if (err.message === REPOSITORY_FORK_MODE_FORKED) {
+      throw err;
+    }
     if (err.message === REPOSITORY_FORKED) {
       throw err;
     }
@@ -588,6 +592,15 @@ export async function initRepo({
 
   if (forkToken) {
     logger.debug('Bot is in fork mode');
+    if (repo.isFork) {
+      logger.debug(
+        `Forked repos cannot be processed when running with a forkToken, so this repo will be skipped`,
+      );
+      logger.debug(
+        `Parent repo for this forked repo is ${repo.parent?.nameWithOwner}`,
+      );
+      throw new Error(REPOSITORY_FORKED);
+    }
     config.forkOrg = forkOrg;
     config.forkToken = forkToken;
     // save parent name then delete
diff --git a/lib/modules/platform/github/types.ts b/lib/modules/platform/github/types.ts
index cd6feee577..f3c0e06661 100644
--- a/lib/modules/platform/github/types.ts
+++ b/lib/modules/platform/github/types.ts
@@ -115,6 +115,9 @@ export type BranchProtection = any;
 export interface GhRepo {
   id: string;
   isFork: boolean;
+  parent?: {
+    nameWithOwner: string;
+  };
   isArchived: boolean;
   nameWithOwner: string;
   autoMergeAllowed: boolean;
diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts
index 0cad95457e..5900222c07 100644
--- a/lib/util/git/index.ts
+++ b/lib/util/git/index.ts
@@ -585,7 +585,7 @@ export async function getFileList(): Promise<string[]> {
 }
 
 export function getBranchList(): string[] {
-  return Object.keys(config.branchCommits);
+  return Object.keys(config.branchCommits ?? /* istanbul ignore next */ {});
 }
 
 export async function isBranchBehindBase(
diff --git a/lib/workers/repository/error.spec.ts b/lib/workers/repository/error.spec.ts
index 6b2ed7a118..1c2a662370 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_MODE_FORKED,
   REPOSITORY_MIRRORED,
   REPOSITORY_NOT_FOUND,
   REPOSITORY_NO_PACKAGE_FILES,
@@ -47,6 +48,7 @@ describe('workers/repository/error', () => {
       REPOSITORY_DISABLED,
       REPOSITORY_CHANGED,
       REPOSITORY_FORKED,
+      REPOSITORY_FORK_MODE_FORKED,
       REPOSITORY_NO_PACKAGE_FILES,
       CONFIG_SECRETS_EXPOSED,
       CONFIG_VALIDATION,
diff --git a/lib/workers/repository/error.ts b/lib/workers/repository/error.ts
index 9433458deb..6e02b09315 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_MODE_FORKED,
   REPOSITORY_MIRRORED,
   REPOSITORY_NOT_FOUND,
   REPOSITORY_NO_CONFIG,
@@ -93,6 +94,12 @@ export default async function handleError(
     logger.error('Repository is not found');
     return err.message;
   }
+  if (err.message === REPOSITORY_FORK_MODE_FORKED) {
+    logger.info(
+      'Repository is a fork and cannot be processed when Renovate is running in fork mode itself',
+    );
+    return err.message;
+  }
   if (err.message === REPOSITORY_FORKED) {
     logger.info(
       'Repository is a fork and not manually configured - skipping - did you want to run with --fork-processing=enabled?',
diff --git a/lib/workers/repository/result.ts b/lib/workers/repository/result.ts
index 8382967188..70ba5f0c57 100644
--- a/lib/workers/repository/result.ts
+++ b/lib/workers/repository/result.ts
@@ -12,6 +12,7 @@ import {
   REPOSITORY_DISABLED_BY_CONFIG,
   REPOSITORY_EMPTY,
   REPOSITORY_FORKED,
+  REPOSITORY_FORK_MODE_FORKED,
   REPOSITORY_MIRRORED,
   REPOSITORY_NOT_FOUND,
   REPOSITORY_NO_CONFIG,
@@ -47,6 +48,7 @@ export function processResult(
     REPOSITORY_DISABLED,
     REPOSITORY_DISABLED_BY_CONFIG,
     REPOSITORY_EMPTY,
+    REPOSITORY_FORK_MODE_FORKED,
     REPOSITORY_FORKED,
     REPOSITORY_MIRRORED,
     REPOSITORY_NOT_FOUND,
-- 
GitLab