diff --git a/docs/usage/java.md b/docs/usage/java.md index c0c58ea5e64555..372936a1590081 100644 --- a/docs/usage/java.md +++ b/docs/usage/java.md @@ -77,6 +77,14 @@ Renovate can update dependency versions found in Maven `pom.xml` files. Renovate will search repositories for all `pom.xml` files and processes them independently. +Renovate will also parse `settings.xml` files in the following locations: + +- `.mvn/settings.xml` +- `.m2/settings.xml` +- `settings.xml` + +Any repository URLs found within will be added as `registryUrls` to extracted dependencies. + ## Custom registry support, and authentication Unless using `deepExtract`, Renovate does not make use of authentication credentials available to Gradle. diff --git a/lib/manager/maven/__fixtures__/complex.settings.xml b/lib/manager/maven/__fixtures__/complex.settings.xml new file mode 100644 index 00000000000000..9017cbb6d5c6ca --- /dev/null +++ b/lib/manager/maven/__fixtures__/complex.settings.xml @@ -0,0 +1,74 @@ + + + + my-maven-repo + https://artifactory.company.com/artifactory/my-maven-repo + * + + + my-maven-repo-v2 + https://repo.adobe.com/nexus/content/groups/public + custom-repo + + + + + adobe-public + + + adobe-public-releases + Adobe Public Repository + https://repo.adobe.com/nexus/content/groups/public + + true + never + + + false + + + + adobe-public-releases-v2 + Adobe Public Repository v2 + https://repo.adobe.com/v2/nexus/content/groups/public + + true + never + + + false + + + + + + adobe-public-v2 + + + adobe-public-releases-v3 + Adobe Public Repository + https://repo.adobe.com/v3/nexus/content/groups/public + + true + never + + + false + + + + adobe-public-releases-v4 + Adobe Public Repository v2 + https://repo.adobe.com/v4/nexus/content/groups/public + + true + never + + + false + + + + + + diff --git a/lib/manager/maven/__fixtures__/mirror.settings.xml b/lib/manager/maven/__fixtures__/mirror.settings.xml new file mode 100644 index 00000000000000..8b024b07d096f5 --- /dev/null +++ b/lib/manager/maven/__fixtures__/mirror.settings.xml @@ -0,0 +1,9 @@ + + + + my-maven-repo + https://artifactory.company.com/artifactory/my-maven-repo + * + + + diff --git a/lib/manager/maven/__fixtures__/profile.settings.xml b/lib/manager/maven/__fixtures__/profile.settings.xml new file mode 100644 index 00000000000000..eb0143dd2933f2 --- /dev/null +++ b/lib/manager/maven/__fixtures__/profile.settings.xml @@ -0,0 +1,21 @@ + + + + adobe-public + + + adobe-public-releases + Adobe Public Repository + https://repo.adobe.com/nexus/content/groups/public + + true + never + + + false + + + + + + diff --git a/lib/manager/maven/extract.spec.ts b/lib/manager/maven/extract.spec.ts index e850cd1808c6b2..ac2d3adad94fb1 100644 --- a/lib/manager/maven/extract.spec.ts +++ b/lib/manager/maven/extract.spec.ts @@ -1,9 +1,13 @@ import { loadFixture } from '../../../test/util'; -import { extractPackage } from './extract'; +import { extractPackage, extractRegistries } from './extract'; const minimumContent = loadFixture(`minimum.pom.xml`); const simpleContent = loadFixture(`simple.pom.xml`); +const mirrorSettingsContent = loadFixture(`mirror.settings.xml`); +const profileSettingsContent = loadFixture(`profile.settings.xml`); +const complexSettingsContent = loadFixture(`complex.settings.xml`); + describe('manager/maven/extract', () => { describe('extractDependencies', () => { it('returns null for invalid XML', () => { @@ -81,4 +85,37 @@ describe('manager/maven/extract', () => { }); }); }); + describe('extractRegistries', () => { + it('returns null for invalid XML', () => { + expect(extractRegistries(undefined)).toBeEmptyArray(); + expect(extractRegistries('invalid xml content')).toBeEmptyArray(); + expect(extractRegistries('')).toBeEmptyArray(); + expect(extractRegistries('')).toBeEmptyArray(); + }); + + it('extract registries from a simple mirror settings file', () => { + const res = extractRegistries(mirrorSettingsContent); + expect(res).toStrictEqual([ + 'https://artifactory.company.com/artifactory/my-maven-repo', + ]); + }); + + it('extract registries from a simple profile settings file', () => { + const res = extractRegistries(profileSettingsContent); + expect(res).toStrictEqual([ + 'https://repo.adobe.com/nexus/content/groups/public', + ]); + }); + + it('extract registries from a complex profile settings file', () => { + const res = extractRegistries(complexSettingsContent); + expect(res).toStrictEqual([ + 'https://artifactory.company.com/artifactory/my-maven-repo', + 'https://repo.adobe.com/nexus/content/groups/public', + 'https://repo.adobe.com/v2/nexus/content/groups/public', + 'https://repo.adobe.com/v3/nexus/content/groups/public', + 'https://repo.adobe.com/v4/nexus/content/groups/public', + ]); + }); + }); }); diff --git a/lib/manager/maven/extract.ts b/lib/manager/maven/extract.ts index 74e91ff3da3c62..059f3826ced608 100644 --- a/lib/manager/maven/extract.ts +++ b/lib/manager/maven/extract.ts @@ -223,6 +223,61 @@ export function extractPackage( return result; } +export function extractRegistries(rawContent: string): string[] { + if (!rawContent) { + return []; + } + + const settings = parseSettings(rawContent); + if (!settings) { + return []; + } + + const urls = []; + + const mirrorUrls = parseUrls(settings, 'mirrors'); + urls.push(...mirrorUrls); + + settings.childNamed('profiles')?.eachChild((profile) => { + const repositoryUrls = parseUrls(profile, 'repositories'); + urls.push(...repositoryUrls); + }); + + // filter out duplicates + return [...new Set(urls)]; +} + +function parseUrls(xmlNode: XmlElement, path: string): string[] { + const children = xmlNode.descendantWithPath(path); + const urls = []; + if (children?.children) { + children.eachChild((child) => { + const url = child.valueWithPath('url'); + if (url) { + urls.push(url); + } + }); + } + return urls; +} + +export function parseSettings(raw: string): XmlDocument | null { + let settings: XmlDocument; + try { + settings = new XmlDocument(raw); + } catch (e) { + return null; + } + const { name, attr } = settings; + if (name !== 'settings') { + return null; + } + if (attr.xmlns === 'http://maven.apache.org/SETTINGS/1.0.0') { + return settings; + } + return null; +} + export function resolveParents(packages: PackageFile[]): PackageFile[] { const packageFileNames: string[] = []; const extractedPackages: Record = {}; @@ -310,17 +365,42 @@ export async function extractAllPackageFiles( packageFiles: string[] ): Promise { const packages: PackageFile[] = []; + const additionalRegistryUrls = []; + for (const packageFile of packageFiles) { const content = await readLocalFile(packageFile, 'utf8'); - if (content) { + if (!content) { + logger.trace({ packageFile }, 'packageFile has no content'); + continue; + } + if (packageFile.endsWith('settings.xml')) { + const registries = extractRegistries(content); + if (registries) { + logger.debug( + { registries, packageFile }, + 'Found registryUrls in settings.xml' + ); + additionalRegistryUrls.push(...registries); + } + } else { const pkg = extractPackage(content, packageFile); if (pkg) { packages.push(pkg); } else { - logger.debug({ packageFile }, 'can not read dependencies'); + logger.trace({ packageFile }, 'can not read dependencies'); + } + } + } + if (additionalRegistryUrls) { + for (const pkgFile of packages) { + for (const dep of pkgFile.deps) { + /* istanbul ignore else */ + if (dep.registryUrls) { + dep.registryUrls.push(...additionalRegistryUrls); + } else { + dep.registryUrls = [...additionalRegistryUrls]; + } } - } else { - logger.debug({ packageFile }, 'packageFile has no content'); } } return cleanResult(resolveParents(packages)); diff --git a/lib/manager/maven/index.spec.ts b/lib/manager/maven/index.spec.ts index e9027061dc60a2..c3ef2b1a801b67 100644 --- a/lib/manager/maven/index.spec.ts +++ b/lib/manager/maven/index.spec.ts @@ -9,6 +9,7 @@ const pomContent = loadFixture('simple.pom.xml'); const pomParent = loadFixture('parent.pom.xml'); const pomChild = loadFixture('child.pom.xml'); const origContent = loadFixture('grouping.pom.xml'); +const settingsContent = loadFixture('mirror.settings.xml'); function selectDep(deps: PackageDependency[], name = 'org.example:quuz') { return deps.find((dep) => dep.depName === name); @@ -28,6 +29,27 @@ describe('manager/maven/index', () => { expect(res).toBeEmptyArray(); }); + it('should return packages with urls from a settings file', async () => { + fs.readLocalFile + .mockResolvedValueOnce(settingsContent) + .mockResolvedValueOnce(pomContent); + const packages = await extractAllPackageFiles({}, [ + 'settings.xml', + 'simple.pom.xml', + ]); + const urls = [ + 'https://repo.maven.apache.org/maven2', + 'https://maven.atlassian.com/content/repositories/atlassian-public/', + 'https://artifactory.company.com/artifactory/my-maven-repo', + ]; + for (const pkg of packages) { + for (const dep of pkg.deps) { + const depUrls = [...dep.registryUrls]; + expect(depUrls).toEqual(urls); + } + } + }); + it('should return package files info', async () => { fs.readLocalFile.mockResolvedValueOnce(pomContent); const packages = await extractAllPackageFiles({}, ['random.pom.xml']); diff --git a/lib/manager/maven/index.ts b/lib/manager/maven/index.ts index b53ff3213328a2..7b285561f4e513 100644 --- a/lib/manager/maven/index.ts +++ b/lib/manager/maven/index.ts @@ -8,7 +8,7 @@ export { updateDependency } from './update'; export const language = ProgrammingLanguage.Java; export const defaultConfig = { - fileMatch: ['\\.pom\\.xml$', '(^|/)pom\\.xml$'], + fileMatch: ['(^|/|\\.)pom\\.xml$', '^(((\\.mvn)|(\\.m2))/)?settings\\.xml$'], versioning: mavenVersioning.id, };