From 39fb207a83982bd0bb87b0e987755705d0df25f9 Mon Sep 17 00:00:00 2001
From: RahulGautamSingh <rahultesnik@gmail.com>
Date: Wed, 8 Jan 2025 17:48:30 +0530
Subject: [PATCH] refactor(workers/reconfigure): update code structure (#33340)

---
 lib/workers/repository/finalize/index.ts      |   4 +-
 lib/workers/repository/finalize/prune.ts      |   2 +-
 .../repository/reconfigure/index.spec.ts      | 228 ++----------------
 lib/workers/repository/reconfigure/index.ts   | 188 +--------------
 lib/workers/repository/reconfigure/utils.ts   |   3 +
 .../repository/reconfigure/validate.spec.ts   | 228 ++++++++++++++++++
 .../repository/reconfigure/validate.ts        | 184 ++++++++++++++
 7 files changed, 440 insertions(+), 397 deletions(-)
 create mode 100644 lib/workers/repository/reconfigure/utils.ts
 create mode 100644 lib/workers/repository/reconfigure/validate.spec.ts
 create mode 100644 lib/workers/repository/reconfigure/validate.ts

diff --git a/lib/workers/repository/finalize/index.ts b/lib/workers/repository/finalize/index.ts
index e530834c7e..8878201696 100644
--- a/lib/workers/repository/finalize/index.ts
+++ b/lib/workers/repository/finalize/index.ts
@@ -4,7 +4,7 @@ import { platform } from '../../../modules/platform';
 import * as repositoryCache from '../../../util/cache/repository';
 import { clearRenovateRefs } from '../../../util/git';
 import { PackageFiles } from '../package-files';
-import { validateReconfigureBranch } from '../reconfigure';
+import { checkReconfigureBranch } from '../reconfigure';
 import { pruneStaleBranches } from './prune';
 import {
   runBranchSummary,
@@ -16,7 +16,7 @@ export async function finalizeRepo(
   config: RenovateConfig,
   branchList: string[],
 ): Promise<void> {
-  await validateReconfigureBranch(config);
+  await checkReconfigureBranch(config);
   await repositoryCache.saveCache();
   await pruneStaleBranches(config, branchList);
   await ensureIssuesClosing();
diff --git a/lib/workers/repository/finalize/prune.ts b/lib/workers/repository/finalize/prune.ts
index 7d30c01f97..918751344b 100644
--- a/lib/workers/repository/finalize/prune.ts
+++ b/lib/workers/repository/finalize/prune.ts
@@ -9,7 +9,7 @@ import { scm } from '../../../modules/platform/scm';
 import { getBranchList, setUserRepoConfig } from '../../../util/git';
 import { escapeRegExp, regEx } from '../../../util/regex';
 import { uniqueStrings } from '../../../util/string';
-import { getReconfigureBranchName } from '../reconfigure';
+import { getReconfigureBranchName } from '../reconfigure/utils';
 
 async function cleanUpBranches(
   config: RenovateConfig,
diff --git a/lib/workers/repository/reconfigure/index.spec.ts b/lib/workers/repository/reconfigure/index.spec.ts
index d7b9e5a97d..52aff264f7 100644
--- a/lib/workers/repository/reconfigure/index.spec.ts
+++ b/lib/workers/repository/reconfigure/index.spec.ts
@@ -1,242 +1,42 @@
-import { mock } from 'jest-mock-extended';
 import type { RenovateConfig } from '../../../../test/util';
-import { fs, git, mocked, partial, platform, scm } from '../../../../test/util';
+import { logger, mocked, scm } from '../../../../test/util';
 import { GlobalConfig } from '../../../config/global';
-import { logger } from '../../../logger';
-import type { Pr } from '../../../modules/platform/types';
-import * as _cache from '../../../util/cache/repository';
-import type { LongCommitSha } from '../../../util/git/types';
-import * as _merge from '../init/merge';
-import { validateReconfigureBranch } from '.';
+import * as _validate from './validate';
+import { checkReconfigureBranch } from '.';
 
-jest.mock('../../../util/cache/repository');
-jest.mock('../../../util/fs');
-jest.mock('../../../util/git');
-jest.mock('../init/merge');
+jest.mock('./validate');
 
-const cache = mocked(_cache);
-const merge = mocked(_merge);
+const validate = mocked(_validate);
 
 describe('workers/repository/reconfigure/index', () => {
   const config: RenovateConfig = {
     branchPrefix: 'prefix/',
     baseBranch: 'base',
-    statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
-      configValidation: 'renovate/config-validation',
-    }),
   };
 
   beforeEach(() => {
-    config.repository = 'some/repo';
-    merge.detectConfigFile.mockResolvedValue('renovate.json');
-    scm.branchExists.mockResolvedValue(true);
-    cache.getCache.mockReturnValue({});
-    git.getBranchCommit.mockReturnValue('sha' as LongCommitSha);
-    fs.readLocalFile.mockResolvedValue(null);
-    platform.getBranchStatusCheck.mockResolvedValue(null);
     GlobalConfig.reset();
+    scm.branchExists.mockResolvedValue(true);
+    validate.validateReconfigureBranch.mockResolvedValue(undefined);
   });
 
   it('no effect when running with platform=local', async () => {
     GlobalConfig.set({ platform: 'local' });
-    await validateReconfigureBranch(config);
-    expect(logger.debug).toHaveBeenCalledWith(
+    await checkReconfigureBranch(config);
+    expect(logger.logger.debug).toHaveBeenCalledWith(
       'Not attempting to reconfigure when running with local platform',
     );
   });
 
   it('no effect on repo with no reconfigure branch', async () => {
     scm.branchExists.mockResolvedValueOnce(false);
-    await validateReconfigureBranch(config);
-    expect(logger.debug).toHaveBeenCalledWith('No reconfigure branch found');
-  });
-
-  it('logs error if config file search fails', async () => {
-    const err = new Error();
-    merge.detectConfigFile.mockRejectedValueOnce(err as never);
-    await validateReconfigureBranch(config);
-    expect(logger.error).toHaveBeenCalledWith(
-      { err },
-      'Error while searching for config file in reconfigure branch',
-    );
-  });
-
-  it('throws error if config file not found in reconfigure branch', async () => {
-    merge.detectConfigFile.mockResolvedValue(null);
-    await validateReconfigureBranch(config);
-    expect(logger.warn).toHaveBeenCalledWith(
-      'No config file found in reconfigure branch',
-    );
-  });
-
-  it('logs error if config file is unreadable', async () => {
-    const err = new Error();
-    fs.readLocalFile.mockRejectedValueOnce(err as never);
-    await validateReconfigureBranch(config);
-    expect(logger.error).toHaveBeenCalledWith(
-      { err },
-      'Error while reading config file',
-    );
-  });
-
-  it('throws error if config file is empty', async () => {
-    await validateReconfigureBranch(config);
-    expect(logger.warn).toHaveBeenCalledWith('Empty or invalid config file');
-  });
-
-  it('throws error if config file content is invalid', async () => {
-    fs.readLocalFile.mockResolvedValueOnce(`
-        {
-            "name":
-        }
-        `);
-    await validateReconfigureBranch(config);
-    expect(logger.error).toHaveBeenCalledWith(
-      { err: expect.any(Object) },
-      'Error while parsing config file',
-    );
-    expect(platform.setBranchStatus).toHaveBeenCalledWith({
-      branchName: 'prefix/reconfigure',
-      context: 'renovate/config-validation',
-      description: 'Validation Failed - Unparsable config file',
-      state: 'red',
-    });
-  });
-
-  it('handles failed validation', async () => {
-    fs.readLocalFile.mockResolvedValueOnce(`
-        {
-            "enabledManagers": ["docker"]
-        }
-        `);
-    await validateReconfigureBranch(config);
-    expect(logger.debug).toHaveBeenCalledWith(
-      { errors: expect.any(String) },
-      'Validation Errors',
-    );
-    expect(platform.setBranchStatus).toHaveBeenCalledWith({
-      branchName: 'prefix/reconfigure',
-      context: 'renovate/config-validation',
-      description: 'Validation Failed',
-      state: 'red',
-    });
-  });
-
-  it('adds comment if reconfigure PR exists', async () => {
-    fs.readLocalFile.mockResolvedValueOnce(`
-        {
-            "enabledManagers": ["docker"]
-        }
-        `);
-    platform.findPr.mockResolvedValueOnce(mock<Pr>({ number: 1 }));
-    await validateReconfigureBranch(config);
-    expect(logger.debug).toHaveBeenCalledWith(
-      { errors: expect.any(String) },
-      'Validation Errors',
-    );
-    expect(platform.setBranchStatus).toHaveBeenCalled();
-    expect(platform.ensureComment).toHaveBeenCalled();
-  });
-
-  it('handles successful validation', async () => {
-    const pJson = `
-    {
-       "renovate": {
-        "enabledManagers": ["npm"]
-       }
-    }
-    `;
-    merge.detectConfigFile.mockResolvedValue('package.json');
-    fs.readLocalFile.mockResolvedValueOnce(pJson).mockResolvedValueOnce(pJson);
-    await validateReconfigureBranch(config);
-    expect(platform.setBranchStatus).toHaveBeenCalledWith({
-      branchName: 'prefix/reconfigure',
-      context: 'renovate/config-validation',
-      description: 'Validation Successful',
-      state: 'green',
-    });
-  });
-
-  it('skips adding status check if statusCheckNames.configValidation is null', async () => {
-    cache.getCache.mockReturnValueOnce({
-      reconfigureBranchCache: {
-        reconfigureBranchSha: 'new-sha',
-        isConfigValid: false,
-      },
-    });
-
-    await validateReconfigureBranch({
-      ...config,
-      statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
-        configValidation: null,
-      }),
-    });
-    expect(logger.debug).toHaveBeenCalledWith(
-      'Status check is null or an empty string, skipping status check addition.',
-    );
-    expect(platform.setBranchStatus).not.toHaveBeenCalled();
-  });
-
-  it('skips adding status check if statusCheckNames.configValidation is empty string', async () => {
-    cache.getCache.mockReturnValueOnce({
-      reconfigureBranchCache: {
-        reconfigureBranchSha: 'new-sha',
-        isConfigValid: false,
-      },
-    });
-
-    await validateReconfigureBranch({
-      ...config,
-      statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
-        configValidation: '',
-      }),
-    });
-    expect(logger.debug).toHaveBeenCalledWith(
-      'Status check is null or an empty string, skipping status check addition.',
-    );
-    expect(platform.setBranchStatus).not.toHaveBeenCalled();
-  });
-
-  it('skips validation if cache is valid', async () => {
-    cache.getCache.mockReturnValueOnce({
-      reconfigureBranchCache: {
-        reconfigureBranchSha: 'sha',
-        isConfigValid: false,
-      },
-    });
-    await validateReconfigureBranch(config);
-    expect(logger.debug).toHaveBeenCalledWith(
-      'Skipping validation check as branch sha is unchanged',
-    );
-  });
-
-  it('skips validation if status check present', async () => {
-    cache.getCache.mockReturnValueOnce({
-      reconfigureBranchCache: {
-        reconfigureBranchSha: 'new_sha',
-        isConfigValid: false,
-      },
-    });
-    platform.getBranchStatusCheck.mockResolvedValueOnce('green');
-    await validateReconfigureBranch(config);
-    expect(logger.debug).toHaveBeenCalledWith(
-      'Skipping validation check because status check already exists.',
+    await checkReconfigureBranch(config);
+    expect(logger.logger.debug).toHaveBeenCalledWith(
+      'No reconfigure branch found',
     );
   });
 
-  it('handles non-default config file', async () => {
-    merge.detectConfigFile.mockResolvedValue('.renovaterc');
-    fs.readLocalFile.mockResolvedValueOnce(`
-        {
-            "enabledManagers": ["npm",]
-        }
-        `);
-    await validateReconfigureBranch(config);
-    expect(platform.setBranchStatus).toHaveBeenCalledWith({
-      branchName: 'prefix/reconfigure',
-      context: 'renovate/config-validation',
-      description: 'Validation Successful',
-      state: 'green',
-    });
+  it('validates reconfigure branch', async () => {
+    await expect(checkReconfigureBranch(config)).toResolve();
   });
 });
diff --git a/lib/workers/repository/reconfigure/index.ts b/lib/workers/repository/reconfigure/index.ts
index abdb1d0146..5977918c3b 100644
--- a/lib/workers/repository/reconfigure/index.ts
+++ b/lib/workers/repository/reconfigure/index.ts
@@ -1,49 +1,15 @@
-import is from '@sindresorhus/is';
-import JSON5 from 'json5';
 import { GlobalConfig } from '../../../config/global';
 import type { RenovateConfig } from '../../../config/types';
-import { validateConfig } from '../../../config/validation';
 import { logger } from '../../../logger';
-import { platform } from '../../../modules/platform';
-import { ensureComment } from '../../../modules/platform/comment';
 import { scm } from '../../../modules/platform/scm';
-import type { BranchStatus } from '../../../types';
-import { getCache } from '../../../util/cache/repository';
-import { readLocalFile } from '../../../util/fs';
-import { getBranchCommit } from '../../../util/git';
-import { regEx } from '../../../util/regex';
-import { detectConfigFile } from '../init/merge';
-import {
-  deleteReconfigureBranchCache,
-  setReconfigureBranchCache,
-} from './reconfigure-cache';
+import { deleteReconfigureBranchCache } from './reconfigure-cache';
+import { getReconfigureBranchName } from './utils';
+import { validateReconfigureBranch } from './validate';
 
-async function setBranchStatus(
-  branchName: string,
-  description: string,
-  state: BranchStatus,
-  context?: string | null,
-): Promise<void> {
-  if (!is.nonEmptyString(context)) {
-    // already logged this case when validating the status check
-    return;
-  }
-
-  await platform.setBranchStatus({
-    branchName,
-    context,
-    description,
-    state,
-  });
-}
-
-export function getReconfigureBranchName(prefix: string): string {
-  return `${prefix}reconfigure`;
-}
-export async function validateReconfigureBranch(
+export async function checkReconfigureBranch(
   config: RenovateConfig,
 ): Promise<void> {
-  logger.debug('validateReconfigureBranch()');
+  logger.debug('checkReconfigureBranch()');
   if (GlobalConfig.get('platform') === 'local') {
     logger.debug(
       'Not attempting to reconfigure when running with local platform',
@@ -51,10 +17,8 @@ export async function validateReconfigureBranch(
     return;
   }
 
-  const context = config.statusCheckNames?.configValidation;
-
-  const branchName = getReconfigureBranchName(config.branchPrefix!);
-  const branchExists = await scm.branchExists(branchName);
+  const reconfigureBranch = getReconfigureBranchName(config.branchPrefix!);
+  const branchExists = await scm.branchExists(reconfigureBranch);
 
   // this is something the user initiates, so skip if no branch exists
   if (!branchExists) {
@@ -63,141 +27,5 @@ export async function validateReconfigureBranch(
     return;
   }
 
-  // look for config file
-  // 1. check reconfigure branch cache and use the configFileName if it exists
-  // 2. checkout reconfigure branch and look for the config file, don't assume default configFileName
-  const branchSha = getBranchCommit(branchName)!;
-  const cache = getCache();
-  let configFileName: string | null = null;
-  const reconfigureCache = cache.reconfigureBranchCache;
-  // only use valid cached information
-  if (reconfigureCache?.reconfigureBranchSha === branchSha) {
-    logger.debug('Skipping validation check as branch sha is unchanged');
-    return;
-  }
-
-  if (context) {
-    const validationStatus = await platform.getBranchStatusCheck(
-      branchName,
-      context,
-    );
-
-    // if old status check is present skip validation
-    if (is.nonEmptyString(validationStatus)) {
-      logger.debug(
-        'Skipping validation check because status check already exists.',
-      );
-      return;
-    }
-  } else {
-    logger.debug(
-      'Status check is null or an empty string, skipping status check addition.',
-    );
-  }
-
-  try {
-    await scm.checkoutBranch(branchName);
-    configFileName = await detectConfigFile();
-  } catch (err) {
-    logger.error(
-      { err },
-      'Error while searching for config file in reconfigure branch',
-    );
-  }
-
-  if (!is.nonEmptyString(configFileName)) {
-    logger.warn('No config file found in reconfigure branch');
-    await setBranchStatus(
-      branchName,
-      'Validation Failed - No config file found',
-      'red',
-      context,
-    );
-    setReconfigureBranchCache(branchSha, false);
-    await scm.checkoutBranch(config.defaultBranch!);
-    return;
-  }
-
-  let configFileRaw: string | null = null;
-  try {
-    configFileRaw = await readLocalFile(configFileName, 'utf8');
-  } catch (err) {
-    logger.error({ err }, 'Error while reading config file');
-  }
-
-  if (!is.nonEmptyString(configFileRaw)) {
-    logger.warn('Empty or invalid config file');
-    await setBranchStatus(
-      branchName,
-      'Validation Failed - Empty/Invalid config file',
-      'red',
-      context,
-    );
-    setReconfigureBranchCache(branchSha, false);
-    await scm.checkoutBranch(config.baseBranch!);
-    return;
-  }
-
-  let configFileParsed: any;
-  try {
-    configFileParsed = JSON5.parse(configFileRaw);
-    // no need to confirm renovate field in package.json we already do it in `detectConfigFile()`
-    if (configFileName === 'package.json') {
-      configFileParsed = configFileParsed.renovate;
-    }
-  } catch (err) {
-    logger.error({ err }, 'Error while parsing config file');
-    await setBranchStatus(
-      branchName,
-      'Validation Failed - Unparsable config file',
-      'red',
-      context,
-    );
-    setReconfigureBranchCache(branchSha, false);
-    await scm.checkoutBranch(config.baseBranch!);
-    return;
-  }
-
-  // perform validation and provide a passing or failing check run based on result
-  const validationResult = await validateConfig('repo', configFileParsed);
-
-  // failing check
-  if (validationResult.errors.length > 0) {
-    logger.debug(
-      { errors: validationResult.errors.map((err) => err.message).join(', ') },
-      'Validation Errors',
-    );
-
-    // add comment to reconfigure PR if it exists
-    const branchPr = await platform.findPr({
-      branchName,
-      state: 'open',
-      includeOtherAuthors: true,
-    });
-    if (branchPr) {
-      let body = `There is an error with this repository's Renovate configuration that needs to be fixed.\n\n`;
-      body += `Location: \`${configFileName}\`\n`;
-      body += `Message: \`${validationResult.errors
-        .map((e) => e.message)
-        .join(', ')
-        .replace(regEx(/`/g), "'")}\`\n`;
-
-      await ensureComment({
-        number: branchPr.number,
-        topic: 'Action Required: Fix Renovate Configuration',
-        content: body,
-      });
-    }
-
-    await setBranchStatus(branchName, 'Validation Failed', 'red', context);
-    setReconfigureBranchCache(branchSha, false);
-    await scm.checkoutBranch(config.baseBranch!);
-    return;
-  }
-
-  // passing check
-  await setBranchStatus(branchName, 'Validation Successful', 'green', context);
-
-  setReconfigureBranchCache(branchSha, true);
-  await scm.checkoutBranch(config.baseBranch!);
+  await validateReconfigureBranch(config);
 }
diff --git a/lib/workers/repository/reconfigure/utils.ts b/lib/workers/repository/reconfigure/utils.ts
new file mode 100644
index 0000000000..e5208d6a10
--- /dev/null
+++ b/lib/workers/repository/reconfigure/utils.ts
@@ -0,0 +1,3 @@
+export function getReconfigureBranchName(prefix: string): string {
+  return `${prefix}reconfigure`;
+}
diff --git a/lib/workers/repository/reconfigure/validate.spec.ts b/lib/workers/repository/reconfigure/validate.spec.ts
new file mode 100644
index 0000000000..730bf75e37
--- /dev/null
+++ b/lib/workers/repository/reconfigure/validate.spec.ts
@@ -0,0 +1,228 @@
+import { mock } from 'jest-mock-extended';
+import type { RenovateConfig } from '../../../../test/util';
+import { fs, git, mocked, partial, platform, scm } from '../../../../test/util';
+import { GlobalConfig } from '../../../config/global';
+import { logger } from '../../../logger';
+import type { Pr } from '../../../modules/platform/types';
+import * as _cache from '../../../util/cache/repository';
+import type { LongCommitSha } from '../../../util/git/types';
+import * as _merge from '../init/merge';
+import { validateReconfigureBranch } from './validate';
+
+jest.mock('../../../util/cache/repository');
+jest.mock('../../../util/fs');
+jest.mock('../../../util/git');
+jest.mock('../init/merge');
+
+const cache = mocked(_cache);
+const merge = mocked(_merge);
+
+describe('workers/repository/reconfigure/validate', () => {
+  const config: RenovateConfig = {
+    branchPrefix: 'prefix/',
+    baseBranch: 'base',
+    statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
+      configValidation: 'renovate/config-validation',
+    }),
+  };
+
+  beforeEach(() => {
+    config.repository = 'some/repo';
+    merge.detectConfigFile.mockResolvedValue('renovate.json');
+    scm.branchExists.mockResolvedValue(true);
+    cache.getCache.mockReturnValue({});
+    git.getBranchCommit.mockReturnValue('sha' as LongCommitSha);
+    fs.readLocalFile.mockResolvedValue(null);
+    platform.getBranchStatusCheck.mockResolvedValue(null);
+    GlobalConfig.reset();
+  });
+
+  it('logs error if config file search fails', async () => {
+    const err = new Error();
+    merge.detectConfigFile.mockRejectedValueOnce(err as never);
+    await validateReconfigureBranch(config);
+    expect(logger.error).toHaveBeenCalledWith(
+      { err },
+      'Error while searching for config file in reconfigure branch',
+    );
+  });
+
+  it('throws error if config file not found in reconfigure branch', async () => {
+    merge.detectConfigFile.mockResolvedValue(null);
+    await validateReconfigureBranch(config);
+    expect(logger.warn).toHaveBeenCalledWith(
+      'No config file found in reconfigure branch',
+    );
+  });
+
+  it('logs error if config file is unreadable', async () => {
+    const err = new Error();
+    fs.readLocalFile.mockRejectedValueOnce(err as never);
+    await validateReconfigureBranch(config);
+    expect(logger.error).toHaveBeenCalledWith(
+      { err },
+      'Error while reading config file',
+    );
+  });
+
+  it('throws error if config file is empty', async () => {
+    await validateReconfigureBranch(config);
+    expect(logger.warn).toHaveBeenCalledWith('Empty or invalid config file');
+  });
+
+  it('throws error if config file content is invalid', async () => {
+    fs.readLocalFile.mockResolvedValueOnce(`
+        {
+            "name":
+        }
+        `);
+    await validateReconfigureBranch(config);
+    expect(logger.error).toHaveBeenCalledWith(
+      { err: expect.any(Object) },
+      'Error while parsing config file',
+    );
+    expect(platform.setBranchStatus).toHaveBeenCalledWith({
+      branchName: 'prefix/reconfigure',
+      context: 'renovate/config-validation',
+      description: 'Validation Failed - Unparsable config file',
+      state: 'red',
+    });
+  });
+
+  it('handles failed validation', async () => {
+    fs.readLocalFile.mockResolvedValueOnce(`
+        {
+            "enabledManagers": ["docker"]
+        }
+        `);
+    await validateReconfigureBranch(config);
+    expect(logger.debug).toHaveBeenCalledWith(
+      { errors: expect.any(String) },
+      'Validation Errors',
+    );
+    expect(platform.setBranchStatus).toHaveBeenCalledWith({
+      branchName: 'prefix/reconfigure',
+      context: 'renovate/config-validation',
+      description: 'Validation Failed',
+      state: 'red',
+    });
+  });
+
+  it('adds comment if reconfigure PR exists', async () => {
+    fs.readLocalFile.mockResolvedValueOnce(`
+        {
+            "enabledManagers": ["docker"]
+        }
+        `);
+    platform.findPr.mockResolvedValueOnce(mock<Pr>({ number: 1 }));
+    await validateReconfigureBranch(config);
+    expect(logger.debug).toHaveBeenCalledWith(
+      { errors: expect.any(String) },
+      'Validation Errors',
+    );
+    expect(platform.setBranchStatus).toHaveBeenCalled();
+    expect(platform.ensureComment).toHaveBeenCalled();
+  });
+
+  it('handles successful validation', async () => {
+    const pJson = `
+    {
+       "renovate": {
+        "enabledManagers": ["npm"]
+       }
+    }
+    `;
+    merge.detectConfigFile.mockResolvedValue('package.json');
+    fs.readLocalFile.mockResolvedValueOnce(pJson).mockResolvedValueOnce(pJson);
+    await validateReconfigureBranch(config);
+    expect(platform.setBranchStatus).toHaveBeenCalledWith({
+      branchName: 'prefix/reconfigure',
+      context: 'renovate/config-validation',
+      description: 'Validation Successful',
+      state: 'green',
+    });
+  });
+
+  it('skips adding status check if statusCheckNames.configValidation is null', async () => {
+    cache.getCache.mockReturnValueOnce({
+      reconfigureBranchCache: {
+        reconfigureBranchSha: 'new-sha',
+        isConfigValid: false,
+      },
+    });
+
+    await validateReconfigureBranch({
+      ...config,
+      statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
+        configValidation: null,
+      }),
+    });
+    expect(logger.debug).toHaveBeenCalledWith(
+      'Status check is null or an empty string, skipping status check addition.',
+    );
+    expect(platform.setBranchStatus).not.toHaveBeenCalled();
+  });
+
+  it('skips adding status check if statusCheckNames.configValidation is empty string', async () => {
+    cache.getCache.mockReturnValueOnce({
+      reconfigureBranchCache: {
+        reconfigureBranchSha: 'new-sha',
+        isConfigValid: false,
+      },
+    });
+
+    await validateReconfigureBranch({
+      ...config,
+      statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
+        configValidation: '',
+      }),
+    });
+    expect(logger.debug).toHaveBeenCalledWith(
+      'Status check is null or an empty string, skipping status check addition.',
+    );
+    expect(platform.setBranchStatus).not.toHaveBeenCalled();
+  });
+
+  it('skips validation if cache is valid', async () => {
+    cache.getCache.mockReturnValueOnce({
+      reconfigureBranchCache: {
+        reconfigureBranchSha: 'sha',
+        isConfigValid: false,
+      },
+    });
+    await validateReconfigureBranch(config);
+    expect(logger.debug).toHaveBeenCalledWith(
+      'Skipping validation check as branch sha is unchanged',
+    );
+  });
+
+  it('skips validation if status check present', async () => {
+    cache.getCache.mockReturnValueOnce({
+      reconfigureBranchCache: {
+        reconfigureBranchSha: 'new_sha',
+        isConfigValid: false,
+      },
+    });
+    platform.getBranchStatusCheck.mockResolvedValueOnce('green');
+    await validateReconfigureBranch(config);
+    expect(logger.debug).toHaveBeenCalledWith(
+      'Skipping validation check because status check already exists.',
+    );
+  });
+
+  it('handles non-default config file', async () => {
+    merge.detectConfigFile.mockResolvedValue('.renovaterc');
+    fs.readLocalFile.mockResolvedValueOnce(`
+        {
+            "enabledManagers": ["npm",]
+        }
+        `);
+    await validateReconfigureBranch(config);
+    expect(platform.setBranchStatus).toHaveBeenCalledWith({
+      branchName: 'prefix/reconfigure',
+      context: 'renovate/config-validation',
+      description: 'Validation Successful',
+      state: 'green',
+    });
+  });
+});
diff --git a/lib/workers/repository/reconfigure/validate.ts b/lib/workers/repository/reconfigure/validate.ts
new file mode 100644
index 0000000000..ca1b6a68d6
--- /dev/null
+++ b/lib/workers/repository/reconfigure/validate.ts
@@ -0,0 +1,184 @@
+import is from '@sindresorhus/is';
+import JSON5 from 'json5';
+import type { RenovateConfig } from '../../../config/types';
+import { validateConfig } from '../../../config/validation';
+import { logger } from '../../../logger';
+import { platform } from '../../../modules/platform';
+import { ensureComment } from '../../../modules/platform/comment';
+import { scm } from '../../../modules/platform/scm';
+import type { BranchStatus } from '../../../types';
+import { getCache } from '../../../util/cache/repository';
+import { readLocalFile } from '../../../util/fs';
+import { getBranchCommit } from '../../../util/git';
+import { regEx } from '../../../util/regex';
+import { detectConfigFile } from '../init/merge';
+import { setReconfigureBranchCache } from './reconfigure-cache';
+import { getReconfigureBranchName } from './utils';
+
+async function setBranchStatus(
+  branchName: string,
+  description: string,
+  state: BranchStatus,
+  context?: string | null,
+): Promise<void> {
+  if (!is.nonEmptyString(context)) {
+    // already logged this case when validating the status check
+    return;
+  }
+
+  await platform.setBranchStatus({
+    branchName,
+    context,
+    description,
+    state,
+  });
+}
+
+export async function validateReconfigureBranch(
+  config: RenovateConfig,
+): Promise<void> {
+  logger.debug('validateReconfigureBranch()');
+
+  const context = config.statusCheckNames?.configValidation;
+  const branchName = getReconfigureBranchName(config.branchPrefix!);
+
+  // look for config file
+  // 1. check reconfigure branch cache and use the configFileName if it exists
+  // 2. checkout reconfigure branch and look for the config file, don't assume default configFileName
+  const branchSha = getBranchCommit(branchName)!;
+  const cache = getCache();
+  let configFileName: string | null = null;
+  const reconfigureCache = cache.reconfigureBranchCache;
+  // only use valid cached information
+  if (reconfigureCache?.reconfigureBranchSha === branchSha) {
+    logger.debug('Skipping validation check as branch sha is unchanged');
+    return;
+  }
+
+  if (context) {
+    const validationStatus = await platform.getBranchStatusCheck(
+      branchName,
+      context,
+    );
+
+    // if old status check is present skip validation
+    if (is.nonEmptyString(validationStatus)) {
+      logger.debug(
+        'Skipping validation check because status check already exists.',
+      );
+      return;
+    }
+  } else {
+    logger.debug(
+      'Status check is null or an empty string, skipping status check addition.',
+    );
+  }
+
+  try {
+    await scm.checkoutBranch(branchName);
+    configFileName = await detectConfigFile();
+  } catch (err) {
+    logger.error(
+      { err },
+      'Error while searching for config file in reconfigure branch',
+    );
+  }
+
+  if (!is.nonEmptyString(configFileName)) {
+    logger.warn('No config file found in reconfigure branch');
+    await setBranchStatus(
+      branchName,
+      'Validation Failed - No config file found',
+      'red',
+      context,
+    );
+    setReconfigureBranchCache(branchSha, false);
+    await scm.checkoutBranch(config.defaultBranch!);
+    return;
+  }
+
+  let configFileRaw: string | null = null;
+  try {
+    configFileRaw = await readLocalFile(configFileName, 'utf8');
+  } catch (err) {
+    logger.error({ err }, 'Error while reading config file');
+  }
+
+  if (!is.nonEmptyString(configFileRaw)) {
+    logger.warn('Empty or invalid config file');
+    await setBranchStatus(
+      branchName,
+      'Validation Failed - Empty/Invalid config file',
+      'red',
+      context,
+    );
+    setReconfigureBranchCache(branchSha, false);
+    await scm.checkoutBranch(config.baseBranch!);
+    return;
+  }
+
+  let configFileParsed: any;
+  try {
+    configFileParsed = JSON5.parse(configFileRaw);
+    // no need to confirm renovate field in package.json we already do it in `detectConfigFile()`
+    if (configFileName === 'package.json') {
+      configFileParsed = configFileParsed.renovate;
+    }
+  } catch (err) {
+    logger.error({ err }, 'Error while parsing config file');
+    await setBranchStatus(
+      branchName,
+      'Validation Failed - Unparsable config file',
+      'red',
+      context,
+    );
+    setReconfigureBranchCache(branchSha, false);
+    await scm.checkoutBranch(config.baseBranch!);
+    return;
+  }
+
+  // perform validation and provide a passing or failing check based on result
+  const validationResult = await validateConfig('repo', configFileParsed);
+
+  // failing check
+  if (validationResult.errors.length > 0) {
+    logger.debug(
+      { errors: validationResult.errors.map((err) => err.message).join(', ') },
+      'Validation Errors',
+    );
+
+    const reconfigurePr = await platform.findPr({
+      branchName,
+      state: 'open',
+      includeOtherAuthors: true,
+    });
+
+    // add comment to reconfigure PR if it exists
+    if (reconfigurePr) {
+      let body = `There is an error with this repository's Renovate configuration that needs to be fixed.\n\n`;
+      body += `Location: \`${configFileName}\`\n`;
+      body += `Message: \`${validationResult.errors
+        .map((e) => e.message)
+        .join(', ')
+        .replace(regEx(/`/g), "'")}\`\n`;
+
+      await ensureComment({
+        number: reconfigurePr.number,
+        topic: 'Action Required: Fix Renovate Configuration',
+        content: body,
+      });
+    }
+
+    await setBranchStatus(branchName, 'Validation Failed', 'red', context);
+    setReconfigureBranchCache(branchSha, false);
+    await scm.checkoutBranch(config.baseBranch!);
+    return;
+  }
+
+  // passing check
+  await setBranchStatus(branchName, 'Validation Successful', 'green', context);
+
+  setReconfigureBranchCache(branchSha, true);
+  await scm.checkoutBranch(config.baseBranch!);
+  return;
+}
-- 
GitLab