Files
Sebastian Poxhofer c41b46bac3 feat(logging): do not use dynamic fields in log messages with severity WARN or higher (#44560)
* 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>
2026-07-16 06:53:37 +00:00

293 lines
8.5 KiB
TypeScript

import { isArray } from '@sindresorhus/is';
import JSON5 from 'json5';
import { getEnvName } from '../../../../config/options/env.ts';
import { getOptions } from '../../../../config/options/index.ts';
import type { AllConfig } from '../../../../config/types.ts';
import { logger } from '../../../../logger/index.ts';
import { parseJson } from '../../../../util/common.ts';
import { coersions } from './coersions.ts';
import { migrateAndValidateConfig } from './util.ts';
function normalizePrefixes(
env: NodeJS.ProcessEnv,
prefix: string | undefined,
): NodeJS.ProcessEnv {
const result = { ...env };
if (prefix) {
for (const [key, val] of Object.entries(result)) {
if (key.startsWith(prefix)) {
const newKey = key.replace(prefix, 'RENOVATE_');
result[newKey] = val;
delete result[key];
}
}
}
return result;
}
const renameKeys = {
aliases: 'registryAliases',
azureAutoComplete: 'platformAutomerge', // migrate: azureAutoComplete
gitLabAutomerge: 'platformAutomerge', // migrate: gitLabAutomerge
mergeConfidenceApiBaseUrl: 'mergeConfidenceEndpoint',
mergeConfidenceSupportedDatasources: 'mergeConfidenceDatasources',
allowedPostUpgradeCommands: 'allowedCommands',
baseBranches: 'baseBranchPatterns',
};
function renameEnvKeys(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const result = { ...env };
for (const [from, to] of Object.entries(renameKeys)) {
const fromKey = getEnvName({ name: from });
const toKey = getEnvName({ name: to });
if (env[fromKey]) {
result[toKey] = env[fromKey];
delete result[fromKey];
}
}
return result;
}
const migratedKeysWithValues = [
{
oldName: 'recreateClosed',
newName: 'recreateWhen',
from: 'true',
to: 'always',
},
{
oldName: 'recreateClosed',
newName: 'recreateWhen',
from: 'false',
to: 'auto',
},
];
function massageEnvKeyValues(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const result = { ...env };
for (const { oldName, newName, from, to } of migratedKeysWithValues) {
const key = getEnvName({ name: oldName });
if (env[key] !== undefined && result[key] === from) {
delete result[key];
result[getEnvName({ name: newName })] = to;
}
}
return result;
}
interface ConvertedExperimentalEnvVar {
optionName: string;
// Normalize the raw env var value before passing it through, if needed.
normalizeValue?: (value: string) => string;
}
// Maps RENOVATE_X_ env vars that have been promoted to regular config options
// to the option name they now correspond to.
export const convertedExperimentalEnvVars: ReadonlyMap<
string,
ConvertedExperimentalEnvVar
> = new Map([
['RENOVATE_X_AUTODISCOVER_REPO_SORT', { optionName: 'autodiscoverRepoSort' }],
[
'RENOVATE_X_AUTODISCOVER_REPO_ORDER',
{ optionName: 'autodiscoverRepoOrder' },
],
['RENOVATE_X_DOCKER_MAX_PAGES', { optionName: 'dockerMaxPages' }],
['RENOVATE_X_DELETE_CONFIG_FILE', { optionName: 'deleteConfigFile' }],
['RENOVATE_X_S3_ENDPOINT', { optionName: 's3Endpoint' }],
['RENOVATE_X_S3_PATH_STYLE', { optionName: 's3PathStyle' }],
[
'RENOVATE_X_MERGE_CONFIDENCE_API_BASE_URL',
{ optionName: 'mergeConfidenceEndpoint' },
],
[
'RENOVATE_X_MERGE_CONFIDENCE_SUPPORTED_DATASOURCES',
{ optionName: 'mergeConfidenceDatasources' },
],
[
'RENOVATE_X_REPO_CACHE_FORCE_LOCAL',
{
optionName: 'repositoryCacheForceLocal',
// The old env var was treated as a flag: any non-empty value meant true.
normalizeValue: (v: string) => (v ? 'true' : v),
},
],
]);
/**
* Massages the experimental env vars which have been converted to config options
*
* e.g. RENOVATE_X_AUTODISCOVER_REPO_SORT -> RENOVATE_AUTODISCOVER_REPO_SORT
*/
function massageConvertedExperimentalVars(
env: NodeJS.ProcessEnv,
): NodeJS.ProcessEnv {
const result = { ...env };
for (const [
oldKey,
{ optionName, normalizeValue },
] of convertedExperimentalEnvVars) {
if (env[oldKey] !== undefined) {
const newKey = getEnvName({ name: optionName });
result[newKey] = normalizeValue
? normalizeValue(env[oldKey])
: env[oldKey];
delete result[oldKey];
}
}
return result;
}
export async function getConfig(
inputEnv: NodeJS.ProcessEnv,
configEnvKey = 'RENOVATE_CONFIG',
): Promise<AllConfig> {
const env = prepareEnv(inputEnv);
const config = await parseAndValidateOrExit(env, configEnvKey);
const options = getOptions();
config.hostRules ??= [];
for (const option of options) {
if (option.env === false) {
continue;
}
const envName = getEnvName(option);
const envVal = env[envName];
if (!envVal) {
continue;
}
if (option.type === 'array' && option.subType === 'object') {
try {
const parsed = JSON5.parse(envVal);
if (isArray(parsed)) {
// @ts-expect-error -- type can't be narrowed
config[option.name] = parsed;
} else {
logger.debug(
{ val: envVal, envName },
'Could not parse object array',
);
}
} catch {
logger.debug(
{ val: envVal, envName },
'Could not parse environment variable',
);
}
} else {
const coerce = coersions[option.type];
try {
// @ts-expect-error -- type can't be narrowed
config[option.name] = coerce(envVal);
} catch (e) {
throw new Error(`${envName} was invalid: ${e}`);
}
if (option.name === 'dryRun') {
if ((config[option.name] as string) === 'true') {
logger.warn('env config dryRun property has been changed to full');
config[option.name] = 'full';
} else if ((config[option.name] as string) === 'false') {
logger.warn('env config dryRun property has been changed to null');
delete config[option.name];
} else if ((config[option.name] as string) === 'null') {
delete config[option.name];
}
}
if (option.name === 'requireConfig') {
if ((config[option.name] as string) === 'true') {
logger.warn(
'env config requireConfig property has been changed to required',
);
config[option.name] = 'required';
} else if ((config[option.name] as string) === 'false') {
logger.warn(
'env config requireConfig property has been changed to optional',
);
config[option.name] = 'optional';
}
}
if (option.name === 'platformCommit') {
if ((config[option.name] as string) === 'true') {
logger.warn(
'env config platformCommit property has been changed to enabled',
);
config[option.name] = 'enabled';
} else if ((config[option.name] as string) === 'false') {
logger.warn(
'env config platformCommit property has been changed to disabled',
);
config[option.name] = 'disabled';
}
}
}
}
const githubComToken = env.GITHUB_COM_TOKEN ?? env.RENOVATE_GITHUB_COM_TOKEN;
if (githubComToken) {
logger.debug(`Converting GITHUB_COM_TOKEN into a global host rule`);
config.hostRules.push({
hostType: 'github',
matchHost: 'github.com',
token: githubComToken,
});
}
// These env vars are deprecated and deleted to make sure they're not used
const unsupportedEnv = [
'BITBUCKET_TOKEN',
'BITBUCKET_USERNAME',
'BITBUCKET_PASSWORD',
'GITHUB_ENDPOINT',
'GITHUB_TOKEN',
'GITLAB_ENDPOINT',
'GITLAB_TOKEN',
'VSTS_ENDPOINT',
'VSTS_TOKEN',
];
for (const val of unsupportedEnv) {
delete env[val];
}
return config;
}
export function prepareEnv(inputEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
let env = normalizePrefixes(inputEnv, inputEnv.ENV_PREFIX);
env = massageConvertedExperimentalVars(env);
env = renameEnvKeys(env);
// massage the values of migrated configuration keys
return massageEnvKeyValues(env);
}
export async function parseAndValidateOrExit(
env: NodeJS.ProcessEnv,
configEnvKey: string,
): Promise<AllConfig> {
if (!env[configEnvKey]) {
return {};
}
try {
const config = parseJson(
env[configEnvKey],
`${configEnvKey}.env.json5`,
) as AllConfig;
logger.debug({ config }, `Detected config in env ${configEnvKey}`);
return await migrateAndValidateConfig(config, `${configEnvKey}`);
} catch (err) {
logger.fatal(
{ err, configEnvKey },
'Could not parse config environment variable',
);
process.exit(1);
}
}