From bb4c548a4b209a0ff7e4b262ee51622dadc7c4ae Mon Sep 17 00:00:00 2001 From: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:27:21 +0800 Subject: [PATCH] Update BuildWithLatestWinAppSdkDaily pipeline (#45555) ## Summary of the Pull Request This pull request introduces an "artifact-based" mode for consuming the Windows App SDK in CI pipelines, allowing builds to use NuGet packages directly from Azure DevOps pipeline artifacts instead of public/internal feeds. This is achieved by adding new parameters and logic to pipeline YAML and PowerShell scripts, supporting scenarios where packages are not yet published to a feed. The changes also improve robustness when updating package versions and add documentation for authentication requirements. These changes make the pipeline more flexible and robust, enabling builds to consume unreleased or pre-release packages directly from CI artifacts, which is especially useful for testing and validation scenarios. ## PR Checklist - [ ] Closes: #xxx - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .github/actions/spell-check/expect.txt | 1 + .pipelines/UpdateVersions.ps1 | 460 ++++++++++++++++-- .../v2/ci-using-the-latest-winappsdk.yml | 26 + .pipelines/v2/templates/job-build-project.yml | 25 + .pipelines/v2/templates/pipeline-ci-build.yml | 25 + ...eps-update-winappsdk-and-restore-nuget.yml | 28 ++ 6 files changed, 511 insertions(+), 54 deletions(-) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 4a208a400a..02622eb731 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1072,6 +1072,7 @@ Metacharacter metadatamatters Metadatas metafile +metapackage mfc Mgmt Microwaved diff --git a/.pipelines/UpdateVersions.ps1 b/.pipelines/UpdateVersions.ps1 index 4e68663236..72b4f25cad 100644 --- a/.pipelines/UpdateVersions.ps1 +++ b/.pipelines/UpdateVersions.ps1 @@ -13,9 +13,36 @@ Param( # Root folder Path for processing [Parameter(Mandatory=$False,Position=4)] - [string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json" + [string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json", + + # Use Azure Pipeline artifact as source for metapackage + [Parameter(Mandatory=$False,Position=5)] + [boolean]$useArtifactSource = $False, + + # Azure DevOps organization URL + [Parameter(Mandatory=$False,Position=6)] + [string]$azureDevOpsOrg = "https://dev.azure.com/microsoft", + + # Azure DevOps project name + [Parameter(Mandatory=$False,Position=7)] + [string]$azureDevOpsProject = "ProjectReunion", + + # Pipeline build ID (or "latest" for latest build) + [Parameter(Mandatory=$False,Position=8)] + [string]$buildId = "", + + # Artifact name containing the NuGet packages + [Parameter(Mandatory=$False,Position=9)] + [string]$artifactName = "WindowsAppSDK_Nuget_And_MSIX", + + # Metapackage name to look for in artifact + [Parameter(Mandatory=$False,Position=10)] + [string]$metaPackageName = "Microsoft.WindowsAppSDK" ) +# Script-level constants +$script:PackageVersionRegex = '^(.+?)\.(\d+\..*)$' + function Read-FileWithEncoding { @@ -57,7 +84,7 @@ function Add-NuGetSourceAndMapping { # Ensure packageSources exists if (-not $Xml.configuration.packageSources) { - $Xml.configuration.AppendChild($Xml.CreateElement("packageSources")) | Out-Null + $null = $Xml.configuration.AppendChild($Xml.CreateElement("packageSources")) } $sources = $Xml.configuration.packageSources @@ -66,13 +93,13 @@ function Add-NuGetSourceAndMapping { if (-not $sourceNode) { $sourceNode = $Xml.CreateElement("add") $sourceNode.SetAttribute("key", $Key) - $sources.AppendChild($sourceNode) | Out-Null + $null = $sources.AppendChild($sourceNode) } $sourceNode.SetAttribute("value", $Value) # Ensure packageSourceMapping exists if (-not $Xml.configuration.packageSourceMapping) { - $Xml.configuration.AppendChild($Xml.CreateElement("packageSourceMapping")) | Out-Null + $null = $Xml.configuration.AppendChild($Xml.CreateElement("packageSourceMapping")) } $mapping = $Xml.configuration.packageSourceMapping @@ -80,7 +107,7 @@ function Add-NuGetSourceAndMapping { $invalidNodes = $mapping.SelectNodes("packageSource[not(@key) or @key='']") if ($invalidNodes) { foreach ($node in $invalidNodes) { - $mapping.RemoveChild($node) | Out-Null + $null = $mapping.RemoveChild($node) } } @@ -91,9 +118,9 @@ function Add-NuGetSourceAndMapping { $mappingSource.SetAttribute("key", $Key) # Insert at top for priority if ($mapping.HasChildNodes) { - $mapping.InsertBefore($mappingSource, $mapping.FirstChild) | Out-Null + $null = $mapping.InsertBefore($mappingSource, $mapping.FirstChild) } else { - $mapping.AppendChild($mappingSource) | Out-Null + $null = $mapping.AppendChild($mappingSource) } } @@ -110,14 +137,273 @@ function Add-NuGetSourceAndMapping { foreach ($pattern in $Patterns) { $pkg = $Xml.CreateElement("package") $pkg.SetAttribute("pattern", $pattern) - $mappingSource.AppendChild($pkg) | Out-Null + $null = $mappingSource.AppendChild($pkg) } } +function Download-ArtifactFromPipeline { + param ( + [string]$Organization, + [string]$Project, + [string]$BuildId, + [string]$ArtifactName, + [string]$OutputDir + ) + + Write-Host "Downloading artifact '$ArtifactName' from build $BuildId..." + $null = New-Item -ItemType Directory -Path $OutputDir -Force + + try { + # Authenticate with Azure DevOps using System Access Token (if available) + if ($env:SYSTEM_ACCESSTOKEN) { + Write-Host "Authenticating with Azure DevOps using System Access Token..." + $env:AZURE_DEVOPS_EXT_PAT = $env:SYSTEM_ACCESSTOKEN + } else { + Write-Host "No SYSTEM_ACCESSTOKEN found, assuming az CLI is already authenticated..." + } + + # Use az CLI to download artifact + & az pipelines runs artifact download ` + --organization $Organization ` + --project $Project ` + --run-id $BuildId ` + --artifact-name $ArtifactName ` + --path $OutputDir + + if ($LASTEXITCODE -eq 0) { + Write-Host "Successfully downloaded artifact to $OutputDir" + return $true + } else { + Write-Warning "Failed to download artifact. Exit code: $LASTEXITCODE" + return $false + } + } catch { + Write-Warning "Error downloading artifact: $_" + return $false + } +} + +function Get-NuspecDependencies { + param ( + [string]$NupkgPath, + [string]$TargetFramework = "" + ) + + $tempDir = Join-Path $env:TEMP "nuspec_parse_$(Get-Random)" + + try { + # Extract .nupkg (it's a zip file) + # Workaround: Expand-Archive may not recognize .nupkg extension, so copy to .zip first + $tempZip = Join-Path $env:TEMP "temp_$(Get-Random).zip" + Copy-Item $NupkgPath -Destination $tempZip -Force + Expand-Archive -Path $tempZip -DestinationPath $tempDir -Force + Remove-Item $tempZip -Force -ErrorAction SilentlyContinue + + # Find .nuspec file + $nuspecFile = Get-ChildItem -Path $tempDir -Filter "*.nuspec" -Recurse | Select-Object -First 1 + + if (-not $nuspecFile) { + Write-Warning "No .nuspec file found in $NupkgPath" + return @{} + } + + [xml]$nuspec = Get-Content $nuspecFile.FullName + + # Extract package info + $packageId = $nuspec.package.metadata.id + $version = $nuspec.package.metadata.version + Write-Host "Parsing $packageId version $version" + + # Parse dependencies + $dependencies = @{} + $depGroups = $nuspec.package.metadata.dependencies.group + + if ($depGroups) { + # Dependencies are grouped by target framework + foreach ($group in $depGroups) { + $fx = $group.targetFramework + Write-Host " Target Framework: $fx" + + foreach ($dep in $group.dependency) { + $depId = $dep.id + $depVer = $dep.version + # Remove version range brackets if present (e.g., "[2.0.0]" -> "2.0.0") + $depVer = $depVer -replace '[\[\]]', '' + $dependencies[$depId] = $depVer + Write-Host " - ${depId} : ${depVer}" + } + } + } else { + # No grouping, direct dependencies + $deps = $nuspec.package.metadata.dependencies.dependency + if ($deps) { + foreach ($dep in $deps) { + $depId = $dep.id + $depVer = $dep.version + $depVer = $depVer -replace '[\[\]]', '' + $dependencies[$depId] = $depVer + Write-Host " - ${depId} : ${depVer}" + } + } + } + + return $dependencies + } + catch { + Write-Warning "Failed to parse nuspec: $_" + return @{} + } + finally { + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} + +function Resolve-ArtifactBasedDependencies { + param ( + [string]$ArtifactDir, + [string]$MetaPackageName, + [string]$SourceUrl, + [string]$OutputDir + ) + + Write-Host "Resolving dependencies from artifact-based metapackage..." + $null = New-Item -ItemType Directory -Path $OutputDir -Force + + # Find the metapackage in artifact + $metaNupkg = Get-ChildItem -Path $ArtifactDir -Recurse -Filter "$MetaPackageName.*.nupkg" | + Where-Object { $_.Name -notmatch "Runtime" } | + Select-Object -First 1 + + if (-not $metaNupkg) { + Write-Warning "Metapackage $MetaPackageName not found in artifact" + return @{} + } + + # Extract version from filename + if ($metaNupkg.Name -match "$MetaPackageName\.(.+)\.nupkg") { + $metaVersion = $Matches[1] + Write-Host "Found metapackage: $MetaPackageName version $metaVersion" + } else { + Write-Warning "Could not extract version from $($metaNupkg.Name)" + return @{} + } + + # Parse dependencies from metapackage + $dependencies = Get-NuspecDependencies -NupkgPath $metaNupkg.FullName + + # Copy metapackage to output directory + Copy-Item $metaNupkg.FullName -Destination $OutputDir -Force + Write-Host "Copied metapackage to $OutputDir" + + # Prepare package versions hashtable - initialize with metapackage version + $packageVersions = @{ $MetaPackageName = $metaVersion } + + # Copy Runtime package from artifact (it's not in feed) and extract its version + $runtimeNupkg = Get-ChildItem -Path $ArtifactDir -Recurse -Filter "$MetaPackageName.Runtime.*.nupkg" | Select-Object -First 1 + if ($runtimeNupkg) { + Copy-Item $runtimeNupkg.FullName -Destination $OutputDir -Force + Write-Host "Copied Runtime package to $OutputDir" + + # Extract version from Runtime package filename + if ($runtimeNupkg.Name -match "$MetaPackageName\.Runtime\.(.+)\.nupkg") { + $runtimeVersion = $Matches[1] + $packageVersions["$MetaPackageName.Runtime"] = $runtimeVersion + Write-Host "Extracted Runtime package version: $runtimeVersion" + } else { + Write-Warning "Could not extract version from Runtime package: $($runtimeNupkg.Name)" + } + } + + # Download other dependencies from feed (excluding Runtime as it's already copied) + # Create temp nuget.config that includes both local packages and remote feed + # This allows NuGet to find packages already copied from artifact + $tempConfig = Join-Path $env:TEMP "nuget_artifact_$(Get-Random).config" + $tempConfigContent = @" + + + + + + + + +"@ + Set-Content -Path $tempConfig -Value $tempConfigContent + + try { + foreach ($depId in $dependencies.Keys) { + # Skip Runtime as it's already copied from artifact + if ($depId -like "*Runtime*") { + # Don't overwrite the version we extracted from the Runtime package filename + if (-not $packageVersions.ContainsKey($depId)) { + $packageVersions[$depId] = $dependencies[$depId] + } + Write-Host "Skipping $depId (already in artifact)" + continue + } + + $depVersion = $dependencies[$depId] + Write-Host "Downloading dependency: $depId version $depVersion from feed..." + + & nuget install $depId ` + -Version $depVersion ` + -ConfigFile $tempConfig ` + -OutputDirectory $OutputDir ` + -NonInteractive ` + -NoCache ` + | Out-Null + + if ($LASTEXITCODE -eq 0) { + $packageVersions[$depId] = $depVersion + Write-Host " Successfully downloaded $depId" + } else { + Write-Warning " Failed to download $depId version $depVersion" + } + } + } + finally { + Remove-Item $tempConfig -Force -ErrorAction SilentlyContinue + } + + # Parse all downloaded packages to get actual versions + $directories = Get-ChildItem -Path $OutputDir -Directory + $allLocalPackages = @() + + # Add metapackage and runtime to the list (they are .nupkg files, not directories) + $allLocalPackages += $MetaPackageName + if ($packageVersions.ContainsKey("$MetaPackageName.Runtime")) { + $allLocalPackages += "$MetaPackageName.Runtime" + } + + foreach ($dir in $directories) { + if ($dir.Name -match $script:PackageVersionRegex) { + $pkgId = $Matches[1] + $pkgVer = $Matches[2] + $allLocalPackages += $pkgId + if (-not $packageVersions.ContainsKey($pkgId)) { + $packageVersions[$pkgId] = $pkgVer + } + } + } + + # Update nuget.config dynamically during pipeline execution + # This modification is temporary and won't be committed back to the repo + $nugetConfig = Join-Path $rootPath "nuget.config" + $configData = Read-FileWithEncoding -Path $nugetConfig + [xml]$xml = $configData.Content + + Add-NuGetSourceAndMapping -Xml $xml -Key "localpackages" -Value $OutputDir -Patterns $allLocalPackages + + $xml.Save($nugetConfig) + Write-Host "Updated nuget.config with localpackages mapping (temporary, for pipeline execution only)." + + return ,$packageVersions +} + function Resolve-WinAppSdkSplitDependencies { Write-Host "Version $WinAppSDKVersion detected. Resolving split dependencies..." $installDir = Join-Path $rootPath "localpackages\output" - New-Item -ItemType Directory -Path $installDir -Force | Out-Null + $null = New-Item -ItemType Directory -Path $installDir -Force # Create a temporary nuget.config to avoid interference from the repo's config $tempConfig = Join-Path $env:TEMP "nuget_$(Get-Random).config" @@ -131,14 +417,24 @@ function Resolve-WinAppSdkSplitDependencies { if ($propsContent -match '" $oldVersionString = "" diff --git a/.pipelines/v2/ci-using-the-latest-winappsdk.yml b/.pipelines/v2/ci-using-the-latest-winappsdk.yml index 16639f44c0..bb70ad277a 100644 --- a/.pipelines/v2/ci-using-the-latest-winappsdk.yml +++ b/.pipelines/v2/ci-using-the-latest-winappsdk.yml @@ -1,3 +1,8 @@ +# NOTE: When using artifact mode (useArtifactSource: true), the pipeline needs +# permission to access System.AccessToken. This is automatically handled by the +# script if SYSTEM_ACCESSTOKEN environment variable is available. +# If you encounter authentication errors, ensure the job has oauth access enabled. + trigger: none pr: none schedules: @@ -37,6 +42,23 @@ parameters: - name: useExperimentalVersion type: boolean default: false + # Artifact mode parameters (optional) + - name: useArtifactSource + type: boolean + displayName: "Use Artifact Source (instead of feed)" + default: false + - name: buildId + type: string + displayName: "Windows App SDK Build ID (required only if using artifact source)" + default: 'N/A' + - name: azureDevOpsProject + type: string + displayName: "Source Project (for artifact mode, default: ProjectReunion)" + default: 'ProjectReunion' + - name: artifactName + type: string + displayName: "Artifact Name (for artifact mode, default: WindowsAppSDK_Nuget_And_MSIX)" + default: 'WindowsAppSDK_Nuget_And_MSIX' extends: template: templates/pipeline-ci-build.yml @@ -49,3 +71,7 @@ extends: useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }} winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }} useExperimentalVersion: ${{ parameters.useExperimentalVersion }} + useArtifactSource: ${{ parameters.useArtifactSource }} + buildId: ${{ parameters.buildId }} + azureDevOpsProject: ${{ parameters.azureDevOpsProject }} + artifactName: ${{ parameters.artifactName }} diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml index e41bfbc0ad..c5cd2ffd0c 100644 --- a/.pipelines/v2/templates/job-build-project.yml +++ b/.pipelines/v2/templates/job-build-project.yml @@ -74,6 +74,25 @@ parameters: - name: useExperimentalVersion type: boolean default: false + # Artifact mode parameters + - name: useArtifactSource + type: boolean + default: false + - name: azureDevOpsOrg + type: string + default: 'https://dev.azure.com/microsoft' + - name: azureDevOpsProject + type: string + default: 'ProjectReunion' + - name: buildId + type: string + default: '' + - name: artifactName + type: string + default: 'WindowsAppSDK_Nuget_And_MSIX' + - name: metaPackageName + type: string + default: 'Microsoft.WindowsAppSDK' - name: csProjectsToPublish type: object default: @@ -226,6 +245,12 @@ jobs: parameters: versionNumber: ${{ parameters.winAppSDKVersionNumber }} useExperimentalVersion: ${{ parameters.useExperimentalVersion }} + useArtifactSource: ${{ parameters.useArtifactSource }} + azureDevOpsOrg: ${{ parameters.azureDevOpsOrg }} + azureDevOpsProject: ${{ parameters.azureDevOpsProject }} + buildId: ${{ parameters.buildId }} + artifactName: ${{ parameters.artifactName }} + metaPackageName: ${{ parameters.metaPackageName }} - ${{ if eq(parameters.useLatestWinAppSDK, false)}}: - template: .\steps-restore-nuget.yml diff --git a/.pipelines/v2/templates/pipeline-ci-build.yml b/.pipelines/v2/templates/pipeline-ci-build.yml index a56c575399..f4ac4e161f 100644 --- a/.pipelines/v2/templates/pipeline-ci-build.yml +++ b/.pipelines/v2/templates/pipeline-ci-build.yml @@ -34,6 +34,25 @@ parameters: - name: useExperimentalVersion type: boolean default: false + # Artifact mode parameters + - name: useArtifactSource + type: boolean + default: false + - name: azureDevOpsOrg + type: string + default: 'https://dev.azure.com/microsoft' + - name: azureDevOpsProject + type: string + default: 'ProjectReunion' + - name: buildId + type: string + default: '' + - name: artifactName + type: string + default: 'WindowsAppSDK_Nuget_And_MSIX' + - name: metaPackageName + type: string + default: 'Microsoft.WindowsAppSDK' stages: - ${{ each platform in parameters.buildPlatforms }}: @@ -65,6 +84,12 @@ stages: ${{ if eq(parameters.useLatestWinAppSDK, true) }}: winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }} useExperimentalVersion: ${{ parameters.useExperimentalVersion }} + useArtifactSource: ${{ parameters.useArtifactSource }} + azureDevOpsOrg: ${{ parameters.azureDevOpsOrg }} + azureDevOpsProject: ${{ parameters.azureDevOpsProject }} + buildId: ${{ parameters.buildId }} + artifactName: ${{ parameters.artifactName }} + metaPackageName: ${{ parameters.metaPackageName }} timeoutInMinutes: 90 - stage: Build_SDK diff --git a/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml b/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml index 566c8045c4..458faf25be 100644 --- a/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml +++ b/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml @@ -5,6 +5,25 @@ parameters: - name: useExperimentalVersion type: boolean default: false + # Artifact mode parameters + - name: useArtifactSource + type: boolean + default: false + - name: azureDevOpsOrg + type: string + default: 'https://dev.azure.com/microsoft' + - name: azureDevOpsProject + type: string + default: 'ProjectReunion' + - name: buildId + type: string + default: '' + - name: artifactName + type: string + default: 'WindowsAppSDK_Nuget_And_MSIX' + - name: metaPackageName + type: string + default: 'Microsoft.WindowsAppSDK' steps: - task: NuGetAuthenticate@1 @@ -12,12 +31,20 @@ steps: - task: PowerShell@2 displayName: Update WinAppSDK Versions + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) inputs: filePath: '$(build.sourcesdirectory)\.pipelines\UpdateVersions.ps1' arguments: > -winAppSdkVersionNumber ${{ parameters.versionNumber }} -useExperimentalVersion $${{ parameters.useExperimentalVersion }} -rootPath "$(build.sourcesdirectory)" + -useArtifactSource $${{ parameters.useArtifactSource }} + -azureDevOpsOrg "${{ parameters.azureDevOpsOrg }}" + -azureDevOpsProject "${{ parameters.azureDevOpsProject }}" + -buildId "${{ parameters.buildId }}" + -artifactName "${{ parameters.artifactName }}" + -metaPackageName "${{ parameters.metaPackageName }}" # - task: NuGetCommand@2 # displayName: 'Restore NuGet packages (slnx)' @@ -36,3 +63,4 @@ steps: feedsToUse: 'config' nugetConfigPath: '$(build.sourcesdirectory)\nuget.config' workingDirectory: '$(build.sourcesdirectory)' + arguments: '/p:NoWarn=NU1602,NU1604'