Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(replacements): support for replacement name templating #20905

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
72bb0e9
feat(auto-replace): support package prefix replacements
setchy Mar 12, 2023
448b0ac
refactor: extract replacement logic into helper functions. add suppo…
setchy Mar 13, 2023
8890fae
fix(lint): add import
setchy Mar 13, 2023
dbabf27
rename functions
setchy Mar 13, 2023
ddb1c66
refactor based on PR feedback
setchy Mar 13, 2023
5340e45
use nonEmptyString instead of not nullOrUndefined
setchy Mar 13, 2023
2476e90
remove config.versioning - redundant
setchy Mar 13, 2023
b3565ff
Merge branch 'main' into feature/20890-replacement-prefix-add-remove
setchy Mar 13, 2023
a45b53f
fix typo
setchy Mar 14, 2023
0b39e24
pivot to a replacementNameTemplate config option
setchy Mar 16, 2023
6ed5650
fix lint error
setchy Mar 16, 2023
1a4ef21
Merge branch 'main' into feature/20890-replacement-prefix-add-remove
setchy Mar 16, 2023
a50ee1f
update
setchy Mar 19, 2023
6c2041a
pivot to using a single new template
setchy Mar 19, 2023
f8c81c3
set compile to true so that only allowedValues are passed
setchy Mar 19, 2023
c3930ad
Merge branch 'main' into feature/20890-replacement-prefix-add-remove
setchy Mar 19, 2023
d5e0dd3
remove new template string. can use replace helper
setchy Mar 19, 2023
d3138dc
update tests
setchy Mar 19, 2023
ef8a350
revert
setchy Mar 19, 2023
ecb536c
update test
setchy Mar 20, 2023
c86c96a
Update lib/workers/repository/process/lookup/utils.ts
setchy Mar 20, 2023
27f1911
docs: improve docs
setchy Mar 21, 2023
99a6ad7
Merge branch 'main' into feature/20890-replacement-prefix-add-remove
setchy Mar 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface LookupUpdateConfig
packageName: string;
minimumConfidence?: string;
replacementName?: string;
replacementNameTemplate?: string;
replacementVersion?: string;
}

Expand Down
72 changes: 72 additions & 0 deletions lib/workers/repository/process/lookup/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}