From 7d519d6c9cd2f6bcdeaef3b3bb647a2efd766d60 Mon Sep 17 00:00:00 2001 From: 999sian <999sian@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:57:50 +0100 Subject: [PATCH] Skip pre-releases when picking "latest"; consolidate platform matching Fixes #89 and removes duplicated platform-detection logic. Pre-release handling (#89): the launcher treated the newest GitHub release as "latest" via Releases.FirstOrDefault(). The /releases API is newest-first but includes pre-releases, so a repo whose newest release is a pre-release (e.g. Skate 3 Recomp's nightly builds) was offered as the current version even though it isn't the finished build. Added GameInfo.SelectLatestRelease(), which prefers the newest stable release and falls back to the newest pre-release only when a repo publishes no stable releases at all (so pre-release-only repos still work). Mirrors GitHub's own /releases/latest semantics. Wired into all three selection sites: GameInfo version-check + download, and MainWindow "Update now". Consolidation: App.axaml.cs and CLIHandler.cs each carried a private copy of GetPlatformIdentifier() duplicating PlatformAssetMatcher.GetPlatformIdentifier(TargetOS.Auto). Removed both copies and call the shared Core method. Self-update asset selection in both files used a naive name.Contains(platformIdentifier) check, weaker than the PlatformAssetMatcher.MatchesPlatform() the game-download path already uses (e.g. "Linux-X64" is a substring of a "Linux-X64"/"Linux-ARM64" pairing only by lucky naming). Routed both self-update paths through MatchesPlatform for consistent, robust matching; the existing .zip/.tar.gz extension filter is preserved. Verified against the real compiled GameInfo.SelectLatestRelease (via reflection) and PlatformAssetMatcher: prerelease-at-top is skipped, all-stable picks newest, prerelease-only falls back, empty/null -> null; and the self-update matcher picks the correct asset per platform from the launcher's real release asset names without the ARM64/X64 mixup. --- App.axaml.cs | 31 +++---------------------------- MainWindow.axaml.cs | 2 +- Models/GameInfo.cs | 26 ++++++++++++++++++++++++-- Services/CLIHandler.cs | 26 ++------------------------ 4 files changed, 30 insertions(+), 55 deletions(-) diff --git a/App.axaml.cs b/App.axaml.cs index 32f1deb..0183e31 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Threading; +using GitHubLauncher.Core.Services; using System; using System.ComponentModel; using System.Diagnostics; @@ -743,9 +744,9 @@ private string NormalizeVersionString(string version) private async Task DownloadAndApplyUpdate(GitHubRelease latestRelease, string currentAppDirectory, UpdateCheckInfo updateCheckInfo) { - string platformIdentifier = GetPlatformIdentifier(); + string platformIdentifier = PlatformAssetMatcher.GetPlatformIdentifier(GitHubLauncher.Core.Models.TargetOS.Auto); var asset = latestRelease.assets.FirstOrDefault(a => - a.name.Contains(platformIdentifier, StringComparison.OrdinalIgnoreCase) && + PlatformAssetMatcher.MatchesPlatform(a.name, platformIdentifier) && (a.name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) || a.name.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)) ); @@ -1291,30 +1292,4 @@ private void ShutdownForUpdate() Environment.Exit(0); }); } - - private string GetPlatformIdentifier() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return "Windows"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "macOS"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - var arch = RuntimeInformation.OSArchitecture; - return arch switch - { - Architecture.Arm64 => "Linux-ARM64", - Architecture.X64 => "Linux-X64", - Architecture.X86 => "Linux-X86", - Architecture.Arm => "Linux-ARM", - _ => "Linux-X64" - }; - } - - throw new PlatformNotSupportedException("Unsupported operating system"); - } } diff --git a/MainWindow.axaml.cs b/MainWindow.axaml.cs index 243e94d..4a4bac7 100644 --- a/MainWindow.axaml.cs +++ b/MainWindow.axaml.cs @@ -1428,7 +1428,7 @@ private async Task HandleUpdateNowAsync(Control anchor, GameInfo game) { game.IsLoading = true; var releases = await game.FetchReleasesAsync(_gameManager.HttpClient); - var latestRelease = releases.FirstOrDefault(); + var latestRelease = GameInfo.SelectLatestRelease(releases); if (latestRelease == null) { await ShowMessageBoxAsync($"No downloadable releases were found for {game.Name}.", "No Releases"); diff --git a/Models/GameInfo.cs b/Models/GameInfo.cs index 010b4b5..eb5343a 100644 --- a/Models/GameInfo.cs +++ b/Models/GameInfo.cs @@ -1217,7 +1217,7 @@ private async Task CheckLatestVersionAsync(HttpClient httpClient, bool forceChec return; } - var latestRelease = result.Releases.FirstOrDefault(); + var latestRelease = SelectLatestRelease(result.Releases); if (latestRelease != null && !string.IsNullOrWhiteSpace(latestRelease.tag_name)) { LatestVersion = latestRelease.tag_name; @@ -1396,6 +1396,28 @@ buttonPanel.Children[0] is Button yesButton && return _cachedRelease; } + /// + /// Selects the release that should be treated as "latest". Releases from + /// the GitHub API are newest-first but include pre-releases, so a repo + /// whose newest release is a pre-release (e.g. nightly/beta builds) would + /// otherwise be offered as the current version. Prefer the newest stable + /// release; fall back to the newest pre-release only when a repo publishes + /// no stable releases at all. Mirrors GitHub's own /releases/latest. + /// + public static GitHubRelease? SelectLatestRelease(IReadOnlyList releases) + { + if (releases == null || releases.Count == 0) + return null; + + for (int i = 0; i < releases.Count; i++) + { + if (!releases[i].prerelease) + return releases[i]; + } + + return releases[0]; + } + public async Task> FetchReleasesAsync(HttpClient httpClient) { if (string.IsNullOrWhiteSpace(Repository)) @@ -1463,7 +1485,7 @@ private async Task DownloadAndInstallAsync(HttpClient httpClient, string gamesFo return; } - latestRelease = releaseResult.Releases.FirstOrDefault(); + latestRelease = SelectLatestRelease(releaseResult.Releases); if (latestRelease == null) { diff --git a/Services/CLIHandler.cs b/Services/CLIHandler.cs index e2bddb1..b41e29f 100644 --- a/Services/CLIHandler.cs +++ b/Services/CLIHandler.cs @@ -858,9 +858,9 @@ private async Task UpdateLauncher() return 0; } - string platformIdentifier = GetPlatformIdentifier(); + string platformIdentifier = PlatformAssetMatcher.GetPlatformIdentifier(TargetOS.Auto); var asset = release.assets.FirstOrDefault(a => - a.name.Contains(platformIdentifier, StringComparison.OrdinalIgnoreCase) && + PlatformAssetMatcher.MatchesPlatform(a.name, platformIdentifier) && (a.name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) || a.name.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))); if (asset == null) @@ -1119,28 +1119,6 @@ exit 1 }); } } - - private static string GetPlatformIdentifier() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return "Windows"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - return "macOS"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return RuntimeInformation.OSArchitecture switch - { - Architecture.Arm64 => "Linux-ARM64", - Architecture.X64 => "Linux-X64", - Architecture.X86 => "Linux-X86", - Architecture.Arm => "Linux-ARM", - _ => "Linux-X64" - }; - } - - throw new PlatformNotSupportedException("Unsupported operating system"); - } - private static bool IsNewerVersion(string latestVersion, string currentVersion) { try