From 59eebe3ce9664e8f3942b6270c0b1900bd3e784b Mon Sep 17 00:00:00 2001 From: Sergio Zharinov <zharinov@users.noreply.github.com> Date: Sat, 2 Feb 2019 21:27:02 +0400 Subject: [PATCH] feat(maven): Add support for Maven (#3147) Alpha version of Maven support Closes #3029 --- lib/config/definitions.js | 13 ++ lib/manager/index.js | 1 + lib/manager/maven/extract.js | 91 +++++++++++++ lib/manager/maven/index.js | 46 +++++++ test/_fixtures/maven/simple.pom.xml | 127 ++++++++++++++++++ .../maven/__snapshots__/extract.spec.js.snap | 72 ++++++++++ test/manager/maven/extract.spec.js | 25 ++++ test/manager/maven/index.spec.js | 77 +++++++++++ .../extract/__snapshots__/index.spec.js.snap | 3 + website/docs/configuration-options.md | 2 + 10 files changed, 457 insertions(+) create mode 100644 lib/manager/maven/extract.js create mode 100644 lib/manager/maven/index.js create mode 100644 test/_fixtures/maven/simple.pom.xml create mode 100644 test/manager/maven/__snapshots__/extract.spec.js.snap create mode 100644 test/manager/maven/extract.spec.js create mode 100644 test/manager/maven/index.spec.js diff --git a/lib/config/definitions.js b/lib/config/definitions.js index b133ce8217..7104021cac 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -1379,6 +1379,19 @@ const options = [ mergeable: true, cli: false, }, + { + name: 'maven', + description: 'Configuration object for when renovating Maven pom.xml files', + releaseStatus: 'alpha', + stage: 'package', + type: 'json', + default: { + enabled: false, + fileMatch: ['\\.pom.xml$', '(^|/)pom.xml$'], + }, + mergeable: true, + cli: false, + }, { name: 'gitlabci', description: diff --git a/lib/manager/index.js b/lib/manager/index.js index 41391e7e56..aa8feb497d 100644 --- a/lib/manager/index.js +++ b/lib/manager/index.js @@ -13,6 +13,7 @@ const managerList = [ 'gradle', 'gradle-wrapper', 'kubernetes', + 'maven', 'meteor', 'npm', 'nuget', diff --git a/lib/manager/maven/extract.js b/lib/manager/maven/extract.js new file mode 100644 index 0000000000..e6bc3fff34 --- /dev/null +++ b/lib/manager/maven/extract.js @@ -0,0 +1,91 @@ +const { XmlDocument } = require('xmldoc'); +const { isVersion } = require('../../versioning/maven'); + +function parsePom(raw) { + let project; + try { + project = new XmlDocument(raw); + } catch (e) { + return null; + } + const { name, attr } = project; + if (name !== 'project') return null; + if (attr.xmlns !== 'http://maven.apache.org/POM/4.0.0') return null; + return project; +} + +function containsPlaceholder(str) { + return /\${.*?}/g.test(str); +} + +function depFromNode(node) { + if (!node.valueWithPath) return null; + const groupId = node.valueWithPath('groupId'); + const artifactId = node.valueWithPath('artifactId'); + const currentValue = node.valueWithPath('version'); + if (groupId && artifactId && currentValue) { + const depName = `${groupId}/${artifactId}`; + const result = { + depName, + currentValue, + }; + if (containsPlaceholder(depName)) { + result.skipReason = 'name-placeholder'; + } else if (containsPlaceholder(currentValue)) { + result.skipReason = 'version-placeholder'; + } else if (!isVersion(currentValue)) { + result.skipReason = 'not-a-version'; + } else { + const versionNode = node.descendantWithPath('version'); + const offset = '<version>'.length - 1; + result.fileReplacePosition = versionNode.startTagPosition + offset; + result.purl = `pkg:maven/${ + result.depName + }?repository_url=https://repo.maven.apache.org/maven2`; + } + return result; + } + return null; +} + +function deepExtract(node, result = [], isRoot = true) { + const dep = depFromNode(node); + if (dep && !isRoot) { + result.push(dep); + } + if (node.children) { + for (const child of node.children) { + deepExtract(child, result, false); + } + } + return result; +} + +function extractDependencies(raw) { + if (!raw) return null; + + const project = parsePom(raw); + if (!project) return null; + + const result = { datasource: 'maven' }; + + const homepage = project.valueWithPath('url'); + if (homepage && !containsPlaceholder(homepage)) { + result.homepage = homepage; + } + + const sourceUrl = project.valueWithPath('scm.url'); + if (sourceUrl && !containsPlaceholder(sourceUrl)) { + result.sourceUrl = sourceUrl; + } + + result.deps = deepExtract(project); + + return result; +} + +module.exports = { + containsPlaceholder, + parsePom, + extractDependencies, +}; diff --git a/lib/manager/maven/index.js b/lib/manager/maven/index.js new file mode 100644 index 0000000000..550c51e6ec --- /dev/null +++ b/lib/manager/maven/index.js @@ -0,0 +1,46 @@ +const { extractDependencies } = require('./extract'); + +async function extractAllPackageFiles(config, packageFiles) { + const mavenFiles = []; + for (const packageFile of packageFiles) { + const content = await platform.getFile(packageFile); + if (content) { + const deps = extractDependencies(content); + if (deps) { + mavenFiles.push({ + packageFile, + manager: 'maven', + ...deps, + }); + } else { + logger.info({ packageFile }, 'can not read dependencies'); + } + } else { + logger.info({ packageFile }, 'packageFile has no content'); + } + } + return mavenFiles; +} + +function updateDependency(fileContent, upgrade) { + const { currentValue, newValue, fileReplacePosition } = upgrade; + const leftPart = fileContent.slice(0, fileReplacePosition); + const rightPart = fileContent.slice(fileReplacePosition); + const versionClosePosition = rightPart.indexOf('</'); + const restPart = rightPart.slice(versionClosePosition); + const versionPart = rightPart.slice(0, versionClosePosition); + const version = versionPart.trim(); + if (version === newValue) { + return fileContent; + } + if (version === currentValue) { + const replacedPart = versionPart.replace(currentValue, newValue); + return leftPart + replacedPart + restPart; + } + return null; +} + +module.exports = { + extractAllPackageFiles, + updateDependency, +}; diff --git a/test/_fixtures/maven/simple.pom.xml b/test/_fixtures/maven/simple.pom.xml new file mode 100644 index 0000000000..0151002b32 --- /dev/null +++ b/test/_fixtures/maven/simple.pom.xml @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0"> + <parent> + <groupId>org.example</groupId> + <artifactId>parent</artifactId> + <version>42</version> + </parent> + + <modelVersion>4.0.0</modelVersion> + <groupId>org.example</groupId> + <artifactId>ExamplePomFile</artifactId> + + <name>Example</name> + <version>0.0.1</version> + <description>Minimal example</description> + <url>http://example.org/index.html</url> + + <scm> + <url>http://example.org/src.git</url> + </scm> + + <issueManagement> + <url>http://example.org/</url> + </issueManagement> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.example</groupId> + <artifactId>foo</artifactId> + <version>0.0.1</version> + </dependency> + <dependency> + <groupId>org.example</groupId> + <artifactId>bar</artifactId> + <version>1.0.0</version> + </dependency> + </dependencies> + </dependencyManagement> + + <build> + <finalName>SamplePomfile</finalName> + <plugins> + <plugin> + <artifactId>maven-release-plugin</artifactId> + <version>2.4.2</version> + <dependencies> + <dependency> + <groupId>org.apache.maven.scm</groupId> + <artifactId>maven-scm-provider-gitexe</artifactId> + <version>1.8.1</version> + </dependency> + </dependencies> + </plugin> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>1.8</source> + <target>1.8</target> + </configuration> + </plugin> + <plugin> + <groupId>org.jasig.maven</groupId> + <artifactId>maven-notice-plugin</artifactId> + <configuration> + <noticeTemplate>NOTICE.template</noticeTemplate> + <generateChildNotices>false</generateChildNotices> + </configuration> + </plugin> + </plugins> + </build> + + + <dependencies> + <dependency> + <groupId>org.example</groupId> + <artifactId>${artifact-id-placeholder}</artifactId> + <version>0.0.1</version> + </dependency> + <dependency> + <groupId>${group-id-placeholder}</groupId> + <artifactId>baz</artifactId> + <version>0.0.1</version> + </dependency> + <dependency> + <groupId>org.example</groupId> + <artifactId>quux</artifactId> + <version>${resourceServerVersion}</version> + </dependency> + <dependency> + <groupId>org.example</groupId> + <artifactId>quuz</artifactId> + <version>1.2.3</version> + </dependency> + <dependency> + <groupId>org.example</groupId> + <artifactId>quuuz</artifactId> + <version>it's not a version</version> + </dependency> + </dependencies> + + <profiles> + <profile> + <id>profile-id</id> + <dependencies> + <dependency> + <groupId>org.example</groupId> + <artifactId>profile-artifact</artifactId> + <version>${profile-placeholder}</version> + </dependency> + </dependencies> + </profile> + </profiles> + + <reporting> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + <version>2.17</version> + <configuration> + <configLocation>google_checks.xml</configLocation> + </configuration> + </plugin> + </plugins> + </reporting> +</project> diff --git a/test/manager/maven/__snapshots__/extract.spec.js.snap b/test/manager/maven/__snapshots__/extract.spec.js.snap new file mode 100644 index 0000000000..308875d11f --- /dev/null +++ b/test/manager/maven/__snapshots__/extract.spec.js.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`manager/maven/extract extractDependencies extract dependencies from any XML position 1`] = ` +Object { + "datasource": "maven", + "deps": Array [ + Object { + "currentValue": "42", + "depName": "org.example/parent", + "fileReplacePosition": 186, + "purl": "pkg:maven/org.example/parent?repository_url=https://repo.maven.apache.org/maven2", + }, + Object { + "currentValue": "0.0.1", + "depName": "org.example/foo", + "fileReplacePosition": 757, + "purl": "pkg:maven/org.example/foo?repository_url=https://repo.maven.apache.org/maven2", + }, + Object { + "currentValue": "1.0.0", + "depName": "org.example/bar", + "fileReplacePosition": 905, + "purl": "pkg:maven/org.example/bar?repository_url=https://repo.maven.apache.org/maven2", + }, + Object { + "currentValue": "1.8.1", + "depName": "org.apache.maven.scm/maven-scm-provider-gitexe", + "fileReplacePosition": 1337, + "purl": "pkg:maven/org.apache.maven.scm/maven-scm-provider-gitexe?repository_url=https://repo.maven.apache.org/maven2", + }, + Object { + "currentValue": "0.0.1", + "depName": "org.example/\${artifact-id-placeholder}", + "skipReason": "name-placeholder", + }, + Object { + "currentValue": "0.0.1", + "depName": "\${group-id-placeholder}/baz", + "skipReason": "name-placeholder", + }, + Object { + "currentValue": "\${resourceServerVersion}", + "depName": "org.example/quux", + "skipReason": "version-placeholder", + }, + Object { + "currentValue": "1.2.3", + "depName": "org.example/quuz", + "fileReplacePosition": 2529, + "purl": "pkg:maven/org.example/quuz?repository_url=https://repo.maven.apache.org/maven2", + }, + Object { + "currentValue": "it's not a version", + "depName": "org.example/quuuz", + "skipReason": "not-a-version", + }, + Object { + "currentValue": "\${profile-placeholder}", + "depName": "org.example/profile-artifact", + "skipReason": "version-placeholder", + }, + Object { + "currentValue": "2.17", + "depName": "org.apache.maven.plugins/maven-checkstyle-plugin", + "fileReplacePosition": 3218, + "purl": "pkg:maven/org.apache.maven.plugins/maven-checkstyle-plugin?repository_url=https://repo.maven.apache.org/maven2", + }, + ], + "homepage": "http://example.org/index.html", + "sourceUrl": "http://example.org/src.git", +} +`; diff --git a/test/manager/maven/extract.spec.js b/test/manager/maven/extract.spec.js new file mode 100644 index 0000000000..157cbff188 --- /dev/null +++ b/test/manager/maven/extract.spec.js @@ -0,0 +1,25 @@ +/* eslint-disable no-template-curly-in-string */ +const fs = require('fs'); +const path = require('path'); +const { extractDependencies } = require('../../../lib/manager/maven/extract'); + +const simpleContent = fs.readFileSync( + path.resolve(__dirname, `../../_fixtures/maven/simple.pom.xml`), + 'utf8' +); + +describe('manager/maven/extract', () => { + describe('extractDependencies', () => { + it('returns null for invalid XML', () => { + expect(extractDependencies()).toBeNull(); + expect(extractDependencies('invalid xml content')).toBeNull(); + expect(extractDependencies('<foobar></foobar>')).toBeNull(); + expect(extractDependencies('<project></project>')).toBeNull(); + }); + + it('extract dependencies from any XML position', () => { + const res = extractDependencies(simpleContent); + expect(res).toMatchSnapshot(); + }); + }); +}); diff --git a/test/manager/maven/index.spec.js b/test/manager/maven/index.spec.js new file mode 100644 index 0000000000..f86894ca10 --- /dev/null +++ b/test/manager/maven/index.spec.js @@ -0,0 +1,77 @@ +const fs = require('fs'); +const { extractDependencies } = require('../../../lib/manager/maven/extract'); +const { + extractAllPackageFiles, + updateDependency, +} = require('../../../lib/manager/maven/index'); + +const pomContent = fs.readFileSync( + 'test/_fixtures/maven/simple.pom.xml', + 'utf8' +); + +const findFn = ({ depName }) => depName === 'org.example/quuz'; + +describe('manager/maven', () => { + describe('extractAllPackageFiles', () => { + it('should return empty if package has no content', async () => { + platform.getFile.mockReturnValueOnce(null); + const res = await extractAllPackageFiles({}, ['random.pom.xml']); + expect(res).toEqual([]); + }); + + it('should return empty for packages with invalid content', async () => { + platform.getFile.mockReturnValueOnce('invalid content'); + const res = await extractAllPackageFiles({}, ['random.pom.xml']); + expect(res).toEqual([]); + }); + + it('should return package files info', async () => { + platform.getFile.mockReturnValueOnce(pomContent); + const packages = await extractAllPackageFiles({}, ['random.pom.xml']); + expect(packages.length).toEqual(1); + + const pkg = packages[0]; + expect(pkg.packageFile).toEqual('random.pom.xml'); + expect(pkg.manager).toEqual('maven'); + expect(pkg.deps).not.toBeNull(); + }); + }); + + describe('updateDependency', () => { + it('should update an existing dependency', () => { + const newValue = '9.9.9.9-final'; + + const { deps } = extractDependencies(pomContent); + const dep = deps.find(findFn); + const upgrade = { ...dep, newValue }; + const updatedContent = updateDependency(pomContent, upgrade); + const updatedDep = extractDependencies(updatedContent).deps.find(findFn); + + expect(updatedDep.currentValue).toEqual(newValue); + }); + + it('should not touch content if new and old versions are equal', () => { + const newValue = '1.2.3'; + + const { deps } = extractDependencies(pomContent); + const dep = deps.find(findFn); + const upgrade = { ...dep, newValue }; + const updatedContent = updateDependency(pomContent, upgrade); + + expect(pomContent).toBe(updatedContent); + }); + + it('should return null if current versions in content and upgrade are not same', () => { + const currentValue = '1.2.2'; + const newValue = '1.2.4'; + + const { deps } = extractDependencies(pomContent); + const dep = deps.find(findFn); + const upgrade = { ...dep, currentValue, newValue }; + const updatedContent = updateDependency(pomContent, upgrade); + + expect(updatedContent).toBeNull(); + }); + }); +}); diff --git a/test/workers/repository/extract/__snapshots__/index.spec.js.snap b/test/workers/repository/extract/__snapshots__/index.spec.js.snap index c4439c3eaa..31bda27d59 100644 --- a/test/workers/repository/extract/__snapshots__/index.spec.js.snap +++ b/test/workers/repository/extract/__snapshots__/index.spec.js.snap @@ -44,6 +44,9 @@ Object { "kubernetes": Array [ Object {}, ], + "maven": Array [ + Object {}, + ], "meteor": Array [ Object {}, ], diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md index 346f1b30af..a107ce814b 100644 --- a/website/docs/configuration-options.md +++ b/website/docs/configuration-options.md @@ -398,6 +398,8 @@ Add to this object if you wish to define rules that apply only to major updates. This value defaults to empty string, as historically no prefix was necessary for when Renovate was JS-only. Now - for example - we use `docker-` for Docker branches, so they may look like `renovate/docker-ubuntu-16.x`. +## maven + ## meteor Set enabled to `true` to enable meteor package updating. -- GitLab