From 4b55b4e51102b78b11aa64021d2cb56b198b2232 Mon Sep 17 00:00:00 2001
From: Danek Duvall <duvall@comfychair.org>
Date: Mon, 7 Aug 2023 22:36:59 -0700
Subject: [PATCH] feat(manager/nuget): bump VersionPrefix in MSBuild project
 files (#23464)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 lib/modules/manager/nuget/extract.ts     |  6 ++---
 lib/modules/manager/nuget/update.spec.ts | 25 +++++++++++++++++++-
 lib/modules/manager/nuget/update.ts      | 23 ++++++++++++++-----
 lib/modules/manager/nuget/util.spec.ts   | 29 ++++++++++++++++++++++++
 lib/modules/manager/nuget/util.ts        | 13 ++++++++++-
 5 files changed, 85 insertions(+), 11 deletions(-)
 create mode 100644 lib/modules/manager/nuget/util.spec.ts

diff --git a/lib/modules/manager/nuget/extract.ts b/lib/modules/manager/nuget/extract.ts
index fd740ba309..80d15ad9f0 100644
--- a/lib/modules/manager/nuget/extract.ts
+++ b/lib/modules/manager/nuget/extract.ts
@@ -12,7 +12,7 @@ import type {
 } from '../types';
 import { extractMsbuildGlobalManifest } from './extract/global-manifest';
 import type { DotnetToolsManifest } from './types';
-import { getConfiguredRegistries } from './util';
+import { findVersion, getConfiguredRegistries } from './util';
 
 /**
  * https://docs.microsoft.com/en-us/nuget/concepts/package-versioning
@@ -123,14 +123,14 @@ export async function extractPackageFile(
   }
 
   let deps: PackageDependency[] = [];
-  let packageFileVersion = undefined;
+  let packageFileVersion: string | undefined;
   try {
     const parsedXml = new XmlDocument(content);
     deps = extractDepsFromXml(parsedXml).map((dep) => ({
       ...dep,
       ...(registryUrls && { registryUrls }),
     }));
-    packageFileVersion = parsedXml.valueWithPath('PropertyGroup.Version');
+    packageFileVersion = findVersion(parsedXml)?.val;
   } catch (err) {
     logger.debug({ err, packageFile }, `Failed to parse XML`);
   }
diff --git a/lib/modules/manager/nuget/update.spec.ts b/lib/modules/manager/nuget/update.spec.ts
index 1701d0625c..42bc169f7b 100644
--- a/lib/modules/manager/nuget/update.spec.ts
+++ b/lib/modules/manager/nuget/update.spec.ts
@@ -66,7 +66,7 @@ describe('modules/manager/nuget/update', () => {
       expect(project.valueWithPath('PropertyGroup.Version')).toBe('1');
     });
 
-    it('does not bump version if csproj has no version', () => {
+    it('does not bump version if extract found no version', () => {
       const { bumpedContent } = bumpPackageVersion(
         minimumContent,
         undefined,
@@ -76,6 +76,18 @@ describe('modules/manager/nuget/update', () => {
       expect(bumpedContent).toEqual(minimumContent);
     });
 
+    it('does not bump version if csproj has no version', () => {
+      const originalContent =
+        '<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net6.0</TargetFramework></PropertyGroup></Project>';
+      const { bumpedContent } = bumpPackageVersion(
+        originalContent,
+        '0.0.1',
+        'patch'
+      );
+
+      expect(bumpedContent).toEqual(originalContent);
+    });
+
     it('returns content if bumping errors', () => {
       const { bumpedContent } = bumpPackageVersion(
         simpleContent,
@@ -95,5 +107,16 @@ describe('modules/manager/nuget/update', () => {
       const project = new XmlDocument(bumpedContent!);
       expect(project.valueWithPath('PropertyGroup.Version')).toBe('1.0.0-2');
     });
+
+    it('bumps csproj version prefix', () => {
+      const content =
+        '<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><VersionPrefix>1.0.0</VersionPrefix></PropertyGroup></Project>';
+      const { bumpedContent } = bumpPackageVersion(content, '1.0.0', 'patch');
+
+      const project = new XmlDocument(bumpedContent!);
+      expect(project.valueWithPath('PropertyGroup.VersionPrefix')).toBe(
+        '1.0.1'
+      );
+    });
   });
 });
diff --git a/lib/modules/manager/nuget/update.ts b/lib/modules/manager/nuget/update.ts
index 232c1ab850..60b6fe5b08 100644
--- a/lib/modules/manager/nuget/update.ts
+++ b/lib/modules/manager/nuget/update.ts
@@ -3,6 +3,7 @@ import { XmlDocument } from 'xmldoc';
 import { logger } from '../../../logger';
 import { replaceAt } from '../../../util/string';
 import type { BumpPackageVersionResult } from '../types';
+import { findVersion } from './util';
 
 export function bumpPackageVersion(
   content: string,
@@ -30,8 +31,23 @@ export function bumpPackageVersion(
 
   try {
     const project = new XmlDocument(content);
-    const versionNode = project.descendantWithPath('PropertyGroup.Version')!;
+    const versionNode = findVersion(project);
+    if (!versionNode) {
+      logger.warn(
+        "Couldn't find Version or VersionPrefix in any PropertyGroup"
+      );
+      return { bumpedContent };
+    }
+
     const currentProjVersion = versionNode.val;
+    if (currentProjVersion !== currentValue) {
+      logger.warn(
+        { currentValue, currentProjVersion },
+        "currentValue passed to bumpPackageVersion() doesn't match value found"
+      );
+      return { bumpedContent };
+    }
+
     const startTagPosition = versionNode.startTagPosition;
     const versionPosition = content.indexOf(
       currentProjVersion,
@@ -43,11 +59,6 @@ export function bumpPackageVersion(
       throw new Error('semver inc failed');
     }
 
-    if (currentProjVersion === newProjVersion) {
-      logger.debug('Version was already bumped');
-      return { bumpedContent };
-    }
-
     logger.debug(`newProjVersion: ${newProjVersion}`);
     bumpedContent = replaceAt(
       content,
diff --git a/lib/modules/manager/nuget/util.spec.ts b/lib/modules/manager/nuget/util.spec.ts
new file mode 100644
index 0000000000..58729ed3ce
--- /dev/null
+++ b/lib/modules/manager/nuget/util.spec.ts
@@ -0,0 +1,29 @@
+import { XmlDocument } from 'xmldoc';
+import { bumpPackageVersion } from './update';
+import { findVersion } from './util';
+
+describe('modules/manager/nuget/util', () => {
+  describe('findVersion', () => {
+    it('finds the version in a later property group', () => {
+      const content =
+        '<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net6.0</TargetFramework></PropertyGroup><PropertyGroup><Version>0.0.1</Version></PropertyGroup></Project>';
+      const { bumpedContent } = bumpPackageVersion(content, '0.0.1', 'patch');
+
+      const project = new XmlDocument(bumpedContent!);
+      const versionNode = findVersion(project);
+      const newVersion = versionNode!.val;
+      expect(newVersion).toBe('0.0.2');
+    });
+
+    it('picks version over versionprefix', () => {
+      const content =
+        '<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><VersionPrefix>0.0.5</VersionPrefix></PropertyGroup><PropertyGroup><Version>0.0.1</Version></PropertyGroup></Project>';
+      const { bumpedContent } = bumpPackageVersion(content, '0.0.1', 'patch');
+
+      const project = new XmlDocument(bumpedContent!);
+      const versionNode = findVersion(project);
+      const newVersion = versionNode!.val;
+      expect(newVersion).toBe('0.0.2');
+    });
+  });
+});
diff --git a/lib/modules/manager/nuget/util.ts b/lib/modules/manager/nuget/util.ts
index f21c787d54..b9463b118d 100644
--- a/lib/modules/manager/nuget/util.ts
+++ b/lib/modules/manager/nuget/util.ts
@@ -1,5 +1,5 @@
 import upath from 'upath';
-import { XmlDocument } from 'xmldoc';
+import { XmlDocument, XmlElement } from 'xmldoc';
 import { logger } from '../../../logger';
 import { findUpLocal, readLocalFile } from '../../../util/fs';
 import { regEx } from '../../../util/regex';
@@ -83,3 +83,14 @@ export async function getConfiguredRegistries(
   }
   return registries;
 }
+
+export function findVersion(parsedXml: XmlDocument): XmlElement | null {
+  for (const tag of ['Version', 'VersionPrefix']) {
+    for (const l1Elem of parsedXml.childrenNamed('PropertyGroup')) {
+      for (const l2Elem of l1Elem.childrenNamed(tag)) {
+        return l2Elem;
+      }
+    }
+  }
+  return null;
+}
-- 
GitLab