mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 10:09:50 +00:00
b22f58045e
* fix(manager): handle registryAliases with shell variable defaults
Allows `registryAliases` keys to contain shell-style variables with
empty (or other) defaults, e.g. `${DEPENDENCY_PROXY:-}` or
`${CI_REGISTRY:-}`. The same image string is then resolved both
online (variable expanded) and offline (variable empty).
Changes in `dockerfile/extract.ts` (used by `gitlabci` too):
* Match the alias key as either `${name}/...` (existing) or `${name}...`
when `name` already ends in a non-identifier boundary character such
as `}`. Bare-identifier keys like `$CI_REGISTRY` continue to require
the slash boundary so they no longer overmatch keys that begin with
the same prefix (e.g. `$CI_REGISTRY` vs `$CI_REGISTRY_IMAGE`).
* Use `ensureTrailingSlash` to handle alias values that already include
a trailing slash.
* Preserve the original `name` and path structure in `dep.depName`
instead of substringing only after the alias `value`. This keeps the
variable in the depName so it survives a `splitImageParts` round-trip
and the resulting branch name comes out clean.
Adds `sanitizeDepName` regex passes in
`lib/workers/repository/updates/flatten.ts` so any leftover
`${VAR}`/`${VAR:-...}` placeholder is stripped from the depName before
it becomes a branch name (otherwise we'd get
`renovate/${dependency_proxy}python-3.x` instead of
`renovate/python-3.x`).
Tests cover:
* `${VAR:-}/image` (with slash) and `${VAR:-}image` (no slash, requires
the boundary form).
* Overlapping prefixes: `$CI_REGISTRY` vs `$CI_REGISTRY_IMAGE`.
* Registry alias with port (`localhost:5000/repo`).
* `sanitizeDepName` strips `${VAR}` / `${VAR:-}` / bare `$VAR`.
Reproducer: https://github.com/Wayneoween/renovate-36293-repro
* revert(manager/gitlabci): keep regex group name as 'prefix'
Reverting the unrelated `prefix` -> `rawPrefix` rename in `depProxyRe`.
Per review on #36293, this rename is out of scope for the registry-alias
fix and just adds review surface.
* refactor(manager/dockerfile): narrow alias boundary check to '}'
Per review on #36293. The previous regex `/[^A-Za-z0-9_]$/` matched any
non-word ending, which is broader than the documented case (alias keys
ending in `}` from `${VAR}` / `${VAR:-...}`). Replace with a plain
`name.endsWith('}')` check, which also avoids re-building a regex on
every iteration of the alias loop.
Existing tests (`'variables with default and slash'`,
`'variables with default no slash'`, `'alias with digest only'`)
continue to exercise the `}`-boundary path; the `'overlapping prefixes'`
case continues to exercise the slash-only path.
* chore(manager/dockerfile): trim comment and TODO library/ strip edge
Per review on #36293:
* Drop the verbose explanation block above the depName-restore branch;
the spec rows already document the expected shape.
* Add a TODO above the `dep.depName?.startsWith(valueWithSlash)` guard
noting a pre-existing edge case: when the inner `getDep` strips a
`library/` prefix (or similar) the depName no longer starts with
`valueWithSlash`, so the alias-rooted depName is not restored. Out of
scope for this PR.
* fix(manager/dockerfile): handle empty registry alias value
- strip trailing slash after ${VAR} placeholders in sanitizeDepName
- treat an empty registry alias value as Docker Hub instead of '/'
- clarify registry alias docs wording
* refactor(manager/dockerfile): drop redundant alias boundary guard
- `matchedAtBoundary` is only read behind `!matchedWithSlash`
- add docker-compose test for `${VAR:-}` registry aliases
* test(manager/dockerfile): pin special-prefix registry alias behavior
`getDep` strips `library/`, `amd64/` and `arm64/` as special prefixes, so
an alias whose value is one of those loses its alias-rooted `depName`.
Both the slash form and the variable form behave this way on `main`.
264 lines
9.8 KiB
TypeScript
264 lines
9.8 KiB
TypeScript
import { isUndefined } from '@sindresorhus/is';
|
|
import {
|
|
filterConfig,
|
|
getManagerConfig,
|
|
mergeChildConfig,
|
|
} from '../../../config/index.ts';
|
|
import type { RenovateConfig } from '../../../config/types.ts';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import { getDefaultConfig } from '../../../modules/datasource/index.ts';
|
|
import { get } from '../../../modules/manager/index.ts';
|
|
import type { PackageFile } from '../../../modules/manager/types.ts';
|
|
import { coerceArray } from '../../../util/array.ts';
|
|
import { detectSemanticCommits } from '../../../util/git/semantic.ts';
|
|
import { applyPackageRules } from '../../../util/package-rules/index.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import * as template from '../../../util/template/index.ts';
|
|
import { parseUrl } from '../../../util/url.ts';
|
|
import type { BranchUpgradeConfig } from '../../types.ts';
|
|
import { replacementAlreadyExists } from '../common.ts';
|
|
import { generateBranchName } from './branch-name.ts';
|
|
|
|
function upper(str: string): string {
|
|
return str.charAt(0).toUpperCase() + str.substring(1);
|
|
}
|
|
|
|
export function sanitizeDepName(depName: string): string {
|
|
return depName
|
|
.replace(regEx(/\$\{[^}]+\}\/?/g), '')
|
|
.replace(regEx(/[${}]/g), '')
|
|
.replace('@types/', '')
|
|
.replace('@', '')
|
|
.replace(regEx(/\//g), '-')
|
|
.replace(regEx(/\s+/g), '-')
|
|
.replace(regEx(/:/g), '-')
|
|
.replace(regEx(/-+/), '-')
|
|
.toLowerCase();
|
|
}
|
|
|
|
export function applyUpdateConfig(input: BranchUpgradeConfig): any {
|
|
const updateConfig = { ...input };
|
|
delete updateConfig.packageRules;
|
|
// TODO: Remove next line once #8075 is complete
|
|
updateConfig.depNameSanitized = updateConfig.depName
|
|
? sanitizeDepName(updateConfig.depName)
|
|
: undefined;
|
|
updateConfig.newNameSanitized = updateConfig.newName
|
|
? sanitizeDepName(updateConfig.newName)
|
|
: undefined;
|
|
if (updateConfig.sourceUrl) {
|
|
const parsedSourceUrl = parseUrl(updateConfig.sourceUrl);
|
|
if (parsedSourceUrl?.pathname) {
|
|
updateConfig.sourceRepoSlug = parsedSourceUrl.pathname
|
|
.replace(regEx(/^\//), '') // remove leading slash
|
|
.replace(regEx(/\//g), '-') // change slashes to hyphens
|
|
.replace(regEx(/-+/g), '-'); // remove multiple hyphens
|
|
updateConfig.sourceRepo = parsedSourceUrl.pathname.replace(
|
|
regEx(/^\//),
|
|
'',
|
|
); // remove leading slash
|
|
updateConfig.sourceRepoOrg = updateConfig.sourceRepo.replace(
|
|
regEx(/\/.*/g),
|
|
'',
|
|
); // remove everything after first slash
|
|
updateConfig.sourceRepoName = updateConfig.sourceRepo.replace(
|
|
regEx(/.*\//g),
|
|
'',
|
|
); // remove everything up to the last slash
|
|
}
|
|
}
|
|
if (updateConfig.sourceDirectory) {
|
|
updateConfig.sourceDirectory = template.compile(
|
|
updateConfig.sourceDirectory,
|
|
updateConfig,
|
|
);
|
|
}
|
|
generateBranchName(updateConfig);
|
|
return updateConfig;
|
|
}
|
|
|
|
export async function flattenUpdates(
|
|
config: RenovateConfig,
|
|
packageFiles: Record<string, PackageFile[]>,
|
|
): Promise<BranchUpgradeConfig[]> {
|
|
const updates = [];
|
|
const updateTypes = [
|
|
'major',
|
|
'minor',
|
|
'patch',
|
|
'pin',
|
|
'digest',
|
|
'lockFileMaintenance',
|
|
'replacement',
|
|
];
|
|
for (const [manager, files] of Object.entries(packageFiles)) {
|
|
const managerConfig = getManagerConfig(config, manager);
|
|
for (const packageFile of files) {
|
|
const packageFileConfig: BranchUpgradeConfig = mergeChildConfig(
|
|
managerConfig,
|
|
packageFile,
|
|
) as never;
|
|
const packagePath = packageFile.packageFile?.split('/');
|
|
if (packagePath.length > 0) {
|
|
packagePath.splice(-1, 1);
|
|
}
|
|
if (packagePath.length > 0) {
|
|
packageFileConfig.parentDir = packagePath.at(-1);
|
|
packageFileConfig.packageFileDir = packagePath.join('/');
|
|
} else {
|
|
packageFileConfig.parentDir = '';
|
|
packageFileConfig.packageFileDir = '';
|
|
}
|
|
let depIndex = 0;
|
|
for (const dep of packageFile.deps) {
|
|
if (dep.updates!.length) {
|
|
const depConfig = mergeChildConfig(packageFileConfig, dep);
|
|
// @ts-expect-error -- not easily typed
|
|
delete depConfig.deps;
|
|
depConfig.depIndex = depIndex; // used for autoreplace
|
|
for (const update of dep.updates!) {
|
|
if (
|
|
update.updateType === 'replacement' &&
|
|
update.newName &&
|
|
replacementAlreadyExists(packageFile.deps, dep, update.newName)
|
|
) {
|
|
logger.debug(
|
|
{ fileName: packageFile.packageFile },
|
|
`Skipping replacement of ${dep.depName} with ${update.newName}: replacement already exists in ${packageFile.packageFile}`,
|
|
);
|
|
continue;
|
|
}
|
|
let updateConfig = mergeChildConfig(depConfig, update);
|
|
delete updateConfig.updates;
|
|
if (updateConfig.updateType) {
|
|
// @ts-expect-error -- not easily typed
|
|
updateConfig[`is${upper(updateConfig.updateType)}`] = true;
|
|
}
|
|
if (updateConfig.updateTypes) {
|
|
updateConfig.updateTypes.forEach((updateType: string) => {
|
|
// @ts-expect-error -- not easily typed
|
|
updateConfig[`is${upper(updateType)}`] = true;
|
|
});
|
|
}
|
|
// apply config from datasource
|
|
const datasourceConfig = await getDefaultConfig(
|
|
depConfig.datasource!,
|
|
);
|
|
updateConfig = mergeChildConfig(updateConfig, datasourceConfig);
|
|
updateConfig = await applyPackageRules(
|
|
updateConfig,
|
|
'datasource-merge',
|
|
);
|
|
// apply major/minor/patch/pin/digest
|
|
updateConfig = mergeChildConfig(
|
|
updateConfig,
|
|
// @ts-expect-error -- not easily typed
|
|
updateConfig[updateConfig.updateType],
|
|
);
|
|
for (const updateType of updateTypes) {
|
|
// @ts-expect-error -- not easily typed
|
|
delete updateConfig[updateType];
|
|
}
|
|
// Apply again in case any were added by the updateType config
|
|
updateConfig = await applyPackageRules(
|
|
updateConfig,
|
|
'update-type-merge',
|
|
);
|
|
updateConfig = applyUpdateConfig(updateConfig);
|
|
updateConfig.baseDeps = packageFile.deps;
|
|
update.branchName = updateConfig.branchName;
|
|
|
|
// make sure that we use the dependency's current state of attestation, rather than using the new update's value
|
|
updateConfig.hasAttestation = depConfig.hasAttestation;
|
|
|
|
updates.push(updateConfig);
|
|
}
|
|
}
|
|
depIndex += 1;
|
|
}
|
|
if (
|
|
get(manager, 'supportsLockFileMaintenance') &&
|
|
packageFileConfig.lockFileMaintenance!.enabled
|
|
) {
|
|
// Apply lockFileMaintenance config before packageRules
|
|
let lockFileConfig = mergeChildConfig(
|
|
packageFileConfig,
|
|
packageFileConfig.lockFileMaintenance!,
|
|
);
|
|
lockFileConfig.updateType = 'lockFileMaintenance';
|
|
lockFileConfig.isLockFileMaintenance = true;
|
|
lockFileConfig = await applyPackageRules(
|
|
lockFileConfig,
|
|
'lock-file-maintenance-merge',
|
|
);
|
|
// Apply lockFileMaintenance and packageRules again
|
|
lockFileConfig = mergeChildConfig(
|
|
lockFileConfig,
|
|
lockFileConfig.lockFileMaintenance!,
|
|
);
|
|
lockFileConfig = await applyPackageRules(
|
|
lockFileConfig,
|
|
'lock-file-maintenance-merge-2',
|
|
);
|
|
// Remove unnecessary objects
|
|
for (const updateType of updateTypes) {
|
|
// @ts-expect-error -- not easily typed
|
|
delete lockFileConfig[updateType];
|
|
}
|
|
delete lockFileConfig.packageRules;
|
|
// @ts-expect-error -- not easily typed
|
|
delete lockFileConfig.deps;
|
|
generateBranchName(lockFileConfig);
|
|
updates.push(lockFileConfig);
|
|
}
|
|
if (get(manager, 'updateLockedDependency')) {
|
|
for (const lockFile of coerceArray(packageFileConfig.lockFiles)) {
|
|
const lockfileRemediations = config.remediations as Record<
|
|
string,
|
|
Record<string, any>[]
|
|
>;
|
|
const remediations = lockfileRemediations?.[lockFile];
|
|
if (remediations) {
|
|
for (const remediation of remediations) {
|
|
let updateConfig = mergeChildConfig(
|
|
packageFileConfig,
|
|
remediation,
|
|
);
|
|
updateConfig = mergeChildConfig(
|
|
updateConfig,
|
|
config.vulnerabilityAlerts,
|
|
);
|
|
delete updateConfig.vulnerabilityAlerts;
|
|
updateConfig.isVulnerabilityAlert = true;
|
|
updateConfig.isRemediation = true;
|
|
updateConfig.lockFile = lockFile;
|
|
updateConfig.currentValue = updateConfig.currentVersion;
|
|
updateConfig.newValue = updateConfig.newVersion;
|
|
updateConfig = applyUpdateConfig(updateConfig);
|
|
updateConfig.enabled = true;
|
|
updates.push(updateConfig);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (config.semanticCommits === 'auto') {
|
|
const semanticCommits = await detectSemanticCommits();
|
|
for (const update of updates) {
|
|
update.semanticCommits = semanticCommits;
|
|
}
|
|
}
|
|
const filteredUpdates = updates
|
|
.filter((update) => update.enabled !== false)
|
|
.filter((update) => isUndefined(update.skipReason))
|
|
.map(({ vulnerabilityAlerts: _, ...update }) => update)
|
|
.map((update) => filterConfig(update, 'branch'));
|
|
if (filteredUpdates.length < updates.length) {
|
|
logger.debug(
|
|
`Filtered out ${updates.length - filteredUpdates.length} disabled update(s). ${filteredUpdates.length} update(s) remaining.`,
|
|
);
|
|
}
|
|
return filteredUpdates;
|
|
}
|