From 7462ee927557601368d68a3dc31b4f85bbffae0c Mon Sep 17 00:00:00 2001 From: Rom Date: Sat, 9 May 2026 20:11:32 -0700 Subject: [PATCH] Omit prerelease versions from update selection. --- src/AppInstallerCLITests/CompositeSource.cpp | 40 +++++++++++++++++++ .../PackageVersionSelection.cpp | 24 +++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/AppInstallerCLITests/CompositeSource.cpp b/src/AppInstallerCLITests/CompositeSource.cpp index b45993ac1a..b6a84a9a33 100644 --- a/src/AppInstallerCLITests/CompositeSource.cpp +++ b/src/AppInstallerCLITests/CompositeSource.cpp @@ -761,6 +761,46 @@ TEST_CASE("CompositePackage_AvailableVersions_NoChannelFilteredOut", "[Composite REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Channel).get() == channel); } +TEST_CASE("CompositePackage_PrereleaseAvailable_StableInstalled_NoUpdate", "[CompositeSource]") +{ + // microsoft/winget-pkgs#368913: a higher-precedence pre-release must not be + // offered as an upgrade to a stable installation. + auto availableVersion = GENERATE("3.14.5"sv, "3.14.5rc1"sv); + const bool isPrerelease = (availableVersion == "3.14.5rc1"sv); + + CompositeTestSetup setup; + setup.Installed->Everything.Matches.emplace_back(setup.MakeInstalled().WithVersion("3.14.4"sv), Criteria()); + setup.Available->SearchFunction = [&](const SearchRequest&) + { + auto manifest = MakeDefaultManifest(availableVersion); + // ManifestComparator::GetPreferredInstaller drops Architecture::Unknown on the host. + manifest.Installers[0].BaseInstallerType = Manifest::InstallerTypeEnum::Exe; + manifest.Installers[0].Arch = Utility::Architecture::Neutral; + + SearchResult result; + result.Matches.emplace_back( + TestCompositePackage::Make(std::vector{ manifest }, setup.Available), + Criteria()); + return result; + }; + + SearchResult result = setup.Search(); + REQUIRE(result.Matches.size() == 1); + + auto latest = GetLatestApplicableVersion(result.Matches[0].Package); + if (isPrerelease) + { + REQUIRE_FALSE(latest.UpdateAvailable); + REQUIRE_FALSE(latest.LatestApplicableVersion); + } + else + { + REQUIRE(latest.UpdateAvailable); + REQUIRE(latest.LatestApplicableVersion); + REQUIRE(latest.LatestApplicableVersion->GetProperty(PackageVersionProperty::Version).get() == availableVersion); + } +} + TEST_CASE("CompositeSource_MultipleAvailableSources_MatchAll", "[CompositeSource]") { TestCommon::TestUserSettings testSettings; diff --git a/src/AppInstallerRepositoryCore/PackageVersionSelection.cpp b/src/AppInstallerRepositoryCore/PackageVersionSelection.cpp index 5303b6e248..a4422264ca 100644 --- a/src/AppInstallerRepositoryCore/PackageVersionSelection.cpp +++ b/src/AppInstallerRepositoryCore/PackageVersionSelection.cpp @@ -10,6 +10,20 @@ namespace AppInstaller::Repository { namespace { + // Heuristic: any non-numeric tail in any Part. Catches both 1.0.0-rc.1 (SemVer) + // and 1.0.0rc1 (e.g. Python PEP 440); the manifest schema has no IsPrerelease field. + bool LooksLikePrerelease(const Utility::Version& version) + { + for (const auto& part : version.GetParts()) + { + if (!part.Other.empty()) + { + return true; + } + } + return false; + } + std::shared_ptr GetAvailablePackageFromSource(const std::vector>& packages, const std::string_view sourceIdentifier) { for (const std::shared_ptr& package : packages) @@ -175,6 +189,9 @@ namespace AppInstaller::Repository } AppInstaller::Manifest::ManifestComparator manifestComparator{ options }; + const bool installedIsPrerelease = installedVersion && + LooksLikePrerelease(Utility::Version{ installedVersion->GetProperty(PackageVersionProperty::Version) }); + auto availableVersionKeys = availableVersions->GetVersionKeys(); for (const auto& availableVersionKey : availableVersionKeys) { @@ -186,6 +203,13 @@ namespace AppInstaller::Repository continue; } + if (installedVersion && !installedIsPrerelease && + LooksLikePrerelease(Utility::Version{ availableVersion->GetProperty(PackageVersionProperty::Version) })) + { + // Stable installations are not auto-upgraded to a pre-release + continue; + } + if (evaluator.EvaluatePinType(availableVersion) != AppInstaller::Pinning::PinType::Unknown) { // Pinned