From d573aabcf639c21d999aabcc7c0513eed8cd28ee Mon Sep 17 00:00:00 2001 From: Nick Josevski Date: Tue, 8 Sep 2026 21:01:50 +1000 Subject: [PATCH 1/3] Run gzip directly in BuildDockerImages instead of through pwsh BuildDockerImages launched PowerShell for exactly one thing: running `gzip -k -9 -f` on the OCI tar. That made the step depend on whichever .NET runtime the agent's `pwsh` global tool was built against, and on main's build agent those no longer line up: App: /root/.dotnet/tools/pwsh Framework: 'Microsoft.NETCore.App', version '10.0.0' .NET location: .../.nuke/temp/dotnet-unix The following frameworks were found: 8.0.30 The agent's `pwsh` needs .NET 10, and the only runtime on offer is the .NET 8 SDK that build.sh bootstraps into .nuke/temp and puts on PATH. Calling gzip directly removes pwsh from the equation. This was the build's only use of PowerShellTasks, so nothing else in the build cares about the agent's pwsh now. Failures also surface properly. `pwsh -Command` exits 0 regardless of the native exit code, so a failed gzip used to show up later as a confusing missing-artifact error from PublishArtifacts. A Nuke Tool asserts a zero exit code, so it now fails at the gzip call with gzip's stderr attached. Verified with a throwaway target: resolves /usr/bin/gzip from PATH, arguments pass through intact, -k keeps the .tar alongside the .gz, and a deliberate failure raises `ProcessException: Process 'gzip' exited with code 1`. Not addressed here: reaching the SDK bootstrap at all means `dotnet --version` failed, so the agent no longer satisfies global.json's 8.0.419 pin. That costs every build a full SDK download and belongs with the .NET 10 work. Co-Authored-By: Claude Opus 5 (1M context) --- build/Build.Docker.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/build/Build.Docker.cs b/build/Build.Docker.cs index c9bf2006f..585b83b93 100644 --- a/build/Build.Docker.cs +++ b/build/Build.Docker.cs @@ -1,12 +1,15 @@ using Calamari.Build.Utilities; using JetBrains.Annotations; +using Nuke.Common.Tooling; using Nuke.Common.Tools.Docker; -using Nuke.Common.Tools.PowerShell; namespace Calamari.Build; public partial class Build { + //Resolved from PATH so a missing gzip fails by name, rather than as a mystery exit code + static Tool Gzip => ToolResolver.GetPathTool("gzip"); + [PublicAPI] Target BuildDockerImages => d => @@ -82,10 +85,10 @@ public partial class Build return settings; }); - //compress with gzip - PowerShellTasks.PowerShell(_ => _ - .EnableNoProfile() - .SetCommand($"gzip -k -9 -f '{outputFile}'")); + //compress with gzip. Invoked directly rather than via pwsh, which only + //added a dependency on whatever .NET runtime the agent's `pwsh` global tool + //was built against - not the SDK this build pins. + Gzip($"-k -9 -f \"{outputFile}\""); //gzip always uses the .gz suffix var compressedZipPath = $"{outputFile}.gz"; From 37858d60a04ea9ae7351ff178e3eb7484ed94775 Mon Sep 17 00:00:00 2001 From: Alastair Pitts Date: Thu, 17 Sep 2026 12:15:20 +1000 Subject: [PATCH 2/3] Update WindowsFxVersion for Azure App Service tests (#2165) --- .../AzureAppServiceDeployContainerBehaviourFixture.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/Calamari.AzureAppService.Tests/AzureAppServiceDeployContainerBehaviourFixture.cs b/source/Calamari.AzureAppService.Tests/AzureAppServiceDeployContainerBehaviourFixture.cs index de16da63f..e1845a6d8 100644 --- a/source/Calamari.AzureAppService.Tests/AzureAppServiceDeployContainerBehaviourFixture.cs +++ b/source/Calamari.AzureAppService.Tests/AzureAppServiceDeployContainerBehaviourFixture.cs @@ -57,7 +57,7 @@ protected override async Task ConfigureTestResources(ResourceGroupResource resou { SiteConfig = new SiteConfigProperties { - WindowsFxVersion = "DOCKER|mcr.microsoft.com/dotnet/samples:aspnetapp", + WindowsFxVersion = "DOCKER|mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2022", IsAlwaysOn = true, AppSettings = new List { @@ -318,4 +318,4 @@ void AddVariables(VariableDictionary vars) } } } -} \ No newline at end of file +} From 2c53efa024c7f66f7552e111f915c1ae6cfc4576 Mon Sep 17 00:00:00 2001 From: Alastair Pitts Date: Thu, 17 Sep 2026 12:31:06 +1000 Subject: [PATCH 3/3] Update WindowsFxVersion in new file --- ...pServiceDeployContainerBehaviourFixture.cs | 321 ------------------ ...pServiceDeployContainerBehaviourFixture.cs | 2 +- 2 files changed, 1 insertion(+), 322 deletions(-) delete mode 100644 source/Calamari.AzureAppService.Tests/AzureAppServiceDeployContainerBehaviourFixture.cs diff --git a/source/Calamari.AzureAppService.Tests/AzureAppServiceDeployContainerBehaviourFixture.cs b/source/Calamari.AzureAppService.Tests/AzureAppServiceDeployContainerBehaviourFixture.cs deleted file mode 100644 index e1845a6d8..000000000 --- a/source/Calamari.AzureAppService.Tests/AzureAppServiceDeployContainerBehaviourFixture.cs +++ /dev/null @@ -1,321 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Azure; -using Azure.ResourceManager.AppService; -using Azure.ResourceManager.AppService.Models; -using Azure.ResourceManager.Resources; -using Calamari.Azure; -using Calamari.Azure.AppServices; -using Calamari.AzureAppService.Azure; -using Calamari.AzureAppService.Behaviors; -using Calamari.Common.Commands; -using Calamari.Common.Plumbing.Variables; -using Calamari.Testing; -using Calamari.Testing.Azure; -using Calamari.Testing.Helpers; -using FluentAssertions; -using NUnit.Framework; -using Octostache; -using Polly; - -namespace Calamari.AzureAppService.Tests -{ - /// - /// Tests that both windows and linux app services can have container deployments - /// - /// - /// Both test fixtures have the same two tests, but they have different setups, so it's just easier to have separate test fixtures. - /// - public class AzureAppServiceDeployContainerBehaviourFixture - { - [TestFixture] - public class WhenUsingAWindowsAppService : AppServiceIntegrationTest - { - CalamariVariables newVariables; - readonly HttpClient client = new HttpClient(); - - //We are having capacity issues in EastUS and WestUS2 - protected override string DefaultResourceGroupLocation => RandomAzureRegion.GetRandomRegionWithExclusions("eastus", "westus2"); - - protected override async Task ConfigureTestResources(ResourceGroupResource resourceGroup) - { - var (_, webSite) = await CreateAppServicePlanAndWebApp(resourceGroup, - new AppServicePlanData(resourceGroup.Data.Location) - { - IsXenon = true, - IsHyperV = true, - Sku = new AppServiceSkuDescription - { - Name = "P1V3", - Tier = "PremiumV3" - } - }, - webSiteData: new WebSiteData(resourceGroup.Data.Location) - { - SiteConfig = new SiteConfigProperties - { - WindowsFxVersion = "DOCKER|mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2022", - IsAlwaysOn = true, - AppSettings = new List - { - new AppServiceNameValuePair { Name = "DOCKER_REGISTRY_SERVER_URL", Value = "https://index.docker.io" }, - new AppServiceNameValuePair { Name = "WEBSITES_ENABLE_APP_SERVICE_STORAGE", Value = "false" }, - new AppServiceNameValuePair { Name = "WEBSITES_CONTAINER_START_TIME_LIMIT", Value = "460" } - } - } - }); - - WebSiteResource = webSite; - - await AssertSetupSuccessAsync(); - } - - [Test] - public async Task AzureWindowsContainerDeploy() - { - newVariables = new CalamariVariables(); - AddVariables(newVariables); - - var runningContext = new RunningDeployment("", newVariables); - - await new AzureAppServiceContainerDeployBehaviour(new InMemoryLog()).Execute(runningContext); - - var targetSite = new AzureTargetSite(SubscriptionId, - ResourceGroupName, - WebSiteResource.Data.Name); - - await AssertDeploySuccessAsync(targetSite); - } - - [Test] - public async Task AzureWindowsContainerSlotDeploy() - { - var slotName = "stage"; - - newVariables = new CalamariVariables(); - AddVariables(newVariables); - newVariables.Add("Octopus.Action.Azure.DeploymentSlot", slotName); - await WebSiteResource.GetWebSiteSlots() - .CreateOrUpdateAsync(WaitUntil.Completed, - slotName, - WebSiteResource.Data); - - var runningContext = new RunningDeployment("", newVariables); - - await new AzureAppServiceContainerDeployBehaviour(new InMemoryLog()).Execute(runningContext); - - var targetSite = new AzureTargetSite(SubscriptionId, - ResourceGroupName, - WebSiteResource.Data.Name, - slotName); - - await AssertDeploySuccessAsync(targetSite); - } - - async Task AssertSetupSuccessAsync() - { - var timeout = Policy.TimeoutAsync(TimeSpan.FromMinutes(5)); - - var receivedContent = await timeout.ExecuteAsync(async () => - { - string content; - do - { - var response = await RetryPolicies.TestsTransientHttpErrorsPolicy - .ExecuteAsync(async context => - { - var r = await client.GetAsync($@"https://{WebSiteResource.Data.DefaultHostName}"); - - if (!r.IsSuccessStatusCode) - { - var messageContent = await r.Content.ReadAsStringAsync(); - TestContext.WriteLine($"Unable to retrieve content from https://{WebSiteResource.Data.DefaultHostName}, failed with: {messageContent}"); - } - - r.EnsureSuccessStatusCode(); - return r; - }, - contextData: new Dictionary()); - - content = await response.Content.ReadAsStringAsync(); - } while (content is null || content.Contains("container is starting up")); - - return content; - }); - - receivedContent.Should().Contain("

Welcome to .NET

"); - } - - async Task AssertDeploySuccessAsync(AzureTargetSite targetSite) - { - var imageName = newVariables.Get(SpecialVariables.Action.Package.PackageId); - var registryUrl = newVariables.Get(SpecialVariables.Action.Package.Registry); - var imageVersion = newVariables.Get(SpecialVariables.Action.Package.PackageVersion) ?? "latest"; - - var config = await WebSiteResource.GetWebSiteConfig().GetAsync(); - Assert.AreEqual($@"DOCKER|{imageName}:{imageVersion}", config.Value.Data.WindowsFxVersion); - - var appSettings = await ArmClient.GetAppSettingsListAsync(targetSite); - Assert.AreEqual("https://" + registryUrl, appSettings.FirstOrDefault(app => app.Name == "DOCKER_REGISTRY_SERVER_URL")?.Value); - } - - void AddVariables(VariableDictionary vars) - { - AddAzureVariables(vars); - vars.Add(SpecialVariables.Action.Package.FeedId, "Feeds-42"); - vars.Add(SpecialVariables.Action.Package.Registry, "index.docker.io"); - vars.Add(SpecialVariables.Action.Package.PackageId, "e2eteam/sample-apiserver"); - vars.Add(SpecialVariables.Action.Package.Image, "e2eteam/sample-apiserver:1.17"); - vars.Add(SpecialVariables.Action.Package.PackageVersion, "1.17"); - vars.Add(SpecialVariables.Action.Azure.DeploymentType, "Container"); - } - } - - [TestFixture] - public class WhenUsingALinuxAppService : AppServiceIntegrationTest - { - CalamariVariables newVariables; - readonly HttpClient client = new HttpClient(); - - // For some reason we are having issues creating these linux resources on Standard in EastUS - protected override string DefaultResourceGroupLocation => RandomAzureRegion.GetRandomRegionWithExclusions("eastus"); - - protected override async Task ConfigureTestResources(ResourceGroupResource resourceGroup) - { - var (_, webSite) = await CreateAppServicePlanAndWebApp(resourceGroup, - new AppServicePlanData(resourceGroup.Data.Location) - { - Kind = "linux", - IsReserved = true, - Sku = new AppServiceSkuDescription - { - Name = "P1V3", - Tier = "PremiumV3" - } - }, - new WebSiteData(resourceGroup.Data.Location) - { - Kind = "app,linux,container", - SiteConfig = new SiteConfigProperties - { - LinuxFxVersion = "DOCKER|mcr.microsoft.com/dotnet/samples:aspnetapp", - IsAlwaysOn = true, - AppSettings = new List - { - new AppServiceNameValuePair { Name = "DOCKER_REGISTRY_SERVER_URL", Value = "https://index.docker.io" }, - new AppServiceNameValuePair { Name = "WEBSITES_ENABLE_APP_SERVICE_STORAGE", Value = "false" }, - new AppServiceNameValuePair { Name = "WEBSITES_CONTAINER_START_TIME_LIMIT", Value = "460" } - } - } - }); - - WebSiteResource = webSite; - - await AssertSetupSuccessAsync(); - } - - [Test] - public async Task AzureLinuxContainerDeploy() - { - newVariables = new CalamariVariables(); - AddVariables(newVariables); - - var runningContext = new RunningDeployment("", newVariables); - - await new AzureAppServiceContainerDeployBehaviour(new InMemoryLog()).Execute(runningContext); - - var targetSite = new AzureTargetSite(SubscriptionId, - ResourceGroupName, - WebSiteResource.Data.Name); - - await AssertDeploySuccessAsync(targetSite); - } - - [Test] - public async Task AzureLinuxContainerSlotDeploy() - { - var slotName = "stage"; - - newVariables = new CalamariVariables(); - AddVariables(newVariables); - newVariables.Add("Octopus.Action.Azure.DeploymentSlot", slotName); - await WebSiteResource.GetWebSiteSlots() - .CreateOrUpdateAsync(WaitUntil.Completed, - slotName, - WebSiteResource.Data); - - var runningContext = new RunningDeployment("", newVariables); - - await new AzureAppServiceContainerDeployBehaviour(new InMemoryLog()).Execute(runningContext); - - var targetSite = new AzureTargetSite(SubscriptionId, - ResourceGroupName, - WebSiteResource.Data.Name, - slotName); - - await AssertDeploySuccessAsync(targetSite); - } - - async Task AssertSetupSuccessAsync() - { - var timeout = Policy.TimeoutAsync(TimeSpan.FromMinutes(5)); - - var receivedContent = await timeout.ExecuteAsync(async () => - { - string content; - do - { - var response = await RetryPolicies.TestsTransientHttpErrorsPolicy - .ExecuteAsync(async context => - { - var r = await client.GetAsync($@"https://{WebSiteResource.Data.DefaultHostName}"); - - if (!r.IsSuccessStatusCode) - { - var messageContent = await r.Content.ReadAsStringAsync(); - TestContext.WriteLine($"Unable to retrieve content from https://{WebSiteResource.Data.DefaultHostName}, failed with: {messageContent}"); - } - - r.EnsureSuccessStatusCode(); - return r; - }, - contextData: new Dictionary()); - - content = await response.Content.ReadAsStringAsync(); - } while (content is null || content.Contains("container is starting up")); - - return content; - }); - - receivedContent.Should().Contain("

Welcome to .NET

"); - } - - async Task AssertDeploySuccessAsync(AzureTargetSite targetSite) - { - var imageName = newVariables.Get(SpecialVariables.Action.Package.PackageId); - var registryUrl = newVariables.Get(SpecialVariables.Action.Package.Registry); - var imageVersion = newVariables.Get(SpecialVariables.Action.Package.PackageVersion) ?? "latest"; - - var config = await WebSiteResource.GetWebSiteConfig().GetAsync(); - Assert.AreEqual($@"DOCKER|{imageName}:{imageVersion}", config.Value.Data.LinuxFxVersion); - - var appSettings = await ArmClient.GetAppSettingsListAsync(targetSite); - Assert.AreEqual("https://" + registryUrl, appSettings.FirstOrDefault(app => app.Name == "DOCKER_REGISTRY_SERVER_URL")?.Value); - } - - void AddVariables(VariableDictionary vars) - { - AddAzureVariables(vars); - vars.Add(SpecialVariables.Action.Package.FeedId, "Feeds-42"); - vars.Add(SpecialVariables.Action.Package.Registry, "index.docker.io"); - vars.Add(SpecialVariables.Action.Package.PackageId, "nginx"); - vars.Add(SpecialVariables.Action.Package.Image, "nginx:latest"); - vars.Add(SpecialVariables.Action.Package.PackageVersion, "latest"); - vars.Add(SpecialVariables.Action.Azure.DeploymentType, "Container"); - } - } - } -} diff --git a/source/Calamari.AzureAppService.Tests/ExternalCloudIntegration/AzureAppServiceDeployContainerBehaviourFixture.cs b/source/Calamari.AzureAppService.Tests/ExternalCloudIntegration/AzureAppServiceDeployContainerBehaviourFixture.cs index 20ea175bf..65537c19a 100644 --- a/source/Calamari.AzureAppService.Tests/ExternalCloudIntegration/AzureAppServiceDeployContainerBehaviourFixture.cs +++ b/source/Calamari.AzureAppService.Tests/ExternalCloudIntegration/AzureAppServiceDeployContainerBehaviourFixture.cs @@ -53,7 +53,7 @@ protected override async Task ConfigureTestResources(ResourceGroupResource resou Kind = "app,linux,container", SiteConfig = new SiteConfigProperties { - LinuxFxVersion = "DOCKER|mcr.microsoft.com/dotnet/samples:aspnetapp", + LinuxFxVersion = "DOCKER|mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2022", IsAlwaysOn = true, AppSettings = new List {