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