mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 15:01:49 +00:00
11a217ea2e
* fix(config-validator): warn when RE2 is not in use Without RE2 the validator checks patterns with the JS RegExp engine, which accepts syntax RE2 rejects, so configs can validate clean here and then be rejected when Renovate runs. Co-Authored-By: Claude (Opus 5) <noreply@anthropic.com> * fix(config-validator): debug log for the RE2 opt-out Setting RENOVATE_X_IGNORE_RE2 is deliberate, so it does not warrant a warning, matching what the global worker already does. Co-Authored-By: Claude (Opus 5) <noreply@anthropic.com> --------- Co-authored-by: Claude (Opus 5) <noreply@anthropic.com> Co-authored-by: Jamie Tanna <jamie.tanna@mend.io>
317 lines
10 KiB
JavaScript
317 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
import 'source-map-support/register.js';
|
|
import './punycode.cjs';
|
|
|
|
import { styleText } from 'node:util';
|
|
import { isNonEmptyStringAndNotWhitespace } from '@sindresorhus/is';
|
|
import { Command, CommanderError } from 'commander';
|
|
import { dequal } from 'dequal';
|
|
import { diffLines } from 'diff';
|
|
import fs from 'fs-extra';
|
|
import { getConfigFileNames } from './config/app-strings.ts';
|
|
import { GlobalConfig } from './config/global.ts';
|
|
import { massageConfig } from './config/massage.ts';
|
|
import { migrateConfig } from './config/migration.ts';
|
|
import type { RenovateConfig } from './config/types.ts';
|
|
import { validateConfig } from './config/validation.ts';
|
|
import { pkg } from './expose.ts';
|
|
import { init, logger } from './logger/index.ts';
|
|
import { getEnv } from './util/env.ts';
|
|
import { add as addHostRule } from './util/host-rules.ts';
|
|
import { regEx, regexEngineStatus } from './util/regex.ts';
|
|
import { getConfig as getFileConfig } from './workers/global/config/parse/file.ts';
|
|
import { parseConfigs } from './workers/global/config/parse/index.ts';
|
|
import { getParsedContent } from './workers/global/config/parse/util.ts';
|
|
|
|
await init();
|
|
|
|
const { pathExists, readFile } = fs;
|
|
|
|
let returnVal = 0;
|
|
|
|
/**
|
|
* Make sure that we've resolved configuration from the different places that Renovate users would expect them to be specified
|
|
*
|
|
* This then allows a `configType=repo` config to i.e. be validated alongside a `config.js` or `env RENOVATE_ALLOWED_COMMANDS=...`
|
|
*
|
|
* Note that we intentionally don't fully initialize Renovate and its modules, as we're not fully running, and it would require a Platform to be configured
|
|
* */
|
|
async function partiallyGlobalInitialize(): Promise<void> {
|
|
// NOTE that this doesn't allow command-line arguments
|
|
const globalConfig = await parseConfigs(getEnv(), []);
|
|
GlobalConfig.set(globalConfig);
|
|
|
|
if (globalConfig.hostRules) {
|
|
for (const hostRule of globalConfig.hostRules) {
|
|
addHostRule(hostRule);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function validate(
|
|
configType: 'global' | 'repo',
|
|
desc: string,
|
|
config: RenovateConfig,
|
|
strict: boolean,
|
|
isPreset = false,
|
|
): Promise<void> {
|
|
if (config.hostRules) {
|
|
for (const hostRule of config.hostRules) {
|
|
addHostRule(hostRule);
|
|
}
|
|
}
|
|
const { isMigrated, migratedConfig } = migrateConfig(config);
|
|
if (isMigrated) {
|
|
logger.warn(
|
|
{
|
|
oldConfig: config,
|
|
newConfig: migratedConfig,
|
|
},
|
|
'Config migration necessary',
|
|
);
|
|
const changedObjects = diffLines(
|
|
JSON.stringify(config, null, 2),
|
|
JSON.stringify(migratedConfig, null, 2),
|
|
{ ignoreWhitespace: false, newlineIsToken: false },
|
|
);
|
|
const added = styleText('green', '+ ');
|
|
const removed = styleText('red', '- ');
|
|
const msg = changedObjects
|
|
.flatMap((part) => {
|
|
let linePrefix: string;
|
|
if (part.added) {
|
|
linePrefix = added;
|
|
} else if (part.removed) {
|
|
linePrefix = removed;
|
|
} else {
|
|
linePrefix = ' ';
|
|
}
|
|
return part.value
|
|
.split('\n')
|
|
.filter(isNonEmptyStringAndNotWhitespace)
|
|
.map((line) =>
|
|
line.replace(regEx(/^(?<ws> *)/), `${linePrefix}$<ws>`),
|
|
);
|
|
})
|
|
.join('\n');
|
|
// oxlint-disable-next-line renovate/logger-static-message -- the multi-line, colorized diff must be interpolated to stay readable; as a metadata field it is JSON-escaped onto one line
|
|
logger.warn(`Config migration diff:\n${msg}`);
|
|
if (strict) {
|
|
returnVal = 1;
|
|
}
|
|
}
|
|
const massagedConfig = massageConfig(migratedConfig);
|
|
const res = await validateConfig(configType, massagedConfig, isPreset);
|
|
if (res.errors.length) {
|
|
logger.error(
|
|
{ file: desc, errors: res.errors },
|
|
'Found errors in configuration',
|
|
);
|
|
returnVal = 1;
|
|
}
|
|
if (res.warnings.length) {
|
|
logger.warn(
|
|
{ file: desc, warnings: res.warnings },
|
|
'Found errors in configuration',
|
|
);
|
|
returnVal = 1;
|
|
}
|
|
}
|
|
|
|
interface PackageJson {
|
|
renovate?: RenovateConfig;
|
|
'renovate-config'?: Record<string, RenovateConfig>;
|
|
}
|
|
|
|
(async () => {
|
|
await partiallyGlobalInitialize();
|
|
|
|
const program = new Command('renovate-config-validator')
|
|
.summary('Validate Renovate configuration files')
|
|
.description(
|
|
`Validate your Renovate configuration (repo config, shared presets or global configuration) files\n` +
|
|
// oxlint-disable-next-line renovate/no-hardcoded-docs-url -- CLI help text, no config available
|
|
'If no [config-files...] are given, renovate-config-validator will look at the default config file locations (https://docs.renovatebot.com/configuration-options/)',
|
|
)
|
|
.addHelpText(
|
|
'after',
|
|
// oxlint-disable-next-line renovate/no-hardcoded-docs-url -- CLI help text, no config available
|
|
`
|
|
When specifying [config-files...], Renovate will treat them as global self-hosted configuration files. You can disable this behaviour with --no-global
|
|
|
|
Examples:
|
|
|
|
$ renovate-config-validator
|
|
$ renovate-config-validator --strict
|
|
$ renovate-config-validator first_config.json
|
|
$ renovate-config-validator --strict config.js
|
|
$ renovate-config-validator --no-global renovate.json5
|
|
$ env RENOVATE_CONFIG_FILE=obscure-name.json renovate-config-validator
|
|
|
|
Global configuration:
|
|
|
|
If you have specified global self-hosted configuration (https://docs.renovatebot.com/self-hosted-configuration/) in environment variables or in a \`config.js\`, this will be detected:
|
|
|
|
$ env RENOVATE_ALLOWED_ENV='["GO*"]' renovate-config-validator
|
|
# if passing the filename, make sure it's not validating as a global config
|
|
$ env RENOVATE_ALLOWED_ENV='["GO*"]' renovate-config-validator --no-global renovate.json`,
|
|
)
|
|
.argument('[config-files...]')
|
|
.version(pkg.version, '-v, --version')
|
|
.option(
|
|
'--strict',
|
|
'Fail command if any configuration warnings, errors, or a migration is needed',
|
|
)
|
|
.option(
|
|
'--no-global',
|
|
'When specifying [config-files], do not treat them as global self-hosted configuration file(s)',
|
|
true,
|
|
)
|
|
// allow us to manage the exit code
|
|
.exitOverride();
|
|
|
|
program.action(async (files, opts) => {
|
|
const strict = opts.strict ?? false;
|
|
let filesValidated = 0;
|
|
|
|
// without RE2, patterns are checked by RegExp, which accepts syntax RE2 rejects (e.g. lookahead)
|
|
if (regexEngineStatus.type === 'unavailable') {
|
|
logger.warn(
|
|
{ err: regexEngineStatus.err },
|
|
'RE2 not usable, falling back to RegExp: regex validation may be inaccurate',
|
|
);
|
|
} else if (regexEngineStatus.type === 'ignored') {
|
|
logger.debug(
|
|
'RE2 ignored via RENOVATE_X_IGNORE_RE2: regex validation may be inaccurate',
|
|
);
|
|
}
|
|
|
|
if (files.length) {
|
|
let isGlobalConfig = true;
|
|
if (opts.global === false) {
|
|
isGlobalConfig = false;
|
|
}
|
|
const configType = isGlobalConfig ? 'global' : 'repo';
|
|
for (const file of files) {
|
|
try {
|
|
if (!(await pathExists(file))) {
|
|
returnVal = 1;
|
|
logger.error({ file }, 'File does not exist');
|
|
break;
|
|
}
|
|
const parsedContent = await getParsedContent(file);
|
|
try {
|
|
logger.info(`Validating ${file} as ${configType} config`);
|
|
await validate(configType, file, parsedContent, strict);
|
|
} catch (err) {
|
|
logger.warn({ file, err }, 'File is not valid Renovate config');
|
|
returnVal = 1;
|
|
}
|
|
} catch (err) {
|
|
logger.warn({ file, err }, 'File could not be parsed');
|
|
returnVal = 1;
|
|
}
|
|
|
|
filesValidated++;
|
|
}
|
|
} else {
|
|
for (const file of getConfigFileNames().filter(
|
|
(name) => name !== 'package.json',
|
|
)) {
|
|
try {
|
|
if (!(await pathExists(file))) {
|
|
continue;
|
|
}
|
|
const parsedContent = await getParsedContent(file);
|
|
try {
|
|
logger.info(`Validating ${file}`);
|
|
await validate('repo', file, parsedContent, strict);
|
|
} catch (err) {
|
|
logger.warn({ file, err }, 'File is not valid Renovate config');
|
|
returnVal = 1;
|
|
}
|
|
} catch (err) {
|
|
logger.warn({ file, err }, 'File could not be parsed');
|
|
returnVal = 1;
|
|
}
|
|
|
|
filesValidated++;
|
|
}
|
|
try {
|
|
const pkgJson = JSON.parse(
|
|
await readFile('package.json', 'utf8'),
|
|
) as PackageJson;
|
|
if (pkgJson.renovate) {
|
|
logger.info(`Validating package.json > renovate`);
|
|
await validate(
|
|
'repo',
|
|
'package.json > renovate',
|
|
pkgJson.renovate,
|
|
strict,
|
|
);
|
|
|
|
filesValidated++;
|
|
}
|
|
if (pkgJson['renovate-config']) {
|
|
logger.info(`Validating package.json > renovate-config`);
|
|
for (const presetConfig of Object.values(
|
|
pkgJson['renovate-config'],
|
|
)) {
|
|
await validate(
|
|
'repo',
|
|
'package.json > renovate-config',
|
|
presetConfig,
|
|
strict,
|
|
true,
|
|
);
|
|
|
|
filesValidated++;
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
const env = getEnv();
|
|
const fileConfig = await getFileConfig(env);
|
|
if (!dequal(fileConfig, {})) {
|
|
const file = env.RENOVATE_CONFIG_FILE ?? 'config.js';
|
|
logger.info(`Validating ${file}`);
|
|
try {
|
|
await validate('global', file, fileConfig, strict);
|
|
} catch (err) {
|
|
logger.error({ file, err }, 'File is not valid Renovate config');
|
|
returnVal = 1;
|
|
}
|
|
filesValidated++;
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
if (returnVal === 0 && filesValidated) {
|
|
logger.info(
|
|
`Config validated successfully against ${filesValidated} file(s)`,
|
|
);
|
|
} else if (!filesValidated) {
|
|
logger.warn(`No files to perform configuration validation against`);
|
|
}
|
|
// Use exitCode (not process.exit) so async log streams can flush
|
|
process.exitCode = returnVal;
|
|
});
|
|
|
|
await program.parseAsync();
|
|
})().catch((e) => {
|
|
// Commander throws an error at the end of Action execution i.e. as part of the `help` and `version` commands, and so we don't want to return an error code in this case
|
|
if (
|
|
e instanceof CommanderError &&
|
|
(e.code === 'commander.helpDisplayed' || e.code === 'commander.version')
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// oxlint-disable-next-line no-console -- intentional: display critical error on CLI
|
|
console.error(e);
|
|
process.exit(99);
|
|
});
|