fix(updater): handle 4-part version strings to prevent TypeInitializationException on nightly builds#2249
Conversation
Nightly releases stamp AssemblyInfo with a version like "7.1.0.229925" which has a fourth component not accepted by NuGet.Versioning's SemanticVersion.Parse, causing a TypeInitializationException. - Add ParseVersion() helper that truncates to major.minor.patch before parsing, making static AppVersion initialization safe for nightly builds - Add a Where filter in CheckForUpdate to skip releases whose tag cannot be parsed as a SemanticVersion (defensive guard) - Add VersionTest cases for 4-part nightly version truncation Closes #2248 Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
|
@copilot Only keep the last 4 or 5 digits of the patch when present, if that is acceptable for version parsing |
Instead of discarding the 4th version component entirely (which made all nightly builds report as the same version), use the last 5 digits of the revision number as the patch component. e.g. "7.1.0.229925" → SemanticVersion 7.1.29925 This preserves ordering between nightly builds and is acceptable to NuGet.Versioning's SemanticVersion parser. Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
Done. |
There was a problem hiding this comment.
Pull request overview
This PR updates the updater’s version-handling logic so nightly builds stamped with a 4-part version string (e.g., 7.1.0.229925) no longer crash UpdateChecker during static initialization, restoring update-check functionality for nightlies.
Changes:
- Added
ParseVersion()helper and used it forUpdateChecker’s staticAppVersioninitialization. - Added filtering of GitHub releases to skip tags that can’t be parsed as semantic versions before ordering.
- Added/updated tests for nightly (4-part) and standard (3-part) version parsing, plus some formatting-only changes in icon extractor files.
Reviewed changes
Copilot reviewed 2 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| SoundSwitch/Framework/Updater/UpdateChecker.cs | Introduces ParseVersion() for 4-part versions and filters invalid release tags before ordering. |
| SoundSwitch.Tests/VersionTest.cs | Adds parameterized tests for nightly/standard version parsing behavior. |
| SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs | Formatting/whitespace-only normalization. |
| SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs | Formatting/whitespace-only normalization. |
| foreach (var release in (releases ?? Array.Empty<Release>()) | ||
| .Where(release => SemanticVersion.TryParse(release.TagName.Substring(1), out _)) | ||
| .OrderByDescending(release => SemanticVersion.Parse(release.TagName.Substring(1)))) |
| var parts = rawVersion.Split('.'); | ||
| SemanticVersion parsed; | ||
| if (parts.Length >= 4 && int.TryParse(parts[3], out var revision)) | ||
| { | ||
| var patch = revision % 100_000; | ||
| parsed = new SemanticVersion(int.Parse(parts[0]), int.Parse(parts[1]), patch); | ||
| } | ||
| else | ||
| { | ||
| var truncated = string.Join(".", parts.Take(3)); | ||
| parsed = SemanticVersion.Parse(truncated); | ||
| } |
| var parts = version.Split('.'); | ||
| if (parts.Length >= 4 && int.TryParse(parts[3], out var revision)) | ||
| { | ||
| var patch = revision % 100_000; | ||
| return new SemanticVersion(int.Parse(parts[0]), int.Parse(parts[1]), patch); | ||
| } | ||
|
|
||
| var truncated = string.Join(".", parts.Take(3)); | ||
| return SemanticVersion.Parse(truncated); | ||
| } |
| [TestCase("7.1.0.229925", "7.1.29925")] | ||
| [TestCase("1.2.3.456", "1.2.456")] | ||
| [TestCase("1.2.3.100001", "1.2.1")] | ||
| [TestCase("1.2.3", "1.2.3")] |
Nightly builds stamp
AssemblyInfowith a 4-part version like7.1.0.229925.SemanticVersion.Parse(NuGet.Versioning) rejects this format, causing the static initializer ofUpdateCheckerto throw — poisoning the entire type and breaking all update checks.Changes
UpdateChecker.cs— AddedParseVersion()helper that maps a 4-part version to a valid semver by using the last 5 digits of the revision component as the patch number (e.g.7.1.0.229925→7.1.29925). This preserves ordering between nightly builds rather than collapsing them all to the same version:Used for the static
AppVersionfield (the crash site) and applied a defensive.Where(TryParse)filter inCheckForUpdateto skip any release tags that aren't valid semver.VersionTest.cs— Added parameterized tests covering 4-part nightly versions and standard 3-part versions to guard against regression.