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