This commit is contained in:
RahulGautamSingh 2025-01-03 03:30:58 +05:30 committed by GitHub
commit 1489a3a4dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 441 additions and 397 deletions

View file

@ -4,7 +4,7 @@ import { platform } from '../../../modules/platform';
import * as repositoryCache from '../../../util/cache/repository'; import * as repositoryCache from '../../../util/cache/repository';
import { clearRenovateRefs } from '../../../util/git'; import { clearRenovateRefs } from '../../../util/git';
import { PackageFiles } from '../package-files'; import { PackageFiles } from '../package-files';
import { validateReconfigureBranch } from '../reconfigure'; import { checkReconfigureBranch } from '../reconfigure';
import { pruneStaleBranches } from './prune'; import { pruneStaleBranches } from './prune';
import { import {
runBranchSummary, runBranchSummary,
@ -16,7 +16,7 @@ export async function finalizeRepo(
config: RenovateConfig, config: RenovateConfig,
branchList: string[], branchList: string[],
): Promise<void> { ): Promise<void> {
await validateReconfigureBranch(config); await checkReconfigureBranch(config);
await repositoryCache.saveCache(); await repositoryCache.saveCache();
await pruneStaleBranches(config, branchList); await pruneStaleBranches(config, branchList);
await ensureIssuesClosing(); await ensureIssuesClosing();

View file

@ -9,7 +9,7 @@ import { scm } from '../../../modules/platform/scm';
import { getBranchList, setUserRepoConfig } from '../../../util/git'; import { getBranchList, setUserRepoConfig } from '../../../util/git';
import { escapeRegExp, regEx } from '../../../util/regex'; import { escapeRegExp, regEx } from '../../../util/regex';
import { uniqueStrings } from '../../../util/string'; import { uniqueStrings } from '../../../util/string';
import { getReconfigureBranchName } from '../reconfigure'; import { getReconfigureBranchName } from '../reconfigure/utils';
async function cleanUpBranches( async function cleanUpBranches(
config: RenovateConfig, config: RenovateConfig,

View file

@ -1,242 +1,42 @@
import { mock } from 'jest-mock-extended';
import type { RenovateConfig } from '../../../../test/util'; 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 { GlobalConfig } from '../../../config/global';
import { logger } from '../../../logger'; import * as _validate from './validate';
import type { Pr } from '../../../modules/platform/types'; import { checkReconfigureBranch } from '.';
import * as _cache from '../../../util/cache/repository';
import type { LongCommitSha } from '../../../util/git/types';
import * as _merge from '../init/merge';
import { validateReconfigureBranch } from '.';
jest.mock('../../../util/cache/repository'); jest.mock('./validate');
jest.mock('../../../util/fs');
jest.mock('../../../util/git');
jest.mock('../init/merge');
const cache = mocked(_cache); const validate = mocked(_validate);
const merge = mocked(_merge);
describe('workers/repository/reconfigure/index', () => { describe('workers/repository/reconfigure/index', () => {
const config: RenovateConfig = { const config: RenovateConfig = {
branchPrefix: 'prefix/', branchPrefix: 'prefix/',
baseBranch: 'base', baseBranch: 'base',
statusCheckNames: partial<RenovateConfig['statusCheckNames']>({
configValidation: 'renovate/config-validation',
}),
}; };
beforeEach(() => { 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(); GlobalConfig.reset();
scm.branchExists.mockResolvedValue(true);
validate.validateReconfigureBranch.mockResolvedValue(undefined);
}); });
it('no effect when running with platform=local', async () => { it('no effect when running with platform=local', async () => {
GlobalConfig.set({ platform: 'local' }); GlobalConfig.set({ platform: 'local' });
await validateReconfigureBranch(config); await checkReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith( expect(logger.logger.debug).toHaveBeenCalledWith(
'Not attempting to reconfigure when running with local platform', 'Not attempting to reconfigure when running with local platform',
); );
}); });
it('no effect on repo with no reconfigure branch', async () => { it('no effect on repo with no reconfigure branch', async () => {
scm.branchExists.mockResolvedValueOnce(false); scm.branchExists.mockResolvedValueOnce(false);
await validateReconfigureBranch(config); await checkReconfigureBranch(config);
expect(logger.debug).toHaveBeenCalledWith('No reconfigure branch found'); expect(logger.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 () => { it('validates reconfigure branch', async () => {
merge.detectConfigFile.mockResolvedValue(null); await expect(checkReconfigureBranch(config)).toResolve();
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',
});
}); });
}); });

View file

@ -1,49 +1,15 @@
import is from '@sindresorhus/is';
import JSON5 from 'json5';
import { GlobalConfig } from '../../../config/global'; import { GlobalConfig } from '../../../config/global';
import type { RenovateConfig } from '../../../config/types'; import type { RenovateConfig } from '../../../config/types';
import { validateConfig } from '../../../config/validation';
import { logger } from '../../../logger'; import { logger } from '../../../logger';
import { platform } from '../../../modules/platform';
import { ensureComment } from '../../../modules/platform/comment';
import { scm } from '../../../modules/platform/scm'; import { scm } from '../../../modules/platform/scm';
import type { BranchStatus } from '../../../types'; import { deleteReconfigureBranchCache } from './reconfigure-cache';
import { getCache } from '../../../util/cache/repository'; import { getReconfigureBranchName } from './utils';
import { readLocalFile } from '../../../util/fs'; import { validateReconfigureBranch } from './validate';
import { getBranchCommit } from '../../../util/git';
import { regEx } from '../../../util/regex';
import { detectConfigFile } from '../init/merge';
import {
deleteReconfigureBranchCache,
setReconfigureBranchCache,
} from './reconfigure-cache';
async function setBranchStatus( export async function checkReconfigureBranch(
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(
config: RenovateConfig, config: RenovateConfig,
): Promise<void> { ): Promise<void> {
logger.debug('validateReconfigureBranch()'); logger.debug('checkReconfigureBranch()');
if (GlobalConfig.get('platform') === 'local') { if (GlobalConfig.get('platform') === 'local') {
logger.debug( logger.debug(
'Not attempting to reconfigure when running with local platform', 'Not attempting to reconfigure when running with local platform',
@ -51,10 +17,8 @@ export async function validateReconfigureBranch(
return; return;
} }
const context = config.statusCheckNames?.configValidation; const reconfigureBranch = getReconfigureBranchName(config.branchPrefix!);
const branchExists = await scm.branchExists(reconfigureBranch);
const branchName = getReconfigureBranchName(config.branchPrefix!);
const branchExists = await scm.branchExists(branchName);
// this is something the user initiates, so skip if no branch exists // this is something the user initiates, so skip if no branch exists
if (!branchExists) { if (!branchExists) {
@ -63,141 +27,5 @@ export async function validateReconfigureBranch(
return; return;
} }
// look for config file await validateReconfigureBranch(config);
// 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!);
} }

View file

@ -0,0 +1,3 @@
export function getReconfigureBranchName(prefix: string): string {
return `${prefix}reconfigure`;
}

View file

@ -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',
});
});
});

View file

@ -0,0 +1,185 @@
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 run based on result
const validationResult = await validateConfig('repo', configFileParsed);
// failing check
// what happens when a status check is failed with errors and a comment is added but then the errors are resolved and a new commit is created .. is the comment deleted? test it
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;
}