Skip to content

Commit

Permalink
feat(replacements): support for replacement name templating (#20905)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
  • Loading branch information
setchy and secustor committed Mar 21, 2023
1 parent 036aba7 commit b250220
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 30 deletions.
42 changes: 41 additions & 1 deletion docs/usage/configuration-options.md
Expand Up @@ -2176,9 +2176,49 @@ Managers which do not support replacement:
- `regex`

Use the `replacementName` config option to set the name of a replacement package.
Must be used with `replacementVersion` (see example below).

Can be used in combination with `replacementVersion`.

You can suggest a new community package rule by editing [the `replacements.ts` file on the Renovate repository](https://github.com/renovatebot/renovate/blob/main/lib/config/presets/internal/replacements.ts) and opening a pull request.

### replacementNameTemplate

<!-- prettier-ignore -->
!!! note
`replacementName` will take precedence if used within the same package rule.

Use the `replacementNameTemplate` config option to control the replacement name.

Use the triple brace `{{{ }}}` notation to avoid Handlebars escaping any special characters.

For example, the following package rule can be used to replace the registry for `docker` images:

```json
{
"packageRules": [
{
"matchDatasources": ["docker"],
"matchPackagePrefix": ["^docker.io/.*)"],
"replacementNameTemplate": "{{{replace 'docker.io/' 'ghcr.io/' packageName}}}"
}
]
}
```

Or, to add a registry prefix to any `docker` images that do not contain an explicit registry:

```json
{
"packageRules": [
{
"matchDatasources": ["docker"],
"matchPackagePrefix": ["^([^.]+)(\\/\\:)?$"],
"replacementNameTemplate": "some.registry.org/{{{packageName}}}"
}
]
}
```

### replacementVersion

This config option only works with some managers.
Expand Down
10 changes: 10 additions & 0 deletions lib/config/options/index.ts
Expand Up @@ -1214,6 +1214,16 @@ const options: RenovateOptions[] = [
cli: false,
env: false,
},
{
name: 'replacementNameTemplate',
description: 'Controls what the replacement package name.',
type: 'string',
default: '{{{packageName}}}',
stage: 'package',
parent: 'packageRules',
cli: false,
env: false,
},
{
name: 'replacementVersion',
description:
Expand Down
103 changes: 103 additions & 0 deletions lib/workers/repository/process/lookup/index.spec.ts
Expand Up @@ -1941,6 +1941,14 @@ describe('workers/repository/process/lookup/index', () => {
]);
});

it('handles replacements - skips if package and replacement names match', async () => {
config.packageName = 'openjdk';
config.currentValue = undefined;
config.datasource = DockerDatasource.id;
config.replacementName = 'openjdk';
expect((await lookup.lookupUpdates(config)).updates).toMatchObject([]);
});

it('handles replacements - name and version', async () => {
config.currentValue = '1.4.1';
config.packageName = 'q';
Expand All @@ -1958,6 +1966,101 @@ describe('workers/repository/process/lookup/index', () => {
]);
});

it('handles replacements - can template replacement name without a replacement version', async () => {
config.packageName = 'mirror.some.org/library/openjdk';
config.currentValue = '17.0.0';
config.replacementNameTemplate = `{{{replace 'mirror.some.org/' 'new.registry.io/' packageName}}}`;
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});

expect((await lookup.lookupUpdates(config)).updates).toMatchObject([
{
updateType: 'replacement',
newName: 'new.registry.io/library/openjdk',
newValue: '17.0.0',
},
{
updateType: 'major',
newMajor: 18,
newValue: '18.0.0',
newVersion: '18.0.0',
},
]);
});

it('handles replacements - can template replacement name with a replacement version', async () => {
config.packageName = 'mirror.some.org/library/openjdk';
config.currentValue = '17.0.0';
config.replacementNameTemplate = `{{{replace 'mirror.some.org/' 'new.registry.io/' packageName}}}`;
config.replacementVersion = '18.0.0';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});

expect((await lookup.lookupUpdates(config)).updates).toMatchObject([
{
updateType: 'replacement',
newName: 'new.registry.io/library/openjdk',
newValue: '18.0.0',
},
{
updateType: 'major',
newMajor: 18,
newValue: '18.0.0',
newVersion: '18.0.0',
},
]);
});

it('handles replacements - replacementName takes precedence over replacementNameTemplate', async () => {
config.packageName = 'mirror.some.org/library/openjdk';
config.currentValue = '17.0.0';
config.replacementNameTemplate = `{{{replace 'mirror.some.org/' 'new.registry.io/' packageName}}}`;
config.replacementName = 'eclipse-temurin';
config.datasource = DockerDatasource.id;
getDockerReleases.mockResolvedValueOnce({
releases: [
{
version: '17.0.0',
},
{
version: '18.0.0',
},
],
});

expect((await lookup.lookupUpdates(config)).updates).toMatchObject([
{
updateType: 'replacement',
newName: 'eclipse-temurin',
newValue: '17.0.0',
},
{
updateType: 'major',
newMajor: 18,
newValue: '18.0.0',
newVersion: '18.0.0',
},
]);
});

it('rollback for invalid version to last stable version', async () => {
config.currentValue = '2.5.17';
config.packageName = 'vue';
Expand Down
39 changes: 10 additions & 29 deletions lib/workers/repository/process/lookup/index.ts
Expand Up @@ -26,6 +26,11 @@ import { filterInternalChecks } from './filter-checks';
import { generateUpdate } from './generate';
import { getRollbackUpdate } from './rollback';
import type { LookupUpdateConfig, UpdateResult } from './types';
import {
addReplacementUpdateIfValid,
isReplacementNameRulesConfigured,
isReplacementRulesConfigured,
} from './utils';

export async function lookupUpdates(
inconfig: LookupUpdateConfig
Expand Down Expand Up @@ -157,27 +162,10 @@ export async function lookupUpdates(
}
let rangeStrategy = getRangeStrategy(config);

if (config.replacementName && !config.replacementVersion) {
res.updates.push({
updateType: 'replacement',
newName: config.replacementName,
newValue: currentValue!,
});
if (isReplacementRulesConfigured(config)) {
addReplacementUpdateIfValid(res.updates, config);
}

if (config.replacementName && config.replacementVersion) {
res.updates.push({
updateType: 'replacement',
newName: config.replacementName,
newValue: versioning.getNewValue({
// TODO #7154
currentValue: currentValue!,
newVersion: config.replacementVersion,
rangeStrategy: rangeStrategy!,
isReplacement: true,
})!,
});
}
// istanbul ignore next
if (
isVulnerabilityAlert &&
Expand Down Expand Up @@ -344,19 +332,12 @@ export async function lookupUpdates(
} else {
delete res.skipReason;
}
} else if (
!currentValue &&
config.replacementName &&
!config.replacementVersion
) {
} else if (!currentValue && isReplacementNameRulesConfigured(config)) {
logger.debug(
`Handle name-only replacement for ${packageName} without current version`
);
res.updates.push({
updateType: 'replacement',
newName: config.replacementName,
newValue: currentValue!,
});

addReplacementUpdateIfValid(res.updates, config);
} else {
res.skipReason = 'invalid-value';
}
Expand Down
1 change: 1 addition & 0 deletions lib/workers/repository/process/lookup/types.ts
Expand Up @@ -46,6 +46,7 @@ export interface LookupUpdateConfig
packageName: string;
minimumConfidence?: MergeConfidence | undefined;
replacementName?: string;
replacementNameTemplate?: string;
replacementVersion?: string;
}

Expand Down
72 changes: 72 additions & 0 deletions lib/workers/repository/process/lookup/utils.ts
@@ -0,0 +1,72 @@
import is from '@sindresorhus/is';

import { getRangeStrategy } from '../../../../modules/manager';
import type { LookupUpdate } from '../../../../modules/manager/types';
import * as allVersioning from '../../../../modules/versioning';
import * as template from '../../../../util/template';
import type { LookupUpdateConfig } from './types';

export function addReplacementUpdateIfValid(
updates: LookupUpdate[],
config: LookupUpdateConfig
): void {
const replacementNewName = determineNewReplacementName(config);
const replacementNewValue = determineNewReplacementValue(config);

if (
config.packageName !== replacementNewName ||
config.currentValue !== replacementNewValue
) {
updates.push({
updateType: 'replacement',
newName: replacementNewName,
newValue: replacementNewValue!,
});
}
}

export function isReplacementNameRulesConfigured(
config: LookupUpdateConfig
): boolean {
return (
is.nonEmptyString(config.replacementName) ||
is.nonEmptyString(config.replacementNameTemplate)
);
}

export function isReplacementRulesConfigured(
config: LookupUpdateConfig
): boolean {
return (
isReplacementNameRulesConfigured(config) ||
is.nonEmptyString(config.replacementVersion)
);
}

export function determineNewReplacementName(
config: LookupUpdateConfig
): string {
return (
config.replacementName ??
template.compile(config.replacementNameTemplate!, config, true)
);
}

export function determineNewReplacementValue(
config: LookupUpdateConfig
): string | undefined | null {
const versioning = allVersioning.get(config.versioning);
const rangeStrategy = getRangeStrategy(config);

if (!is.nullOrUndefined(config.replacementVersion)) {
return versioning.getNewValue({
// TODO #7154
currentValue: config.currentValue!,
newVersion: config.replacementVersion,
rangeStrategy: rangeStrategy!,
isReplacement: true,
});
}

return config.currentValue;
}

0 comments on commit b250220

Please sign in to comment.