From 48d6a74ea6f27939304c8a4d03820fb033da09ae Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Tue, 1 Sep 2026 10:27:45 -0600 Subject: [PATCH] Pin Aspire container image tag to the package version, #83 The Aspire extension always requested the "latest" image tag. Docker treats "latest" as an ordinary tag rather than a moving reference resolved on each run, so a host that had already pulled it kept running the image from that first pull. Upgrading the NuGet package therefore left the container on whatever emulator build the host happened to have, which is not necessarily the one the new package expects. The tag is now derived from the package's own informational version, so upgrading the package requests a tag the host does not have yet and the pull brings down the matching emulator. Deriving it from the assembly rather than hardcoding a literal means a release bumps only the csproj version; ImageTag_MatchesPackageVersion asserts the derived tag against the version in the csproj so the two cannot drift apart. The demo app host overrides the tag back to "latest": it tracks the code in the working tree rather than a published release, so the pinned tag does not exist in the registry until the release is tagged. Also prepares the 1.0.0-beta.4 release by bumping both package versions. Co-Authored-By: Claude Opus 5 (1M context) --- .../AppHost.cs | 5 ++ ...reSearchEmulatorResourceExtensionsTests.cs | 24 +++++++++ .../AzureSearchEmulatorResourceTests.cs | 50 ++++++++++++++++++- .../AzureSearchEmulator.Aspire.csproj | 2 +- .../AzureSearchEmulatorResource.cs | 26 +++++++++- .../AzureSearchEmulatorResourceExtensions.cs | 9 ++-- .../AzureSearchEmulator.csproj | 2 +- 7 files changed, 110 insertions(+), 8 deletions(-) diff --git a/AzureSearchEmulator.Aspire.DemoAppHost/AppHost.cs b/AzureSearchEmulator.Aspire.DemoAppHost/AppHost.cs index 6616435..c8bc1d0 100644 --- a/AzureSearchEmulator.Aspire.DemoAppHost/AppHost.cs +++ b/AzureSearchEmulator.Aspire.DemoAppHost/AppHost.cs @@ -6,6 +6,11 @@ // Example container usage via F23.Aspire.Hosting.AzureSearchEmulator builder.AddAzureSearchEmulator("emulator-container") + // The package pins the image tag to its own version, but that image is not published until the + // release is tagged -- so in this repo the pinned tag does not exist yet and the pull fails. + // The demo tracks the code in the working tree rather than a release, so "latest" is the right + // tag here. Consumers of the package should leave the pinned default alone. + .WithImageTag("latest") .WithIndexesVolume(); builder.Build().Run(); diff --git a/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceExtensionsTests.cs b/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceExtensionsTests.cs index 06d4942..ef4e451 100644 --- a/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceExtensionsTests.cs +++ b/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceExtensionsTests.cs @@ -106,6 +106,30 @@ await callback.Callback(new HttpsCertificateConfigurationCallbackAnnotationConte return await valueProvider.GetValueAsync(TestContext.Current.CancellationToken); } + [Fact] + public void AddAzureSearchEmulator_PinsImageTagToPackageVersion() + { + // "latest" is an ordinary Docker tag, so a host that had already pulled it kept running the + // image from that first pull and never picked up the emulator a newer package expects. + // Pinning to the package version means upgrading the NuGet reference pulls a tag the host + // does not have yet. + + // Arrange + var builder = DistributedApplication.CreateBuilder(); + + // Act + var resource = builder.AddAzureSearchEmulator("my-emulator").Resource; + + // Assert + Assert.True(resource.TryGetAnnotationsOfType(out var imageAnnotations)); + var image = Assert.Single(imageAnnotations); + + Assert.Equal("ghcr.io", image.Registry); + Assert.Equal("feature23/azuresearchemulator", image.Image); + Assert.Equal(AzureSearchEmulatorResource.ImageTag, image.Tag); + Assert.NotEqual("latest", image.Tag); + } + [InlineData(false)] [InlineData(true)] [Theory] diff --git a/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceTests.cs b/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceTests.cs index d9bf234..09a936e 100644 --- a/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceTests.cs +++ b/AzureSearchEmulator.Aspire.Tests/AzureSearchEmulatorResourceTests.cs @@ -1,4 +1,5 @@ -using F23.Aspire.Hosting.AzureSearchEmulator; +using System.Xml.Linq; +using F23.Aspire.Hosting.AzureSearchEmulator; namespace AzureSearchEmulator.Aspire.Tests; @@ -16,4 +17,51 @@ public void Constructor_InitializesResourceProperly() // Assert Assert.Equal(name, resource.Name); } + + [Fact] + public void ImageTag_MatchesPackageVersion() + { + // The emulator image and this package are released together, so the pinned tag has to be a + // tag the release actually publishes. The Docker workflow tags images from the git tag via + // `type=semver,pattern={{version}}`, so `v1.2.3` publishes the image tag `1.2.3` -- which + // is the package version. Deriving the tag from the assembly keeps the two in step without + // a second place to edit at release time; this asserts that derivation stays correct. + + // Arrange + // Read the version the package actually ships from the csproj, rather than from the + // assembly the tag is already derived from -- comparing the derivation against its own + // input would pass no matter what the release version is. + var csproj = Path.Combine(GetRepositoryRoot(), + "AzureSearchEmulator.Aspire", "AzureSearchEmulator.Aspire.csproj"); + + var packageVersion = XDocument.Load(csproj) + .Descendants("Version") + .Single() + .Value + .Trim(); + + // Act + var tag = AzureSearchEmulatorResource.ImageTag; + + // Assert + Assert.Equal(packageVersion, tag); + + // The SDK appends "+" to the informational version, which is not part of any + // published image tag. + Assert.DoesNotContain('+', tag); + } + + private static string GetRepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AzureSearchEmulator.sln"))) + { + directory = directory.Parent; + } + + Assert.NotNull(directory); + + return directory.FullName; + } } diff --git a/AzureSearchEmulator.Aspire/AzureSearchEmulator.Aspire.csproj b/AzureSearchEmulator.Aspire/AzureSearchEmulator.Aspire.csproj index 7f3c6c3..4abdfb9 100644 --- a/AzureSearchEmulator.Aspire/AzureSearchEmulator.Aspire.csproj +++ b/AzureSearchEmulator.Aspire/AzureSearchEmulator.Aspire.csproj @@ -13,7 +13,7 @@ azure search emulator ai hosting docker Azure Search Emulator hosting support for Aspire. true - 1.0.0-beta.3 + 1.0.0-beta.4 feature[23] Copyright (c) feature[23] 2025 Paul Irwin diff --git a/AzureSearchEmulator.Aspire/AzureSearchEmulatorResource.cs b/AzureSearchEmulator.Aspire/AzureSearchEmulatorResource.cs index 438fe7c..5e1b19e 100644 --- a/AzureSearchEmulator.Aspire/AzureSearchEmulatorResource.cs +++ b/AzureSearchEmulator.Aspire/AzureSearchEmulatorResource.cs @@ -1,4 +1,5 @@ -using Aspire.Hosting.ApplicationModel; +using System.Reflection; +using Aspire.Hosting.ApplicationModel; namespace F23.Aspire.Hosting.AzureSearchEmulator; @@ -6,4 +7,27 @@ public class AzureSearchEmulatorResource(string name) : ContainerResource(name) { public const int DefaultHttpPort = 5100; public const int DefaultHttpsPort = 5143; + + internal const string ImageRegistry = "ghcr.io"; + internal const string ImageName = "feature23/azuresearchemulator"; + + /// + /// The container image tag used by . + /// + /// + /// This is the emulator version that matches this package's version, rather than "latest". + /// Docker treats "latest" as an ordinary tag, so a host that had already pulled it kept running + /// the image it pulled the first time and never picked up the emulator that a newer package + /// expects. Pinning ties the container to the package, so upgrading the NuGet reference pulls + /// the matching image. + /// + /// This is asserted against the package version by AzureSearchEmulatorResourceTests, so + /// releasing a new version does not need this value updated by hand. + /// + internal static readonly string ImageTag = typeof(AzureSearchEmulatorResource).Assembly + .GetCustomAttribute()! + .InformationalVersion + // The SDK appends "+" to the informational version; the published image tag + // carries only the version itself. + .Split('+')[0]; } diff --git a/AzureSearchEmulator.Aspire/AzureSearchEmulatorResourceExtensions.cs b/AzureSearchEmulator.Aspire/AzureSearchEmulatorResourceExtensions.cs index bbd724c..90672a3 100644 --- a/AzureSearchEmulator.Aspire/AzureSearchEmulatorResourceExtensions.cs +++ b/AzureSearchEmulator.Aspire/AzureSearchEmulatorResourceExtensions.cs @@ -21,7 +21,8 @@ public static class AzureSearchEmulatorResourceExtensions /// /// It is recommended to configure a volume for persisting index data using /// . - /// You can also override the default image tag ("latest") by using the returned resource builder's + /// The image tag defaults to the emulator version matching this package's version. You can + /// override it by using the returned resource builder's /// method. /// public IResourceBuilder AddAzureSearchEmulator(string name, @@ -32,9 +33,9 @@ public IResourceBuilder AddAzureSearchEmulator(stri #pragma warning disable ASPIRECERTIFICATES001 // WithHttpsDeveloperCertificate is experimental; see comment below. var resourceBuilder = builder.AddResource(resource) - .WithImage("feature23/azuresearchemulator") - .WithImageTag("latest") - .WithImageRegistry("ghcr.io") + .WithImage(AzureSearchEmulatorResource.ImageName) + .WithImageTag(AzureSearchEmulatorResource.ImageTag) + .WithImageRegistry(AzureSearchEmulatorResource.ImageRegistry) .WithHttpEndpoint(port: httpPort, targetPort: AzureSearchEmulatorResource.DefaultHttpPort, env: "HTTP_PORTS") .WithHttpsEndpoint(port: httpsPort, targetPort: AzureSearchEmulatorResource.DefaultHttpsPort, env: "HTTPS_PORTS") .WithEnvironment("ASPNETCORE_URLS", $"https://+:{resource.GetEndpoint("https").Property(EndpointProperty.Port)};http://+:{resource.GetEndpoint("http").Property(EndpointProperty.Port)}") diff --git a/AzureSearchEmulator/AzureSearchEmulator.csproj b/AzureSearchEmulator/AzureSearchEmulator.csproj index 4d91153..5172aba 100644 --- a/AzureSearchEmulator/AzureSearchEmulator.csproj +++ b/AzureSearchEmulator/AzureSearchEmulator.csproj @@ -5,7 +5,7 @@ 6751dc28-24d4-4572-8f44-e652593dd678 enable enable - 1.0.0-beta.3 + 1.0.0-beta.4 Nullable 14 true