Skip to content

Fix version checking with mixed state #10956

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
_innerBuilder.Services.AddHostedService<DistributedApplicationRunner>();
_innerBuilder.Services.AddHostedService<VersionCheckService>();
_innerBuilder.Services.AddSingleton<IPackageFetcher, PackageFetcher>();
_innerBuilder.Services.AddSingleton<IPackageVersionProvider, PackageVersionProvider>();
_innerBuilder.Services.AddSingleton(options);
_innerBuilder.Services.AddSingleton<ResourceNotificationService>();
_innerBuilder.Services.AddSingleton<ResourceLoggerService>();
Expand Down
11 changes: 11 additions & 0 deletions src/Aspire.Hosting/VersionChecking/IPackageVersionProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Semver;

namespace Aspire.Hosting.VersionChecking;

internal interface IPackageVersionProvider
{
SemVersion? GetPackageVersion();
}
15 changes: 15 additions & 0 deletions src/Aspire.Hosting/VersionChecking/PackageVersionProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Shared;
using Semver;

namespace Aspire.Hosting.VersionChecking;

internal sealed class PackageVersionProvider : IPackageVersionProvider
{
public SemVersion? GetPackageVersion()
{
return PackageUpdateHelpers.GetCurrentPackageVersion();
}
}
24 changes: 11 additions & 13 deletions src/Aspire.Hosting/VersionChecking/VersionCheckService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ internal sealed class VersionCheckService : BackgroundService

public VersionCheckService(IInteractionService interactionService, ILogger<VersionCheckService> logger,
IConfiguration configuration, DistributedApplicationOptions options, IPackageFetcher packageFetcher,
DistributedApplicationExecutionContext executionContext, TimeProvider timeProvider)
DistributedApplicationExecutionContext executionContext, TimeProvider timeProvider, IPackageVersionProvider packageVersionProvider)
{
_interactionService = interactionService;
_logger = logger;
Expand All @@ -44,7 +44,7 @@ public VersionCheckService(IInteractionService interactionService, ILogger<Versi
_executionContext = executionContext;
_timeProvider = timeProvider;

_appHostVersion = PackageUpdateHelpers.GetCurrentPackageVersion();
_appHostVersion = packageVersionProvider.GetPackageVersion();
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Expand Down Expand Up @@ -92,26 +92,24 @@ private async Task CheckForLatestAsync(CancellationToken cancellationToken)
}
}

SemVersion? latestVersion = null;
List<NuGetPackage>? packages = null;
SemVersion? storedKnownLatestVersion = null;
if (checkForLatestVersion)
{
var appHostDirectory = _configuration["AppHost:Directory"]!;

SecretsStore.TrySetUserSecret(_options.Assembly, LastCheckDateKey, now.ToString("o", CultureInfo.InvariantCulture));
var packages = await _packageFetcher.TryFetchPackagesAsync(appHostDirectory, cancellationToken).ConfigureAwait(false);

latestVersion = PackageUpdateHelpers.GetNewerVersion(_appHostVersion, packages);
packages = await _packageFetcher.TryFetchPackagesAsync(appHostDirectory, cancellationToken).ConfigureAwait(false);
}

if (TryGetConfigVersion(KnownLatestVersionKey, out var storedKnownLatestVersion))
else
{
if (latestVersion == null)
{
// Use the known latest version if we can't check for the latest version.
latestVersion = storedKnownLatestVersion;
}
TryGetConfigVersion(KnownLatestVersionKey, out storedKnownLatestVersion);
}

// Use known package versions to figure out what the newest valid version is.
// Note: A pre-release version is only selected if the current app host version is pre-release.
var latestVersion = PackageUpdateHelpers.GetNewerVersion(_appHostVersion, packages ?? [], storedKnownLatestVersion);

if (latestVersion == null || IsVersionGreaterOrEqual(_appHostVersion, latestVersion))
{
// App host version is up to date or the latest version is unknown.
Expand Down
28 changes: 19 additions & 9 deletions src/Shared/PackageUpdateHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ internal static class PackageUpdateHelpers
return informationalVersion;
}

public static SemVersion? GetNewerVersion(SemVersion currentVersion, IEnumerable<NuGetPackage> availablePackages)
public static SemVersion? GetNewerVersion(SemVersion currentVersion, IEnumerable<NuGetPackage> availablePackages, SemVersion? storedVersion = null)
{
SemVersion? newestStable = null;
SemVersion? newestPrerelease = null;
Expand All @@ -65,17 +65,15 @@ internal static class PackageUpdateHelpers
{
if (SemVersion.TryParse(package.Version, SemVersionStyles.Strict, out var version))
{
if (version.IsPrerelease)
{
newestPrerelease = newestPrerelease is null || SemVersion.PrecedenceComparer.Compare(version, newestPrerelease) > 0 ? version : newestPrerelease;
}
else
{
newestStable = newestStable is null || SemVersion.PrecedenceComparer.Compare(version, newestStable) > 0 ? version : newestStable;
}
ProcessNewVersion(version);
}
}

if (storedVersion != null)
{
ProcessNewVersion(storedVersion);
}

// Apply notification rules
if (currentVersion.IsPrerelease)
{
Expand All @@ -101,6 +99,18 @@ internal static class PackageUpdateHelpers
}

return null;

void ProcessNewVersion(SemVersion version)
{
if (version.IsPrerelease)
{
newestPrerelease = newestPrerelease is null || SemVersion.PrecedenceComparer.Compare(version, newestPrerelease) > 0 ? version : newestPrerelease;
}
else
{
newestStable = newestStable is null || SemVersion.PrecedenceComparer.Compare(version, newestStable) > 0 ? version : newestStable;
}
}
}

public static List<NuGetPackage> ParsePackageSearchResults(string stdout, string? packageId = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Semver;

namespace Aspire.Hosting.Tests.VersionChecking;

Expand Down Expand Up @@ -133,14 +134,69 @@ public async Task ExecuteAsync_InsideLastCheckIntervalHasLastKnown_NoFetchAndDis
Assert.False(packageFetcher.FetchCalled);
}

[Theory]
[InlineData("100.0.0", "100.0.0", false)]
[InlineData("1.0.0", "100.0.0", true)]
[InlineData("1.0.0", "100.0.0-pre1", false)]
[InlineData("1.0.0-pre1", "100.0.0-pre1", true)]
public async Task ExecuteAsync_InsideLastCheckIntervalHasLastKnownPrerelease_NoFetchAndMaybeDisplayMessage(string currentVersion, string lastKnownVersion, bool displayNotification)
{
// Arrange
var currentDate = new DateTimeOffset(2000, 12, 29, 20, 59, 59, TimeSpan.Zero);
var lastCheckDate = currentDate.AddMinutes(-1);

var timeProvider = new TestTimeProvider { UtcNow = currentDate };
var interactionService = new TestInteractionService();
var configurationManager = new ConfigurationManager();
configurationManager.AddInMemoryCollection(new Dictionary<string, string?>
{
[VersionCheckService.LastCheckDateKey] = lastCheckDate.ToString("o", CultureInfo.InvariantCulture),
[VersionCheckService.KnownLatestVersionKey] = lastKnownVersion
});

var packagesTcs = new TaskCompletionSource<List<NuGetPackage>>();
var packageFetcher = new TestPackageFetcher(packagesTcs.Task);
var service = CreateVersionCheckService(
interactionService: interactionService,
packageFetcher: packageFetcher,
configuration: configurationManager,
timeProvider: timeProvider,
packageVersionProvider: new TestPackageVersionProvider(SemVersion.Parse(currentVersion)));

// Act
_ = service.StartAsync(CancellationToken.None);

if (displayNotification)
{
var interaction = await interactionService.Interactions.Reader.ReadAsync().DefaultTimeout();
interaction.CompletionTcs.TrySetResult(InteractionResult.Ok(true));
await service.ExecuteTask!.DefaultTimeout();
}
else
{
await service.ExecuteTask!.DefaultTimeout();
interactionService.Interactions.Writer.Complete();
Assert.False(interactionService.Interactions.Reader.TryRead(out var _));
}

// Assert
Assert.False(packageFetcher.FetchCalled);
}

[Fact]
public async Task ExecuteAsync_OlderVersion_NoMessage()
{
// Arrange
var interactionService = new TestInteractionService();
var configurationManager = new ConfigurationManager();
var packagesTcs = new TaskCompletionSource<List<NuGetPackage>>();
var packageFetcher = new TestPackageFetcher(packagesTcs.Task);

var configurationManager = new ConfigurationManager();
configurationManager.AddInMemoryCollection(new Dictionary<string, string?>
{
[VersionCheckService.KnownLatestVersionKey] = "100.0.0" // ignored
});

var service = CreateVersionCheckService(interactionService: interactionService, packageFetcher: packageFetcher, configuration: configurationManager);

// Act
Expand Down Expand Up @@ -192,7 +248,8 @@ private static VersionCheckService CreateVersionCheckService(
IPackageFetcher? packageFetcher = null,
IConfiguration? configuration = null,
TimeProvider? timeProvider = null,
DistributedApplicationOptions? options = null)
DistributedApplicationOptions? options = null,
IPackageVersionProvider? packageVersionProvider = null)
{
return new VersionCheckService(
interactionService ?? new TestInteractionService(),
Expand All @@ -201,7 +258,8 @@ private static VersionCheckService CreateVersionCheckService(
options ?? new DistributedApplicationOptions(),
packageFetcher ?? new TestPackageFetcher(),
new DistributedApplicationExecutionContext(new DistributedApplicationOperation()),
timeProvider ?? new TestTimeProvider());
timeProvider ?? new TestTimeProvider(),
packageVersionProvider ?? new TestPackageVersionProvider());
}

private sealed class TestTimeProvider : TimeProvider
Expand All @@ -216,6 +274,21 @@ public override DateTimeOffset GetUtcNow()
}
}

private sealed class TestPackageVersionProvider : IPackageVersionProvider
{
private readonly SemVersion _version;

public TestPackageVersionProvider(SemVersion? version = null)
{
_version = version ?? new SemVersion(1, 0, 0);
}

public SemVersion? GetPackageVersion()
{
return _version;
}
}

private sealed class TestPackageFetcher : IPackageFetcher
{
private readonly Task<List<NuGetPackage>> _versionTask;
Expand Down
Loading