mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-24 14:34:21 +00:00
c41b46bac3
* ci(linter): add renovate/logger-static-message rule Adds a custom oxlint rule that enforces a static msg component for logger.warn/error/fatal calls (including logger.once variants) so the messages can be grouped in metrics and error catching services, per docs/development/best-practices.md. It also flags metadata properties named 'error' that hold error-ish values, because the bunyan serializer only handles the 'err' key. The rule is enabled as an error for lib/ production code and disabled for spec files. All 33 pre-existing violations are fixed by moving interpolated values into the metadata object, making messages static, and renaming 'error' metadata keys to 'err'. Affected log-message assertions in co-located specs are updated accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: ignore validator output * fixup new merged in log messages --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
146 lines
4.0 KiB
TypeScript
146 lines
4.0 KiB
TypeScript
import { isArray, isPlainObject, isString } from '@sindresorhus/is';
|
|
import type { RenovateConfig } from '../config/types.ts';
|
|
import {
|
|
CONFIG_SECRETS_INVALID,
|
|
CONFIG_VALIDATION,
|
|
CONFIG_VARIABLES_INVALID,
|
|
} from '../constants/error-messages.ts';
|
|
import { logger } from '../logger/index.ts';
|
|
import { capitalize } from './string.ts';
|
|
|
|
export interface InterpolatorOptions {
|
|
name: 'secrets' | 'variables';
|
|
templateRegex: RegExp;
|
|
nameRegex: RegExp;
|
|
}
|
|
|
|
export function validateInterpolatedValues(
|
|
input: unknown,
|
|
options: InterpolatorOptions,
|
|
): void {
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
const { name, nameRegex } = options;
|
|
|
|
const validationErrors: string[] = [];
|
|
if (isPlainObject(input)) {
|
|
for (const [key, value] of Object.entries(input)) {
|
|
if (!nameRegex.test(key)) {
|
|
validationErrors.push(`Invalid ${name} name "${key}"`);
|
|
}
|
|
if (!isString(value)) {
|
|
validationErrors.push(
|
|
`${capitalize(name)} values must be strings. Found type ${typeof value} for ${name} ${key}`,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
validationErrors.push(
|
|
`Config ${name}s must be a plain object. Found: ${typeof input}`,
|
|
);
|
|
}
|
|
|
|
if (validationErrors.length) {
|
|
logger.error(
|
|
{ name, validationErrors },
|
|
'Invalid interpolated values configured',
|
|
);
|
|
throw new Error(
|
|
name === 'secrets' ? CONFIG_SECRETS_INVALID : CONFIG_VARIABLES_INVALID,
|
|
);
|
|
}
|
|
}
|
|
|
|
function replaceInterpolatedValuesInString(
|
|
key: string,
|
|
value: string,
|
|
input: Record<string, string>,
|
|
options: InterpolatorOptions,
|
|
): string {
|
|
const { name, templateRegex } = options;
|
|
// Reset regex lastIndex for global regexes to ensure consistent behavior
|
|
templateRegex.lastIndex = 0;
|
|
|
|
// do nothing if no interpolator template found
|
|
if (!templateRegex.test(value)) {
|
|
return value;
|
|
}
|
|
|
|
const disallowedPrefixes = ['branch', 'commit', 'group', 'pr', 'semantic'];
|
|
if (disallowedPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
const error = new Error(CONFIG_VALIDATION);
|
|
error.validationSource = 'config';
|
|
error.validationError = `Disallowed ${name} substitution`;
|
|
error.validationMessage = `The field \`${key}\` may not use ${name} substitution`;
|
|
throw error;
|
|
}
|
|
return value.replace(templateRegex, (_, key) => {
|
|
if (input?.[key]) {
|
|
return input[key];
|
|
}
|
|
const error = new Error(CONFIG_VALIDATION);
|
|
error.validationSource = 'config';
|
|
error.validationError = `Unknown ${name} name`;
|
|
error.validationMessage = `The following ${name} name was not found in config: ${String(
|
|
key,
|
|
)}`;
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
export function replaceInterpolatedValuesInObject(
|
|
config_: RenovateConfig,
|
|
input: Record<string, string>,
|
|
options: InterpolatorOptions,
|
|
deleteValues = true,
|
|
): RenovateConfig {
|
|
const config = { ...config_ };
|
|
const { name } = options;
|
|
if (deleteValues) {
|
|
delete config[name];
|
|
}
|
|
for (const [key, value] of Object.entries(config)) {
|
|
if (isPlainObject(value) && key !== 'onboardingConfig') {
|
|
// @ts-expect-error -- type can't be narrowed
|
|
config[key] = replaceInterpolatedValuesInObject(
|
|
value,
|
|
input,
|
|
options,
|
|
deleteValues,
|
|
);
|
|
}
|
|
if (isString(value)) {
|
|
// @ts-expect-error -- type can't be narrowed
|
|
config[key] = replaceInterpolatedValuesInString(
|
|
key,
|
|
value,
|
|
input,
|
|
options,
|
|
);
|
|
}
|
|
if (isArray(value)) {
|
|
for (const [arrayIndex, arrayItem] of value.entries()) {
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (isPlainObject(arrayItem)) {
|
|
value[arrayIndex] = replaceInterpolatedValuesInObject(
|
|
arrayItem,
|
|
input,
|
|
options,
|
|
deleteValues,
|
|
);
|
|
} else if (isString(arrayItem)) {
|
|
value[arrayIndex] = replaceInterpolatedValuesInString(
|
|
key,
|
|
arrayItem,
|
|
input,
|
|
options,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return config;
|
|
}
|