import { codeBlock } from 'common-tags'; import { Fixtures } from '~test/fixtures.ts'; import { logger } from '../../../../logger/index.ts'; import type { CustomExtractConfig } from '../types.ts'; import { defaultConfig, displayName, extractPackageFile } from './index.ts'; 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=(?.*) # (?.*?)/(?[^&]*?)(\\&versioning=(?[^&]*?))?\\s', ], versioningTemplate: '{{#if versioning}}{{versioning}}{{else}}semver{{/if}}', depTypeTemplate: 'final', }; const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); expect(res).toMatchObject({ deps: [ { depName: 'gradle', currentValue: '6.2', datasource: 'gradle-version', versioning: 'maven', depType: 'final', }, { depName: 'nodejs/node', currentValue: '10.19.0', datasource: 'github-tags', versioning: 'node', depType: 'final', }, { depName: 'composer/composer', currentValue: '1.9.3', datasource: 'github-releases', versioning: 'semver', depType: 'final', }, { depName: 'cocoapods', currentValue: '1.9.0', datasource: 'rubygems', versioning: 'ruby', depType: 'final', }, { depName: 'docker/docker-ce', currentValue: '19.03.1', datasource: 'github-releases', versioning: 'docker', depType: 'final', }, { depName: 'python-poetry/poetry', currentValue: '1.0.0', datasource: 'github-releases', versioning: 'semver', depType: 'final', }, { depName: 'npm', currentValue: '6.10.2', datasource: 'npm', versioning: 'semver', depType: 'final', }, { depName: 'yarn', currentValue: '1.19.1', datasource: 'npm', versioning: 'semver', depType: 'final', }, ], }); }); it('returns null if no dependencies found', async () => { const config = { matchStrings: [ 'ENV .*?_VERSION=(?.*) # (?.*?)/(?[^&]*?)(\\&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=(?.*) # (?.*?)/(?[^&]*?)(\\&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=(?.*) # (?.*?)/(?.*?)(\\&versioning=(?.*?))?(\\&extractVersion=(?.*?))?\\s', ], }; const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); expect(res).toMatchObject({ deps: [ { depName: 'openresty/headers-more-nginx-module', currentValue: '0.30', datasource: 'github-tags', extractVersion: '^v(?.*)$', versioning: 'loose', }, ], }); }); it('extracts registryUrl', async () => { const config = { matchStrings: [ 'chart:\n *repository: (?.*?)\n *name: (?.*?)\n *version: (?.*)\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).toMatchObject({ deps: [ { packageName: 'prometheus-operator', currentValue: '8.12.13', datasource: 'helm', registryUrls: ['https://charts.helm.sh/stable'], }, ], }); }); it('extracts and applies a registryUrlTemplate', async () => { const config = { matchStrings: [ 'ENV GRADLE_VERSION=(?.*) # (?.*?)/(?.*?)(\\&versioning=(?.*?))?\\s', ], registryUrlTemplate: 'http://registry.{{depName}}.com/', }; const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); expect(res).toMatchObject({ deps: [ { depName: 'gradle', currentValue: '6.2', datasource: 'gradle-version', registryUrls: ['http://registry.gradle.com/'], }, ], }); 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=(?.*) # (?.*?)/(?.*?)(\\&versioning=(?.*?))?\\s', ], registryUrlTemplate: 'this-is-not-a-valid-url-{{depName}}', }; const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); expect(res).toEqual({ ...config, 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=(?.*) # (?.*?)/(?.*?)(\\&versioning=(?.*?))?\\s', 'ENV NODE_VERSION=(?.*) # (?.*?)/(?.*?)(\\&versioning=(?.*?))?\\s', ], versioningTemplate: '{{#if versioning}}{{versioning}}{{else}}semver{{/if}}', }; const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); expect(res).toMatchObject({ deps: [ { depName: 'gradle', currentValue: '6.2', datasource: 'gradle-version', versioning: 'maven', }, { depName: 'nodejs/node', currentValue: '10.19.0', datasource: 'github-tags', versioning: 'node', }, ], }); }); it('extracts dependency with autoReplaceStringTemplate', async () => { const config = { matchStrings: [ 'image:\\s+(?my\\.old\\.registry\\/aRepository\\/andImage):(?[^\\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).toMatchObject({ deps: [ { depName: 'my.new.registry/aRepository/andImage', currentValue: '1.18-alpine', datasource: 'docker', replaceString: 'image: my.old.registry/aRepository/andImage:1.18-alpine', }, ], }); }); it('extracts indentation: maintains indentation value if whitespace or empty', async () => { const config = { matchStrings: [ '(?\\s*)image:\\s+(?[^\\s]+):(?[^\\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: [ '(?.*)image:\\s+(?[^\\s]+):(?[^\\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*"(?.*)"\\s*\\/\\/', 'prometheus_version:\\s*"(?.*)"\\s*\\/\\/', ], matchStringsStrategy: 'combination', datasourceTemplate: 'docker', }; const res = await extractPackageFile( ansibleYamlContent, 'ansible.yml', config, ); expect(res).toMatchObject({ deps: [ { depName: 'prom/prometheus', currentValue: 'v2.21.0', datasource: 'docker', }, ], }); }); it('extracts with combination strategy and non standard capture groups', async () => { const config: CustomExtractConfig = { matchStrings: [ 'prometheus_registry:\\s*"(?.*)"\\s*\\/\\/', 'prometheus_repository:\\s*"(?.*)"\\s*\\/\\/', 'prometheus_tag:\\s*"(?.*)"\\s*\\/\\/', 'prometheus_version:\\s*"(?.*)"\\s*\\/\\/', ], matchStringsStrategy: 'combination', datasourceTemplate: 'docker', depNameTemplate: '{{{ registry }}}/{{{ repository }}}', }; const res = await extractPackageFile( ansibleYamlContent, 'ansible.yml', config, ); expect(res).toMatchObject({ deps: [ { depName: 'docker.io/prom/prometheus', currentValue: 'v2.21.0', datasource: 'docker', }, ], }); }); it('extracts with combination strategy and multiple matches', async () => { const config: CustomExtractConfig = { matchStrings: [ '.*_image:\\s*"(?.*)"\\s*\\/\\/', '.*_version:\\s*"(?.*)"\\s*\\/\\/', ], matchStringsStrategy: 'combination', datasourceTemplate: 'docker', }; const res = await extractPackageFile( ansibleYamlContent, 'ansible.yml', config, ); expect(res).toMatchObject({ deps: [ { depName: 'prom/prometheus', currentValue: '0.12.0', datasource: 'docker', }, ], }); }); it('extracts with combination strategy and registry url', async () => { const config: CustomExtractConfig = { matchStringsStrategy: 'combination', matchStrings: [ 'CHART_VERSION: (?.*?)\n', 'CHART_REPOSITORY_URL: "(?.*?)"', 'CHART_NAME: "(?.*?)"', ], datasourceTemplate: 'helm', }; const res = await extractPackageFile( exampleGitlabCiYml, '.gitlab-ci.yml', config, ); expect(res).toMatchObject({ deps: [ { depName: 'descheduler', currentValue: '0.19.2', datasource: 'helm', registryUrls: ['https://kubernetes-sigs.github.io/descheduler/'], }, ], }); }); it('extracts with combination strategy: sets replaceString when current version group present', async () => { const config: CustomExtractConfig = { matchStringsStrategy: 'combination', matchStrings: [ 'image:\\s+(?[a-z-]+)(?::(?[a-z0-9.-]+))?(?:@(?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+(?[a-z-]+)(?::(?[a-z0-9.-]+))?(?:@(?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: "(?.*)\\/(?[a-z]+)\\/"', 'CHART_VERSION: (?.*?)\n', ], datasourceTemplate: 'helm', depNameTemplate: 'helm_repo/{{{ depName }}}', }; const res = await extractPackageFile( exampleGitlabCiYml, '.gitlab-ci.yml', config, ); expect(res).toMatchObject({ deps: [ { depName: 'helm_repo/descheduler', currentValue: '0.19.2', datasource: 'helm', registryUrls: ['https://kubernetes-sigs.github.io/'], }, ], }); }); it('extracts with combination strategy and empty file', async () => { const config: CustomExtractConfig = { matchStringsStrategy: 'combination', matchStrings: [ 'CHART_REPOSITORY_URL: "(?.*)\\/(?[a-z]+)\\/"', 'CHART_VERSION: (?.*?)\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*"(?.*)"[^"]*"type":\\s*"(?.*)"[^"]*"value":\\s*"(?.*)"', ], matchStringsStrategy: 'recursive', }; const res = await extractPackageFile( exampleJsonContent, 'example.json', config, ); expect(res).toMatchObject({ deps: [ { depName: 'prom/prometheus', currentValue: 'v2.19.0', datasource: 'docker', }, ], }); }); it('extracts with recursive strategy and multiple matches', async () => { const config: CustomExtractConfig = { matchStrings: [ '"group.{1}":\\s*\\{[^}]*}', '"name":\\s*"(?.*)"[^"]*"type":\\s*"(?.*)"[^"]*"value":\\s*"(?.*)"', ], matchStringsStrategy: 'recursive', }; const res = await extractPackageFile( exampleJsonContent, 'example.json', config, ); expect(res).toMatchObject({ deps: [ { depName: 'prom/prometheus', currentValue: 'v2.19.0', datasource: 'docker', }, { depName: 'grafana/grafana', currentValue: '7.2.2', datasource: 'docker', }, ], }); }); it('extracts with recursive strategy and multiple layers', async () => { const config: CustomExtractConfig = { matchStrings: [ '"backup":\\s*{[^}]*}', '"test":\\s*\\{[^}]*}', '"name":\\s*"(?.*)"[^"]*"type":\\s*"(?.*)"[^"]*"value":\\s*"(?.*)"', ], matchStringsStrategy: 'recursive', }; const res = await extractPackageFile( exampleJsonContent, 'example.json', config, ); expect(res).toMatchObject({ deps: [ { depName: 'grafana/loki', currentValue: '1.6.1', datasource: 'docker', }, ], }); }); 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: [ '"(?[^"]*)":\\s*{[^}]*}', '"(?[^"]*)":\\s*\\{[^}]*}', '"name":\\s*"(?.*)"[^"]*"type":\\s*"(?.*)"[^"]*"value":\\s*"(?.*)"', ], matchStringsStrategy: 'recursive', depNameTemplate: '{{{ first }}}/{{{ second }}}/{{{ depName }}}', }; const res = await extractPackageFile( exampleJsonContent, 'example.json', config, ); expect(res).toMatchObject({ deps: [ { depName: 'group1/group1/prom/prometheus', currentValue: 'v2.19.0', datasource: 'docker', }, { depName: 'group2/group2/grafana/grafana', currentValue: '7.2.2', datasource: 'docker', }, { depName: 'backup/backup/grafana/loki', currentValue: '1.6.1', datasource: 'docker', }, { depName: 'setup/setup/python', currentValue: '3.9.0', datasource: 'docker', }, ], }); }); it('extracts with recursive strategy and without depName', async () => { const config: CustomExtractConfig = { matchStrings: [ 'jacoco\\s*{[^}]*}', 'toolVersion\\s*=\\s*\\"(?\\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=(?[a-z-]+?)(?: (?:packageName|lookupName)=(?.+?))?(?: versioning=(?[a-z-]+?))?\\sRUN install-[a-z]+? (?[a-z-]+?) (?.+?)(?:\\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, }, ], }); }, ); it('uses package file as dep name', async () => { const config: CustomExtractConfig = { matchStrings: ['\\s*(?.+)\\s*$'], depNameTemplate: '{{packageFile}}', datasourceTemplate: 'npm', }; const res = await extractPackageFile('1.0.0', 'xmldoc', config); expect(res).toMatchObject({ deps: [ { depName: 'xmldoc', currentValue: '1.0.0', datasource: 'npm', }, ], }); }); it('uses package file dir as dep name', async () => { const config: CustomExtractConfig = { matchStrings: ['\\s*(?.+)\\s*$'], depNameTemplate: '{{lookup (split packageFileDir "/") 1}}', datasourceTemplate: 'npm', }; const res = await extractPackageFile( '1.0.0', 'packages/xmldoc/version.env', config, ); expect(res).toMatchObject({ deps: [ { depName: 'xmldoc', currentValue: '1.0.0', datasource: 'npm', }, ], }); }); });