diff --git a/lib/modules/manager/maven/extract.spec.ts b/lib/modules/manager/maven/extract.spec.ts index 3ff8e84aaec9d0e176272f8f408a34346fad9c2c..d6689de4238d701d2cb06028bb6373a4ad30db28 100644 --- a/lib/modules/manager/maven/extract.spec.ts +++ b/lib/modules/manager/maven/extract.spec.ts @@ -3,6 +3,7 @@ import { Fixtures } from '../../../../test/fixtures'; import { fs } from '../../../../test/util'; import { extractAllPackageFiles, + extractExtensions, extractPackage, extractRegistries, resolveParents, @@ -365,6 +366,27 @@ describe('modules/manager/maven/extract', () => { }); }); + describe('extractExtensions', () => { + it('returns null for invalid xml files', () => { + expect(extractExtensions('', '.mvn/extensions.xml')).toBeNull(); + expect( + extractExtensions('invalid xml content', '.mvn/extensions.xml'), + ).toBeNull(); + expect( + extractExtensions('<foobar></foobar>', '.mvn/extensions.xml'), + ).toBeNull(); + expect( + extractExtensions('<extensions></extensions>', '.mvn/extensions.xml'), + ).toBeNull(); + expect( + extractExtensions( + '<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0"></extensions>', + '.mvn/extensions.xml', + ), + ).toBeNull(); + }); + }); + describe('extractAllPackageFiles', () => { it('should return empty if package has no content', async () => { fs.readLocalFile.mockResolvedValueOnce(''); @@ -706,6 +728,47 @@ describe('modules/manager/maven/extract', () => { ]); }); + it('should extract from .mvn/extensions.xml file', async () => { + fs.readLocalFile.mockResolvedValueOnce(codeBlock` + <extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd"> + <extension> + <groupId>io.jenkins.tools.incrementals</groupId> + <artifactId>git-changelist-maven-extension</artifactId> + <version>1.6</version> + </extension> + </extensions> + `); + const res = await extractAllPackageFiles({}, ['.mvn/extensions.xml']); + expect(res).toMatchObject([ + { + packageFile: '.mvn/extensions.xml', + deps: [ + { + datasource: 'maven', + depName: + 'io.jenkins.tools.incrementals:git-changelist-maven-extension', + currentValue: '1.6', + depType: 'build', + fileReplacePosition: 372, + registryUrls: ['https://repo.maven.apache.org/maven2'], + }, + ], + }, + ]); + }); + + it('should return empty array if extensions file is invalid or empty', async () => { + fs.readLocalFile + .mockResolvedValueOnce('') + .mockResolvedValueOnce('invalid xml content'); + expect( + await extractAllPackageFiles({}, [ + '.mvn/extensions.xml', + 'grp/.mvn/extensions.xml', + ]), + ).toBeEmptyArray(); + }); + describe('root pom handling', () => { it('should skip root pom.xml', async () => { fs.readLocalFile.mockResolvedValueOnce(codeBlock` diff --git a/lib/modules/manager/maven/extract.ts b/lib/modules/manager/maven/extract.ts index 061c28fd92160d9ffa6308fffcd3ceff70692e2e..c19b2de1c42df86da9f21e9e245ca9c311471a7d 100644 --- a/lib/modules/manager/maven/extract.ts +++ b/lib/modules/manager/maven/extract.ts @@ -15,6 +15,12 @@ const supportedNamespaces = [ 'http://maven.apache.org/SETTINGS/1.2.0', ]; +const supportedExtensionsNamespaces = [ + 'http://maven.apache.org/EXTENSIONS/1.0.0', + 'http://maven.apache.org/EXTENSIONS/1.1.0', + 'http://maven.apache.org/EXTENSIONS/1.2.0', +]; + function parsePom(raw: string, packageFile: string): XmlDocument | null { let project: XmlDocument; try { @@ -39,6 +45,27 @@ function parsePom(raw: string, packageFile: string): XmlDocument | null { return null; } +function parseExtensions(raw: string, packageFile: string): XmlDocument | null { + let extensions: XmlDocument; + try { + extensions = new XmlDocument(raw); + } catch (err) { + logger.debug({ packageFile }, `Failed to parse as XML`); + return null; + } + const { name, attr, children } = extensions; + if (name !== 'extensions') { + return null; + } + if (!supportedExtensionsNamespaces.includes(attr.xmlns)) { + return null; + } + if (!is.nonEmptyArray(children)) { + return null; + } + return extensions; +} + function containsPlaceholder(str: string | null | undefined): boolean { return !!str && regEx(/\${[^}]*?}/).test(str); } @@ -476,6 +503,30 @@ function cleanResult(packageFiles: MavenInterimPackageFile[]): PackageFile[] { return packageFiles; } +export function extractExtensions( + rawContent: string, + packageFile: string, +): PackageFile | null { + if (!rawContent) { + return null; + } + + const extensions = parseExtensions(rawContent, packageFile); + if (!extensions) { + return null; + } + + const result: MavenInterimPackageFile = { + datasource: MavenDatasource.id, + packageFile, + deps: [], + }; + + result.deps = deepExtract(extensions); + + return result; +} + export async function extractAllPackageFiles( _config: ExtractConfig, packageFiles: string[], @@ -498,6 +549,13 @@ export async function extractAllPackageFiles( ); additionalRegistryUrls.push(...registries); } + } else if (packageFile.endsWith('.mvn/extensions.xml')) { + const extensions = extractExtensions(content, packageFile); + if (extensions) { + packages.push(extensions); + } else { + logger.trace({ packageFile }, 'can not read extensions'); + } } else { const pkg = extractPackage(content, packageFile); if (pkg) { diff --git a/lib/modules/manager/maven/index.ts b/lib/modules/manager/maven/index.ts index 8d12ca432bb60642800aa7962861967912b39f35..bd282a23395db6e8862232c8b3cf440ea85ebcc4 100644 --- a/lib/modules/manager/maven/index.ts +++ b/lib/modules/manager/maven/index.ts @@ -6,7 +6,11 @@ export { extractAllPackageFiles } from './extract'; export { bumpPackageVersion, updateDependency } from './update'; export const defaultConfig = { - fileMatch: ['(^|/|\\.)pom\\.xml$', '^(((\\.mvn)|(\\.m2))/)?settings\\.xml$'], + fileMatch: [ + '(^|/|\\.)pom\\.xml$', + '^(((\\.mvn)|(\\.m2))/)?settings\\.xml$', + '(^|/)\\.mvn/extensions\\.xml$', + ], versioning: mavenVersioning.id, };