renovate/lib/modules/manager/custom/regex/index.spec.ts

684 lines
19 KiB
TypeScript

import { codeBlock } from 'common-tags';
import { Fixtures } from '../../../../../test/fixtures';
import { logger } from '../../../../logger';
import type { CustomExtractConfig } from '../types';
import { defaultConfig, displayName, extractPackageFile } from '.';
const dockerfileContent = Fixtures.get(`Dockerfile`);
const ansibleYamlContent = Fixtures.get(`ansible.yml`);
const exampleJsonContent = Fixtures.get(`example.json`);
const exampleGitlabCiYml = Fixtures.get(`gitlab-ci.yml`);
describe('modules/manager/custom/regex/index', () => {
it('has default config', () => {
expect(defaultConfig).toEqual({
pinDigests: false,
});
});
it('has displayName', () => {
expect(displayName).toBe('Regex');
});
it('extracts multiple dependencies', async () => {
const config = {
matchStrings: [
'ENV .*?_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>[^&]*?)(\\&versioning=(?<versioning>[^&]*?))?\\s',
],
versioningTemplate:
'{{#if versioning}}{{versioning}}{{else}}semver{{/if}}',
depTypeTemplate: 'final',
};
const res = await extractPackageFile(
dockerfileContent,
'Dockerfile',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(8);
expect(res?.deps.find((dep) => dep.depName === 'yarn')?.versioning).toBe(
'semver',
);
expect(res?.deps.find((dep) => dep.depName === 'gradle')?.versioning).toBe(
'maven',
);
expect(res?.deps.filter((dep) => dep.depType === 'final')).toHaveLength(8);
});
it('returns null if no dependencies found', async () => {
const config = {
matchStrings: [
'ENV .*?_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<packageName>[^&]*?)(\\&versioning=(?<versioning>[^&]*?))?\\s',
],
versioningTemplate:
'{{#if versioning}}{{versioning}}{{else}}semver{{/if}}',
};
const res = await extractPackageFile('', 'Dockerfile', config);
expect(res).toBeNull();
});
it('returns null if invalid template', async () => {
const config = {
matchStrings: [
'ENV .*?_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>[^&]*?)(\\&versioning=(?<versioning>[^&]*?))?\\s',
],
versioningTemplate: '{{#if versioning}}{{versioning}}{{else}}semver',
};
const res = await extractPackageFile(
dockerfileContent,
'Dockerfile',
config,
);
expect(res).toBeNull();
});
it('extracts extractVersion', async () => {
const config = {
matchStrings: [
'ENV NGINX_MODULE_HEADERS_MORE_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?(\\&extractVersion=(?<extractVersion>.*?))?\\s',
],
};
const res = await extractPackageFile(
dockerfileContent,
'Dockerfile',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(1);
expect(
res?.deps.find(
(dep) => dep.depName === 'openresty/headers-more-nginx-module',
)?.extractVersion,
).toBe('^v(?<version>.*)$');
});
it('extracts registryUrl', async () => {
const config = {
matchStrings: [
'chart:\n *repository: (?<registryUrl>.*?)\n *name: (?<packageName>.*?)\n *version: (?<currentValue>.*)\n',
],
datasourceTemplate: 'helm',
};
const res = await extractPackageFile(
`
apiVersion: helm.fluxcd.io/v1
kind: HelmRelease
metadata:
name: prometheus-operator
namespace: monitoring
spec:
releaseName: prometheus-operator
chart:
repository: https://charts.helm.sh/stable
name: prometheus-operator
version: 8.12.13
`,
'Dockerfile',
config,
);
expect(res).toMatchSnapshot({
deps: [
{
currentValue: '8.12.13',
datasource: 'helm',
packageName: 'prometheus-operator',
registryUrls: ['https://charts.helm.sh/stable'],
},
],
});
});
it('extracts and applies a registryUrlTemplate', async () => {
const config = {
matchStrings: [
'ENV GRADLE_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s',
],
registryUrlTemplate: 'http://registry.{{depName}}.com/',
};
const res = await extractPackageFile(
dockerfileContent,
'Dockerfile',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(1);
expect(
res?.deps.find((dep) => dep.depName === 'gradle')?.registryUrls,
).toEqual(['http://registry.gradle.com/']);
});
it('extracts and does not apply a registryUrlTemplate if the result is an invalid url', async () => {
const config = {
matchStrings: [
'ENV GRADLE_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s',
],
registryUrlTemplate: 'this-is-not-a-valid-url-{{depName}}',
};
const res = await extractPackageFile(
dockerfileContent,
'Dockerfile',
config,
);
expect(res).toMatchSnapshot({
deps: [
{
currentValue: '6.2',
datasource: 'gradle-version',
depName: 'gradle',
versioning: 'maven',
replaceString:
'ENV GRADLE_VERSION=6.2 # gradle-version/gradle&versioning=maven\n',
},
],
});
expect(logger.warn).toHaveBeenCalledWith(
{ value: 'this-is-not-a-valid-url-gradle' },
'Invalid regex manager registryUrl',
);
});
it('extracts multiple dependencies with multiple matchStrings', async () => {
const config = {
matchStrings: [
'ENV GRADLE_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s',
'ENV NODE_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s',
],
versioningTemplate:
'{{#if versioning}}{{versioning}}{{else}}semver{{/if}}',
};
const res = await extractPackageFile(
dockerfileContent,
'Dockerfile',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(2);
expect(
res?.deps.find((dep) => dep.depName === 'nodejs/node')?.versioning,
).toBe('node');
expect(res?.deps.find((dep) => dep.depName === 'gradle')?.versioning).toBe(
'maven',
);
});
it('extracts dependency with autoReplaceStringTemplate', async () => {
const config = {
matchStrings: [
'image:\\s+(?<depName>my\\.old\\.registry\\/aRepository\\/andImage):(?<currentValue>[^\\s]+)',
],
depNameTemplate: 'my.new.registry/aRepository/andImage',
autoReplaceStringTemplate: 'image: {{{depName}}}:{{{newValue}}}',
datasourceTemplate: 'docker',
};
const res = await extractPackageFile(
'image: my.old.registry/aRepository/andImage:1.18-alpine',
'values.yaml',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(1);
});
it('extracts indentation: maintains indentation value if whitespace or empty', async () => {
const config = {
matchStrings: [
'(?<indentation>\\s*)image:\\s+(?<depName>[^\\s]+):(?<currentValue>[^\\s]+)',
],
autoReplaceStringTemplate:
'image:\n{{{indentation}}} name: {{{depName}}}:{{{newValue}}}',
datasourceTemplate: 'docker',
};
const res = await extractPackageFile(
' image: eclipse-temurin:17.0.0-alpine',
'bitbucket-pipelines.yml',
config,
);
expect(res).toMatchObject({
deps: [
{
depName: 'eclipse-temurin',
currentValue: '17.0.0-alpine',
datasource: 'docker',
indentation: ' ',
replaceString: ' image: eclipse-temurin:17.0.0-alpine',
},
],
});
});
it('extracts indentation: discards non-whitespace content', async () => {
const config = {
matchStrings: [
'(?<indentation>.*)image:\\s+(?<depName>[^\\s]+):(?<currentValue>[^\\s]+)',
],
autoReplaceStringTemplate:
'image:\n{{{indentation}}} name: {{{depName}}}:{{{newValue}}}',
datasourceTemplate: 'docker',
};
const res = await extractPackageFile(
'name: image: eclipse-temurin:17.0.0-alpine',
'bitbucket-pipelines.yml',
config,
);
expect(res).toMatchObject({
deps: [
{
depName: 'eclipse-temurin',
currentValue: '17.0.0-alpine',
datasource: 'docker',
indentation: '',
replaceString: 'name: image: eclipse-temurin:17.0.0-alpine',
},
],
});
});
it('extracts with combination strategy', async () => {
const config: CustomExtractConfig = {
matchStrings: [
'prometheus_image:\\s*"(?<depName>.*)"\\s*\\/\\/',
'prometheus_version:\\s*"(?<currentValue>.*)"\\s*\\/\\/',
],
matchStringsStrategy: 'combination',
datasourceTemplate: 'docker',
};
const res = await extractPackageFile(
ansibleYamlContent,
'ansible.yml',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(1);
});
it('extracts with combination strategy and non standard capture groups', async () => {
const config: CustomExtractConfig = {
matchStrings: [
'prometheus_registry:\\s*"(?<registry>.*)"\\s*\\/\\/',
'prometheus_repository:\\s*"(?<repository>.*)"\\s*\\/\\/',
'prometheus_tag:\\s*"(?<tag>.*)"\\s*\\/\\/',
'prometheus_version:\\s*"(?<currentValue>.*)"\\s*\\/\\/',
],
matchStringsStrategy: 'combination',
datasourceTemplate: 'docker',
depNameTemplate: '{{{ registry }}}/{{{ repository }}}',
};
const res = await extractPackageFile(
ansibleYamlContent,
'ansible.yml',
config,
);
expect(res?.deps).toHaveLength(1);
expect(res?.deps[0].depName).toBe('docker.io/prom/prometheus');
expect(res).toMatchSnapshot();
});
it('extracts with combination strategy and multiple matches', async () => {
const config: CustomExtractConfig = {
matchStrings: [
'.*_image:\\s*"(?<depName>.*)"\\s*\\/\\/',
'.*_version:\\s*"(?<currentValue>.*)"\\s*\\/\\/',
],
matchStringsStrategy: 'combination',
datasourceTemplate: 'docker',
};
const res = await extractPackageFile(
ansibleYamlContent,
'ansible.yml',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(1);
});
it('extracts with combination strategy and registry url', async () => {
const config: CustomExtractConfig = {
matchStringsStrategy: 'combination',
matchStrings: [
'CHART_VERSION: (?<currentValue>.*?)\n',
'CHART_REPOSITORY_URL: "(?<registryUrl>.*?)"',
'CHART_NAME: "(?<depName>.*?)"',
],
datasourceTemplate: 'helm',
};
const res = await extractPackageFile(
exampleGitlabCiYml,
'.gitlab-ci.yml',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(1);
});
it('extracts with combination strategy: sets replaceString when current version group present', async () => {
const config: CustomExtractConfig = {
matchStringsStrategy: 'combination',
matchStrings: [
'image:\\s+(?<depName>[a-z-]+)(?::(?<currentValue>[a-z0-9.-]+))?(?:@(?<currentDigest>sha256:[a-f0-9]+))?',
],
autoReplaceStringTemplate:
'image:\n name: {{{depName}}}{{#if newValue}}:{{{newValue}}}{{/if}}{{#if newDigest}}@{{{newDigest}}}{{/if}}',
datasourceTemplate: 'docker',
};
const res = await extractPackageFile(
'image: eclipse-temurin:17.0.0-alpine',
'bitbucket-pipelines.yml',
config,
);
expect(res).toMatchObject({
deps: [
{
depName: 'eclipse-temurin',
datasource: 'docker',
currentValue: '17.0.0-alpine',
replaceString: 'image: eclipse-temurin:17.0.0-alpine',
},
],
});
});
it('extracts with combination strategy: sets replaceString when current digest group present', async () => {
const config: CustomExtractConfig = {
matchStringsStrategy: 'combination',
matchStrings: [
'image:\\s+(?<depName>[a-z-]+)(?::(?<currentValue>[a-z0-9.-]+))?(?:@(?<currentDigest>sha256:[a-f0-9]+))?',
],
autoReplaceStringTemplate:
'image:\n name: {{{depName}}}{{#if newValue}}:{{{newValue}}}{{/if}}{{#if newDigest}}@{{{newDigest}}}{{/if}}',
datasourceTemplate: 'docker',
};
const res = await extractPackageFile(
'image: eclipse-temurin@sha256:1234567890abcdef',
'bitbucket-pipelines.yml',
config,
);
expect(res).toMatchObject({
deps: [
{
depName: 'eclipse-temurin',
datasource: 'docker',
currentDigest: 'sha256:1234567890abcdef',
replaceString: 'image: eclipse-temurin@sha256:1234567890abcdef',
},
],
});
});
it('extracts with combination strategy and templates', async () => {
const config: CustomExtractConfig = {
matchStringsStrategy: 'combination',
matchStrings: [
'CHART_REPOSITORY_URL: "(?<registryUrl>.*)\\/(?<depName>[a-z]+)\\/"',
'CHART_VERSION: (?<currentValue>.*?)\n',
],
datasourceTemplate: 'helm',
depNameTemplate: 'helm_repo/{{{ depName }}}',
};
const res = await extractPackageFile(
exampleGitlabCiYml,
'.gitlab-ci.yml',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(1);
});
it('extracts with combination strategy and empty file', async () => {
const config: CustomExtractConfig = {
matchStringsStrategy: 'combination',
matchStrings: [
'CHART_REPOSITORY_URL: "(?<registryUrl>.*)\\/(?<depName>[a-z]+)\\/"',
'CHART_VERSION: (?<currentValue>.*?)\n',
],
datasourceTemplate: 'helm',
depNameTemplate: 'helm_repo/{{{ depName }}}',
};
const res = await extractPackageFile('', '.gitlab-ci.yml', config);
expect(res).toBeNull();
});
it('extracts with recursive strategy and single match', async () => {
const config: CustomExtractConfig = {
matchStrings: [
'"group1":\\s*\\{[^}]*}',
'"name":\\s*"(?<depName>.*)"[^"]*"type":\\s*"(?<datasource>.*)"[^"]*"value":\\s*"(?<currentValue>.*)"',
],
matchStringsStrategy: 'recursive',
};
const res = await extractPackageFile(
exampleJsonContent,
'example.json',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(1);
});
it('extracts with recursive strategy and multiple matches', async () => {
const config: CustomExtractConfig = {
matchStrings: [
'"group.{1}":\\s*\\{[^}]*}',
'"name":\\s*"(?<depName>.*)"[^"]*"type":\\s*"(?<datasource>.*)"[^"]*"value":\\s*"(?<currentValue>.*)"',
],
matchStringsStrategy: 'recursive',
};
const res = await extractPackageFile(
exampleJsonContent,
'example.json',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(2);
});
it('extracts with recursive strategy and multiple layers ', async () => {
const config: CustomExtractConfig = {
matchStrings: [
'"backup":\\s*{[^}]*}',
'"test":\\s*\\{[^}]*}',
'"name":\\s*"(?<depName>.*)"[^"]*"type":\\s*"(?<datasource>.*)"[^"]*"value":\\s*"(?<currentValue>.*)"',
],
matchStringsStrategy: 'recursive',
};
const res = await extractPackageFile(
exampleJsonContent,
'example.json',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(1);
});
it('extracts with recursive strategy and fail because of not sufficient regexes', async () => {
const config: CustomExtractConfig = {
matchStrings: ['"group.{1}":\\s*\\{[^}]*}'],
matchStringsStrategy: 'recursive',
};
const res = await extractPackageFile(
exampleJsonContent,
'example.json',
config,
);
expect(res).toBeNull();
});
it('extracts with recursive strategy and fail because there is no match', async () => {
const config: CustomExtractConfig = {
matchStrings: ['"trunk.{1}":\\s*\\{[^}]*}'],
matchStringsStrategy: 'recursive',
};
const res = await extractPackageFile(
exampleJsonContent,
'example.json',
config,
);
expect(res).toBeNull();
});
it('extracts with recursive strategy and merged groups', async () => {
const config: CustomExtractConfig = {
matchStrings: [
'"(?<first>[^"]*)":\\s*{[^}]*}',
'"(?<second>[^"]*)":\\s*\\{[^}]*}',
'"name":\\s*"(?<depName>.*)"[^"]*"type":\\s*"(?<datasource>.*)"[^"]*"value":\\s*"(?<currentValue>.*)"',
],
matchStringsStrategy: 'recursive',
depNameTemplate: '{{{ first }}}/{{{ second }}}/{{{ depName }}}',
};
const res = await extractPackageFile(
exampleJsonContent,
'example.json',
config,
);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(4);
});
it('extracts with recursive strategy and without depName', async () => {
const config: CustomExtractConfig = {
matchStrings: [
'jacoco\\s*{[^}]*}',
'toolVersion\\s*=\\s*\\"(?<currentValue>\\S*)\\"\\s*',
],
matchStringsStrategy: 'recursive',
depNameTemplate: 'org.jacoco:jacoco',
datasourceTemplate: 'maven',
};
const res = await extractPackageFile(
`
jacoco {
toolVersion = "0.8.7"
}
`,
'build.gradle.kts',
config,
);
expect(res).toMatchObject({
deps: [
{
depName: 'org.jacoco:jacoco',
currentValue: '0.8.7',
datasource: 'maven',
},
],
});
});
it.each([
[
'dotnet',
codeBlock`
# renovate: datasource=dotnet packageName=dotnet-runtime
RUN install-tool dotnet 6.0.13
`,
'Dockerfile',
'dotnet-version',
'dotnet-runtime',
'dotnet',
],
[
'adoptium-java',
codeBlock`
# renovate: datasource=adoptium-java packageName=java
RUN install-tool java 6.0.13
`,
'Dockerfile',
'java-version',
'java',
'java',
],
[
'node',
codeBlock`
# renovate: datasource=node packageName=node
RUN install-tool node 6.0.13
`,
'Dockerfile',
'node-version',
'node',
'node',
],
])(
'migrates %s',
async (
_oldDatasource,
content,
packageFile,
newDatasource,
packageName,
depName,
) => {
const config: CustomExtractConfig = {
matchStrings: [
'# renovate: datasource=(?<datasource>[a-z-]+?)(?: (?:packageName|lookupName)=(?<packageName>.+?))?(?: versioning=(?<versioning>[a-z-]+?))?\\sRUN install-[a-z]+? (?<depName>[a-z-]+?) (?<currentValue>.+?)(?:\\s|$)',
],
versioningTemplate:
'{{#if versioning}}{{versioning}}{{else}}semver{{/if}}',
};
const res = await extractPackageFile(content, packageFile, config);
expect(res).toMatchObject({
deps: [
{
depName,
packageName,
currentValue: '6.0.13',
datasource: newDatasource,
},
],
});
},
);
});