mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-10 14:06:30 +00:00
Merge d79aa81503
into c12c57b2a8
This commit is contained in:
commit
1489a3a4dd
7 changed files with 441 additions and 397 deletions
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
await checkReconfigureBranch(config);
|
||||
expect(logger.logger.debug).toHaveBeenCalledWith(
|
||||
'No reconfigure branch found',
|
||||
);
|
||||
});
|
||||
|
||||
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',
|
||||
});
|
||||
it('validates reconfigure branch', async () => {
|
||||
await expect(checkReconfigureBranch(config)).toResolve();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
3
lib/workers/repository/reconfigure/utils.ts
Normal file
3
lib/workers/repository/reconfigure/utils.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function getReconfigureBranchName(prefix: string): string {
|
||||
return `${prefix}reconfigure`;
|
||||
}
|
228
lib/workers/repository/reconfigure/validate.spec.ts
Normal file
228
lib/workers/repository/reconfigure/validate.spec.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
185
lib/workers/repository/reconfigure/validate.ts
Normal file
185
lib/workers/repository/reconfigure/validate.ts
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue