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..5b83419c4fb 100644 --- a/eng/pipelines/templates/publish-winget.yml +++ b/eng/pipelines/templates/publish-winget.yml @@ -186,22 +186,29 @@ steps: eq(variables['_IsProductionBranch'], 'true') ) + # The latest standalone wingetcreate executable targets .NET 9 and is not + # 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: succeeded() + - 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 - # 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 b3539d11d21..2d5ddd178f8 100644 --- a/eng/winget/README.md +++ b/eng/winget/README.md @@ -55,6 +55,13 @@ 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. 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 6f5421106ee..68c95dde7bf 100644 --- a/tests/Infrastructure.Tests/Pipelines/ReleasePublishNugetPipelineTests.cs +++ b/tests/Infrastructure.Tests/Pipelines/ReleasePublishNugetPipelineTests.cs @@ -597,6 +597,38 @@ public async Task ExtensionReleaseDryRunInstructionsDoNotOverstatePublisherRoleV Assert.Contains("Separately confirm the service connection identity is a Contributor", workflow); } + [Fact] + 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, "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("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] + 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() {