From 4aa7cef8b266bf229649a4b46effa607e8a9860a Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 19 Aug 2026 16:04:54 -0400 Subject: [PATCH 1/2] fix(release): install .NET 9 for wingetcreate WinGet publication fails before submit because the latest standalone wingetcreate targets .NET 9 and is framework-dependent, while the release agent image has no compatible runtime: wingetcreate.exe : You must install or update .NET to run this application. Install the .NET 9 runtime in the gated submission path before launching wingetcreate. Exercise the latest executable with `info` in normal WinGet preparation builds so future runtime incompatibilities fail before release. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../templates/prepare-winget-manifest.yml | 20 ++++++++++++++ eng/pipelines/templates/publish-winget.yml | 27 ++++++++++++++++--- eng/winget/README.md | 5 ++++ .../ReleasePublishNugetPipelineTests.cs | 26 ++++++++++++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/eng/pipelines/templates/prepare-winget-manifest.yml b/eng/pipelines/templates/prepare-winget-manifest.yml index e02dc433ace..3a7342318a8 100644 --- a/eng/pipelines/templates/prepare-winget-manifest.yml +++ b/eng/pipelines/templates/prepare-winget-manifest.yml @@ -50,6 +50,26 @@ steps: Write-Host "##vso[task.setvariable variable=WinGetIsPrereleaseInStablePackage]$isPrereleaseInStablePackage" displayName: 🟣Set version ${{ parameters.version }} + # The standalone wingetcreate executable is framework-dependent. Exercise it + # in regular builds so an upstream target-framework change is caught before + # the release pipeline attempts a public submission. + - task: UseDotNet@2 + displayName: '🟣Install .NET 9 runtime for wingetcreate' + inputs: + packageType: 'runtime' + version: '9.0.x' + + - pwsh: | + $ErrorActionPreference = 'Stop' + + Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile "$(Build.StagingDirectory)/wingetcreate.exe" + & "$(Build.StagingDirectory)/wingetcreate.exe" info + if ($LASTEXITCODE -ne 0) { + Write-Error "wingetcreate info failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } + displayName: '🟣Verify wingetcreate' + - pwsh: | # Probe-only: do not attempt to install/repair winget here. The 1ES `1es-windows-2022` # pool blocks outbound access to cdn.winget.microsoft.com, so diff --git a/eng/pipelines/templates/publish-winget.yml b/eng/pipelines/templates/publish-winget.yml index 34aea42c72c..9c449b9d426 100644 --- a/eng/pipelines/templates/publish-winget.yml +++ b/eng/pipelines/templates/publish-winget.yml @@ -181,16 +181,35 @@ steps: # Same gate as the installer-URL validation above and the Submit step below. condition: | and( - succeeded(), - eq('${{ parameters.dryRun }}', 'false'), - eq(variables['_IsProductionBranch'], 'true') + succeeded(), + eq('${{ parameters.dryRun }}', 'false'), + eq(variables['_IsProductionBranch'], 'true') + ) + + # The latest standalone wingetcreate executable targets .NET 9 and is not + # self-contained, so the release agent must provide a compatible runtime. + - task: UseDotNet@2 + displayName: '🟣Install .NET 9 runtime for wingetcreate' + inputs: + packageType: 'runtime' + version: '9.0.x' + condition: | + and( + succeeded(), + eq('${{ parameters.dryRun }}', 'false'), + eq(variables['_IsProductionBranch'], 'true') ) - powershell: | $ErrorActionPreference = 'Stop' Write-Host "Downloading wingetcreate..." Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile "$(Build.StagingDirectory)/wingetcreate.exe" - Write-Host "wingetcreate downloaded successfully" + & "$(Build.StagingDirectory)/wingetcreate.exe" info + if ($LASTEXITCODE -ne 0) { + Write-Error "wingetcreate info failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } + Write-Host "wingetcreate downloaded and verified successfully" displayName: '🟣Install wingetcreate' # Skip when no Submit step will run. wingetcreate is only used to submit to # the upstream winget-pkgs repo, so dry-run and non-production-branch builds diff --git a/eng/winget/README.md b/eng/winget/README.md index b3539d11d21..892a8c79c7c 100644 --- a/eng/winget/README.md +++ b/eng/winget/README.md @@ -55,6 +55,11 @@ Where arch is `x64` or `arm64`. | `release-publish-nuget.yml` (release) | — | Stable manifests only | Publishing submits a PR to `microsoft/winget-pkgs` using `wingetcreate submit`. +The Azure DevOps prepare stage also installs the .NET 9 runtime, downloads the +latest standalone `wingetcreate`, and runs `wingetcreate info`. This +side-effect-free smoke catches runtime compatibility changes before the release +pipeline attempts a public submission; it does not exercise credentials or +submission. ## Validation model diff --git a/tests/Infrastructure.Tests/Pipelines/ReleasePublishNugetPipelineTests.cs b/tests/Infrastructure.Tests/Pipelines/ReleasePublishNugetPipelineTests.cs index d7836b1262b..b847baeafc7 100644 --- a/tests/Infrastructure.Tests/Pipelines/ReleasePublishNugetPipelineTests.cs +++ b/tests/Infrastructure.Tests/Pipelines/ReleasePublishNugetPipelineTests.cs @@ -626,6 +626,32 @@ public async Task ExtensionReleaseDryRunInstructionsDoNotOverstatePublisherRoleV Assert.Contains("Separately confirm the service connection identity is a Contributor", workflow); } + [Fact] + public async Task WinGetSubmissionInstallsRequiredDotNetRuntime() + { + var template = await ReadRepoFileAsync("eng/pipelines/templates/publish-winget.yml"); + var runtimeInstallIndex = FindRequiredText(template, "- task: UseDotNet@2"); + var wingetCreateInstallIndex = FindRequiredText(template, "displayName: '🟣Install wingetcreate'"); + var runtimeInstall = template[runtimeInstallIndex..wingetCreateInstallIndex]; + + Assert.Contains("packageType: 'runtime'", runtimeInstall); + Assert.Contains("version: '9.0.x'", runtimeInstall); + Assert.Contains("eq('${{ parameters.dryRun }}', 'false')", runtimeInstall); + Assert.Contains("eq(variables['_IsProductionBranch'], 'true')", runtimeInstall); + } + + [Fact] + public async Task WinGetPreparationExercisesWingetCreate() + { + var template = await ReadRepoFileAsync("eng/pipelines/templates/prepare-winget-manifest.yml"); + + Assert.Contains("- task: UseDotNet@2", template); + Assert.Contains("packageType: 'runtime'", template); + Assert.Contains("version: '9.0.x'", template); + Assert.Contains("https://aka.ms/wingetcreate/latest", template); + Assert.Contains("wingetcreate.exe\" info", template); + } + [Fact] public async Task MarketplacePublishingDocumentationKeepsIdentityDetailsInternalAndRetiresPat() { From 198c9c9ff510fafd199f22975a8614de80eb75b7 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 19 Aug 2026 16:58:10 -0400 Subject: [PATCH 2/2] fix(release): exercise wingetcreate in dry runs The WinGet setup steps were gated by live production submission, so dry runs skipped both the .NET 9 runtime installation and the wingetcreate startup check. That prevented a safe branch run from proving that the latest wingetcreate executable could launch. Run UseDotNet and wingetcreate info whenever the WinGet job is selected, while retaining the live-only gate on URL validation and submission. This validates runtime compatibility without opening an upstream PR. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/templates/publish-winget.yml | 28 ++++++------------- eng/winget/README.md | 4 ++- .../ReleasePublishNugetPipelineTests.cs | 14 +++++++--- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/eng/pipelines/templates/publish-winget.yml b/eng/pipelines/templates/publish-winget.yml index 9c449b9d426..5b83419c4fb 100644 --- a/eng/pipelines/templates/publish-winget.yml +++ b/eng/pipelines/templates/publish-winget.yml @@ -181,24 +181,20 @@ steps: # Same gate as the installer-URL validation above and the Submit step below. condition: | and( - succeeded(), - eq('${{ parameters.dryRun }}', 'false'), - eq(variables['_IsProductionBranch'], 'true') + succeeded(), + eq('${{ parameters.dryRun }}', 'false'), + eq(variables['_IsProductionBranch'], 'true') ) # The latest standalone wingetcreate executable targets .NET 9 and is not - # self-contained, so the release agent must provide a compatible runtime. + # self-contained. Install its runtime whenever the WinGet job is selected so + # dry-run and non-production builds also verify that the CLI can start. - task: UseDotNet@2 displayName: '🟣Install .NET 9 runtime for wingetcreate' inputs: packageType: 'runtime' version: '9.0.x' - condition: | - and( - succeeded(), - eq('${{ parameters.dryRun }}', 'false'), - eq(variables['_IsProductionBranch'], 'true') - ) + condition: succeeded() - powershell: | $ErrorActionPreference = 'Stop' @@ -211,16 +207,8 @@ steps: } Write-Host "wingetcreate downloaded and verified successfully" displayName: '🟣Install wingetcreate' - # Skip when no Submit step will run. wingetcreate is only used to submit to - # the upstream winget-pkgs repo, so dry-run and non-production-branch builds - # gain nothing from downloading it and instead pick up an aka.ms-redirect - # failure surface for free. - condition: | - and( - succeeded(), - eq('${{ parameters.dryRun }}', 'false'), - eq(variables['_IsProductionBranch'], 'true') - ) + # This is side-effect free; the Submit step below retains the production gate. + condition: succeeded() - powershell: | $ErrorActionPreference = 'Stop' diff --git a/eng/winget/README.md b/eng/winget/README.md index 892a8c79c7c..2d5ddd178f8 100644 --- a/eng/winget/README.md +++ b/eng/winget/README.md @@ -59,7 +59,9 @@ The Azure DevOps prepare stage also installs the .NET 9 runtime, downloads the latest standalone `wingetcreate`, and runs `wingetcreate info`. This side-effect-free smoke catches runtime compatibility changes before the release pipeline attempts a public submission; it does not exercise credentials or -submission. +submission. The release WinGet job repeats the same startup check whenever that +job is selected, including dry-run and non-production-branch builds, while the +submission step remains limited to non-dry-run production builds. ## Validation model diff --git a/tests/Infrastructure.Tests/Pipelines/ReleasePublishNugetPipelineTests.cs b/tests/Infrastructure.Tests/Pipelines/ReleasePublishNugetPipelineTests.cs index b847baeafc7..ade200c2767 100644 --- a/tests/Infrastructure.Tests/Pipelines/ReleasePublishNugetPipelineTests.cs +++ b/tests/Infrastructure.Tests/Pipelines/ReleasePublishNugetPipelineTests.cs @@ -627,17 +627,23 @@ public async Task ExtensionReleaseDryRunInstructionsDoNotOverstatePublisherRoleV } [Fact] - public async Task WinGetSubmissionInstallsRequiredDotNetRuntime() + public async Task WinGetJobVerifiesWingetCreateBeforeConditionalSubmission() { var template = await ReadRepoFileAsync("eng/pipelines/templates/publish-winget.yml"); var runtimeInstallIndex = FindRequiredText(template, "- task: UseDotNet@2"); - var wingetCreateInstallIndex = FindRequiredText(template, "displayName: '🟣Install wingetcreate'"); + var wingetCreateInstallIndex = FindRequiredText(template, "Write-Host \"Downloading wingetcreate...\""); + var submitIndex = FindRequiredText(template, "Write-Host \"Submitting WinGet manifests"); var runtimeInstall = template[runtimeInstallIndex..wingetCreateInstallIndex]; + var wingetCreateInstall = template[wingetCreateInstallIndex..submitIndex]; + var submission = template[submitIndex..]; Assert.Contains("packageType: 'runtime'", runtimeInstall); Assert.Contains("version: '9.0.x'", runtimeInstall); - Assert.Contains("eq('${{ parameters.dryRun }}', 'false')", runtimeInstall); - Assert.Contains("eq(variables['_IsProductionBranch'], 'true')", runtimeInstall); + Assert.Contains("condition: succeeded()", runtimeInstall); + Assert.Contains("wingetcreate.exe\" info", wingetCreateInstall); + Assert.Contains("condition: succeeded()", wingetCreateInstall); + Assert.Contains("eq('${{ parameters.dryRun }}', 'false')", submission); + Assert.Contains("eq(variables['_IsProductionBranch'], 'true')", submission); } [Fact]