mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 07:50:47 +00:00
8dc06640a7
As part of follow-up changes in #45706, we want the `apk` / `apt` parsing in the `dockerfile` manager to attempt to determine which distribution - and therefore registry - to look up dependencies for. When there is an image that we don't know how to resolve, we want to treat them as a `skipReason: unknown-registry`. However, this doesn't provide any way for the user to say "I know which registry you should look up updates for". To handle this, we can treat `unknown-registry` differently, allowing it to un-skip itself when there are any `registryUrls`s defined. This allows for general usage, where if Renovate doesn't know how to interact with a given registry, but a user does, then the user can override the registry with a `packageRules` entry. We continue skipping a dependency when `unknown-registry` is set alongside a dependency's `registryUrl`. We add this un-skipping logic into `applyPackageRules()`, but need to make sure that we also gate it inside `lookup()`, so a dependency that's passed through `applyPackageRules()` is re-checked to ensure that the check is enforced. Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
import {
|
|
isNonEmptyArray,
|
|
isNullOrUndefined,
|
|
isString,
|
|
isTruthy,
|
|
} from '@sindresorhus/is';
|
|
import _slugify from 'slugify';
|
|
import type {
|
|
PackageRule,
|
|
PackageRuleInputConfig,
|
|
} from '../../config/types.ts';
|
|
import { mergeChildConfig } from '../../config/utils.ts';
|
|
import { logger } from '../../logger/index.ts';
|
|
import type { StageName } from '../../types/skip-reason.ts';
|
|
import { coerceArray } from '../array.ts';
|
|
import { compile } from '../template/index.ts';
|
|
import matchers from './matchers.ts';
|
|
|
|
const slugify = _slugify;
|
|
|
|
async function matchesRule(
|
|
inputConfig: PackageRuleInputConfig,
|
|
packageRule: PackageRule,
|
|
): Promise<boolean> {
|
|
for (const matcher of matchers) {
|
|
const isMatch = await matcher.matches(inputConfig, packageRule);
|
|
|
|
// no rules are defined
|
|
if (isNullOrUndefined(isMatch)) {
|
|
continue;
|
|
}
|
|
|
|
if (!isTruthy(isMatch)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export async function applyPackageRules<T extends PackageRuleInputConfig>(
|
|
inputConfig: T,
|
|
stageName?: StageName,
|
|
): Promise<T> {
|
|
let config = { ...inputConfig };
|
|
const packageRules = coerceArray(config.packageRules);
|
|
// The `packageRules` array is invariant while rules are being applied, and
|
|
// can be very large (e.g. vulnerability alerts append rules embedding full
|
|
// advisory texts). Remove it from the working config so mergeChildConfig()
|
|
// does not deep-clone the whole array once per matched rule, then restore
|
|
// it afterwards.
|
|
delete config.packageRules;
|
|
logger.trace(
|
|
{ dependency: config.depName, packageRules },
|
|
`Checking against ${packageRules.length} packageRules`,
|
|
);
|
|
for (const packageRule of packageRules) {
|
|
// This rule is considered matched if there was at least one positive match and no negative matches
|
|
if (await matchesRule(config, packageRule)) {
|
|
// Package rule config overrides any existing config
|
|
const toApply = removeMatchers({ ...packageRule });
|
|
if (config.groupSlug && packageRule.groupName && !packageRule.groupSlug) {
|
|
// Need to apply groupSlug otherwise the existing one will take precedence
|
|
toApply.groupSlug = slugify(packageRule.groupName, {
|
|
lower: true,
|
|
});
|
|
}
|
|
|
|
if (
|
|
// if it's got higher precedence, as it's a force'd config option
|
|
// multiple force'd config options are "last defined wins"
|
|
toApply.force?.enabled === false ||
|
|
// otherwise, if it has regular precedence, compare
|
|
(toApply.enabled === false && config.enabled !== false)
|
|
) {
|
|
config.skipReason = 'package-rules';
|
|
if (stageName) {
|
|
config.skipStage = stageName;
|
|
}
|
|
}
|
|
|
|
if (
|
|
(toApply.force?.enabled || toApply.enabled) &&
|
|
// `unknown-registry` is not about whether the user wants the dependency
|
|
// updated, but about Renovate having nowhere to look it up, so a
|
|
// registry clears it rather than `enabled` - see below
|
|
config.skipReason !== 'unknown-registry'
|
|
) {
|
|
delete config.skipReason;
|
|
delete config.skipStage;
|
|
}
|
|
if (
|
|
isString(toApply.overrideDatasource) &&
|
|
toApply.overrideDatasource !== config.datasource
|
|
) {
|
|
logger.debug(
|
|
`Overriding datasource from ${config.datasource} to ${toApply.overrideDatasource} for ${config.depName}`,
|
|
);
|
|
config.datasource = toApply.overrideDatasource;
|
|
}
|
|
if (
|
|
isString(toApply.overrideDepName) &&
|
|
toApply.overrideDepName !== config.depName
|
|
) {
|
|
logger.debug(
|
|
`Overriding depName from ${config.depName} to ${toApply.overrideDepName}`,
|
|
);
|
|
config.depName = compile(toApply.overrideDepName, config);
|
|
}
|
|
if (
|
|
isString(toApply.overridePackageName) &&
|
|
toApply.overridePackageName !== config.packageName
|
|
) {
|
|
logger.debug(
|
|
`Overriding packageName from ${config.packageName} to ${toApply.overridePackageName} for ${config.depName}`,
|
|
);
|
|
config.packageName = compile(toApply.overridePackageName, config);
|
|
}
|
|
if (isString(toApply.sourceUrl)) {
|
|
toApply.sourceUrl = compile(toApply.sourceUrl, config);
|
|
}
|
|
delete toApply.overrideDatasource;
|
|
delete toApply.overrideDepName;
|
|
delete toApply.overridePackageName;
|
|
config = mergeChildConfig(config, toApply);
|
|
}
|
|
}
|
|
// A manager which could not work out where to look leaves `unknown-registry`
|
|
// behind. Config can still answer that, from a rule applied above or from the
|
|
// repository config the rules were applied to, so the dependency is no longer
|
|
// skipped once it has a registry to be looked up in.
|
|
if (
|
|
config.skipReason === 'unknown-registry' &&
|
|
(isNonEmptyArray(config.registryUrls) ||
|
|
isNonEmptyArray(config.defaultRegistryUrls))
|
|
) {
|
|
delete config.skipReason;
|
|
delete config.skipStage;
|
|
}
|
|
|
|
// Restore the rules. If any applied rule carried nested `packageRules`
|
|
// (e.g. from a resolved preset), preserve the concat-merge semantics that
|
|
// mergeChildConfig() would previously have applied.
|
|
if (config.packageRules) {
|
|
config.packageRules = packageRules.concat(config.packageRules);
|
|
} else if ('packageRules' in inputConfig) {
|
|
config.packageRules = inputConfig.packageRules;
|
|
}
|
|
return config;
|
|
}
|
|
|
|
function removeMatchers<T extends Record<string, unknown>>(
|
|
packageRule: T,
|
|
): Record<string, unknown> & {
|
|
force?: Record<string, unknown>;
|
|
} {
|
|
for (const key of Object.keys(packageRule)) {
|
|
if (key.startsWith('match') || key.startsWith('exclude')) {
|
|
delete packageRule[key];
|
|
}
|
|
}
|
|
|
|
return packageRule;
|
|
}
|