From 56fc4511ebd129c3e29eeb7105dbc22f3d78acf6 Mon Sep 17 00:00:00 2001 From: Scott Jones Date: Tue, 25 Aug 2026 15:12:14 -0400 Subject: [PATCH 1/2] Hydrate the triggering Samples branch Derive the package-closure target from Build.SourceBranch so main and release CI runs hydrate their matching protected branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26f0def2-a93d-4d41-a610-c6c6fa760d7e --- SamplesFeed-Hydration.yml | 12 ++++++------ eng/README.md | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/SamplesFeed-Hydration.yml b/SamplesFeed-Hydration.yml index 573812be0..e9ac06a7a 100644 --- a/SamplesFeed-Hydration.yml +++ b/SamplesFeed-Hydration.yml @@ -15,8 +15,8 @@ trigger: pr: none variables: - # Define SamplesBranch as a queue-time variable to override this main fallback. - EffectiveSamplesBranch: $[coalesce(variables.SamplesBranch, 'main')] + # Hydrate the same protected branch whose update triggered this run. + EffectiveSamplesBranch: $[replace(variables['Build.SourceBranch'], 'refs/heads/', '')] SamplesCheckoutDirectory: '$(Pipeline.Workspace)\samples-to-hydrate' SamplesFeedConfig: '$(Build.SourcesDirectory)\eng\SamplesFeed.nuget.config' @@ -35,9 +35,9 @@ jobs: inputs: targetType: inline script: | - if ($env:SAMPLES_BRANCH -notmatch '^(main|release/[A-Za-z0-9][A-Za-z0-9._-]*|copilot/samples-feed-hydration-bootstrap)$') + if ($env:SAMPLES_BRANCH -notmatch '^(main|release/[A-Za-z0-9][A-Za-z0-9._-]*)$') { - throw "SamplesBranch must be 'main', a protected 'release/*' branch, or the trusted bootstrap branch; received '$env:SAMPLES_BRANCH'." + throw "The triggering branch must be 'main' or a protected 'release/*' branch; received '$env:SAMPLES_BRANCH'." } git clone --branch $env:SAMPLES_BRANCH --depth 1 --single-branch ` @@ -103,9 +103,9 @@ jobs: inputs: targetType: inline script: | - if ($env:SAMPLES_BRANCH -notmatch '^(main|release/[A-Za-z0-9][A-Za-z0-9._-]*|copilot/samples-feed-hydration-bootstrap)$') + if ($env:SAMPLES_BRANCH -notmatch '^(main|release/[A-Za-z0-9][A-Za-z0-9._-]*)$') { - throw "SamplesBranch must be 'main', a protected 'release/*' branch, or the trusted bootstrap branch; received '$env:SAMPLES_BRANCH'." + throw "The triggering branch must be 'main' or a protected 'release/*' branch; received '$env:SAMPLES_BRANCH'." } git clone --branch $env:SAMPLES_BRANCH --depth 1 --single-branch ` diff --git a/eng/README.md b/eng/README.md index 7bbc43eb3..cffd60736 100644 --- a/eng/README.md +++ b/eng/README.md @@ -6,7 +6,7 @@ Create the pipeline in the public `shine-oss/WinAppSDK-Samples` Azure DevOps project and point it at this repository and YAML file. Configure the pipeline with a maximum of one concurrent run because the feed upstream setting is shared state. -Create a pipeline variable named `SamplesBranch`, allow users to override it at queue time, and give it a default value of `main`. It selects the branch whose complete dependency closure is hydrated; set it to the matching `release/x.0-stable` or `release/x.0-experimental` branch when hydrating a release branch. The YAML also falls back to `main` when the variable is absent and rejects branches outside protected `main` and `release/*`. The pipeline clones that branch separately and uses `eng/SamplesFeed.nuget.config`, so it can populate the feed before the selected branch switches its checked-in `Samples/nuget.config` to the single-source configuration. +The pipeline derives the branch to hydrate from `Build.SourceBranch`. Updates to `main` hydrate `main`, while updates to a matching `release/*` branch hydrate that release branch. Branches outside protected `main` and `release/*` are rejected. The pipeline clones the triggering branch separately and uses `eng/SamplesFeed.nuget.config`. The `WinAppSDK-Samples Build Service (shine-oss)` identity needs: @@ -15,11 +15,11 @@ The `WinAppSDK-Samples Build Service (shine-oss)` identity needs: - the feed `Collaborator` capability to save packages from the upstream. Do not enable pull-request triggers for the hydration job. It receives `System.AccessToken`, so it must only execute scripts from protected branches. Pull requests continue to use the existing sample build pipelines; package-version changes become anonymously available after the protected-branch hydration run completes. -Restrict permission to queue the pipeline and override `SamplesBranch` to trusted maintainers. +Restrict permission to queue the pipeline to trusted maintainers. ## Flow -1. Clone the branch selected by `SamplesBranch`. +1. Clone the protected branch whose update triggered the pipeline. 2. Enable NuGet Gallery as the feed upstream. 3. Restore all sample solutions and explicit CMake NuGet dependencies through the single-source hydration config with the authenticated build identity. 4. Disable the upstream in an `always()` cleanup step. From 5d0fa2f21120b22b40bc72dc8aba09e80ab9b4b6 Mon Sep 17 00:00:00 2001 From: Scott Jones Date: Tue, 25 Aug 2026 15:24:57 -0400 Subject: [PATCH 2/2] Add selectable feed hydration modes Support automatic or selected-branch hydration, anonymous validation-only runs, and explicit Microsoft.WindowsAppSDK version mirroring with dependency closure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26f0def2-a93d-4d41-a610-c6c6fa760d7e --- SamplesFeed-Hydration.yml | 275 +++++++++++++++++----------- eng/README.md | 19 +- eng/RestoreWindowsAppSDKPackage.ps1 | 37 ++++ 3 files changed, 217 insertions(+), 114 deletions(-) create mode 100644 eng/RestoreWindowsAppSDKPackage.ps1 diff --git a/SamplesFeed-Hydration.yml b/SamplesFeed-Hydration.yml index e9ac06a7a..2645b9442 100644 --- a/SamplesFeed-Hydration.yml +++ b/SamplesFeed-Hydration.yml @@ -1,3 +1,21 @@ +parameters: +- name: HydrationMode + displayName: Hydration mode + type: string + default: Branch + values: + - Branch + - ValidateOnly + - WindowsAppSDKVersion +- name: SamplesBranch + displayName: Samples branch (manual Branch or ValidateOnly mode; blank uses the triggering branch) + type: string + default: '' +- name: WindowsAppSDKVersion + displayName: Microsoft.WindowsAppSDK version (WindowsAppSDKVersion mode only) + type: string + default: '' + trigger: batch: true branches: @@ -9,14 +27,17 @@ trigger: - Samples/** - SamplesFeed-Hydration.yml - eng/RestorePublicSamplesPackages.ps1 + - eng/RestoreWindowsAppSDKPackage.ps1 - eng/SamplesFeed.nuget.config - eng/SetSamplesFeedUpstream.ps1 pr: none variables: - # Hydrate the same protected branch whose update triggered this run. - EffectiveSamplesBranch: $[replace(variables['Build.SourceBranch'], 'refs/heads/', '')] + ${{ if ne(parameters.SamplesBranch, '') }}: + EffectiveSamplesBranch: ${{ parameters.SamplesBranch }} + ${{ else }}: + EffectiveSamplesBranch: $[replace(variables['Build.SourceBranch'], 'refs/heads/', '')] SamplesCheckoutDirectory: '$(Pipeline.Workspace)\samples-to-hydrate' SamplesFeedConfig: '$(Build.SourcesDirectory)\eng\SamplesFeed.nuget.config' @@ -24,117 +45,161 @@ pool: vmImage: windows-2022 jobs: -- job: Hydrate - displayName: Hydrate WinAppSDK-SampleDeps - timeoutInMinutes: 120 - steps: - - checkout: self - - - task: PowerShell@2 - displayName: Checkout selected Samples branch - inputs: - targetType: inline - script: | - if ($env:SAMPLES_BRANCH -notmatch '^(main|release/[A-Za-z0-9][A-Za-z0-9._-]*)$') - { - throw "The triggering branch must be 'main' or a protected 'release/*' branch; received '$env:SAMPLES_BRANCH'." - } - - git clone --branch $env:SAMPLES_BRANCH --depth 1 --single-branch ` - https://github.com/microsoft/WindowsAppSDK-Samples.git ` - $env:SAMPLES_CHECKOUT_DIRECTORY - - if ($LASTEXITCODE -ne 0) - { - throw "Failed to check out Samples branch '$env:SAMPLES_BRANCH'." - } - env: - SAMPLES_BRANCH: $(EffectiveSamplesBranch) - SAMPLES_CHECKOUT_DIRECTORY: $(SamplesCheckoutDirectory) - - - task: NuGetToolInstaller@1 - displayName: Use NuGet - - - task: UseDotNet@2 - displayName: Install sample .NET SDKs - inputs: - packageType: sdk - useGlobalJson: true - workingDirectory: '$(SamplesCheckoutDirectory)\Samples' - - - task: NuGetAuthenticate@1 - displayName: Authenticate to WinAppSDK-SampleDeps - - - task: PowerShell@2 - displayName: Enable NuGet Gallery upstream - inputs: - filePath: '$(Build.SourcesDirectory)\eng\SetSamplesFeedUpstream.ps1' - arguments: -Action Enable - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - - - task: PowerShell@2 - displayName: Hydrate package closure - inputs: - filePath: '$(Build.SourcesDirectory)\eng\RestorePublicSamplesPackages.ps1' - arguments: > - -SamplesRoot "$(SamplesCheckoutDirectory)\Samples" - -NuGetConfigPath "$(SamplesFeedConfig)" - -PackagesDirectory "$(Pipeline.Workspace)\hydration-packages" - - - task: PowerShell@2 - displayName: Disable NuGet Gallery upstream - condition: always() - inputs: - filePath: '$(Build.SourcesDirectory)\eng\SetSamplesFeedUpstream.ps1' - arguments: -Action Disable - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) +- ${{ if ne(parameters.HydrationMode, 'ValidateOnly') }}: + - job: Hydrate + displayName: Hydrate WinAppSDK-SampleDeps + timeoutInMinutes: 120 + steps: + - checkout: self + + - ${{ if eq(parameters.HydrationMode, 'Branch') }}: + - task: PowerShell@2 + displayName: Checkout selected Samples branch + inputs: + targetType: inline + script: | + if ($env:SAMPLES_BRANCH -notmatch '^(main|release/[A-Za-z0-9][A-Za-z0-9._-]*)$') + { + throw "The selected branch must be 'main' or a protected 'release/*' branch; received '$env:SAMPLES_BRANCH'." + } + + git clone --branch $env:SAMPLES_BRANCH --depth 1 --single-branch ` + https://github.com/microsoft/WindowsAppSDK-Samples.git ` + $env:SAMPLES_CHECKOUT_DIRECTORY + + if ($LASTEXITCODE -ne 0) + { + throw "Failed to check out Samples branch '$env:SAMPLES_BRANCH'." + } + env: + SAMPLES_BRANCH: $(EffectiveSamplesBranch) + SAMPLES_CHECKOUT_DIRECTORY: $(SamplesCheckoutDirectory) + + - task: NuGetToolInstaller@1 + displayName: Use NuGet + + - ${{ if eq(parameters.HydrationMode, 'Branch') }}: + - task: UseDotNet@2 + displayName: Install sample .NET SDKs + inputs: + packageType: sdk + useGlobalJson: true + workingDirectory: '$(SamplesCheckoutDirectory)\Samples' + + - task: NuGetAuthenticate@1 + displayName: Authenticate to WinAppSDK-SampleDeps + + - ${{ if eq(parameters.HydrationMode, 'WindowsAppSDKVersion') }}: + - task: PowerShell@2 + displayName: Validate Microsoft.WindowsAppSDK version + inputs: + targetType: inline + script: | + if ([string]::IsNullOrWhiteSpace($env:WINDOWS_APP_SDK_VERSION) -or + $env:WINDOWS_APP_SDK_VERSION -notmatch '^[0-9A-Za-z][0-9A-Za-z.+-]*$') + { + throw "WindowsAppSDKVersion must be a valid non-empty NuGet version." + } + env: + WINDOWS_APP_SDK_VERSION: ${{ parameters.WindowsAppSDKVersion }} + + - task: PowerShell@2 + displayName: Enable NuGet Gallery upstream + inputs: + filePath: '$(Build.SourcesDirectory)\eng\SetSamplesFeedUpstream.ps1' + arguments: -Action Enable + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - ${{ if eq(parameters.HydrationMode, 'Branch') }}: + - task: PowerShell@2 + displayName: Hydrate branch package closure + inputs: + filePath: '$(Build.SourcesDirectory)\eng\RestorePublicSamplesPackages.ps1' + arguments: > + -SamplesRoot "$(SamplesCheckoutDirectory)\Samples" + -NuGetConfigPath "$(SamplesFeedConfig)" + -PackagesDirectory "$(Pipeline.Workspace)\hydration-packages" + + - ${{ if eq(parameters.HydrationMode, 'WindowsAppSDKVersion') }}: + - task: PowerShell@2 + displayName: Hydrate Microsoft.WindowsAppSDK package closure + inputs: + filePath: '$(Build.SourcesDirectory)\eng\RestoreWindowsAppSDKPackage.ps1' + arguments: > + -NuGetConfigPath "$(SamplesFeedConfig)" + -PackagesDirectory "$(Pipeline.Workspace)\hydration-packages" + env: + WINDOWS_APP_SDK_VERSION: ${{ parameters.WindowsAppSDKVersion }} + + - task: PowerShell@2 + displayName: Disable NuGet Gallery upstream + condition: always() + inputs: + filePath: '$(Build.SourcesDirectory)\eng\SetSamplesFeedUpstream.ps1' + arguments: -Action Disable + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - job: ValidateAnonymousRestore displayName: Validate anonymous public restore - dependsOn: Hydrate + ${{ if ne(parameters.HydrationMode, 'ValidateOnly') }}: + dependsOn: Hydrate timeoutInMinutes: 120 steps: - checkout: self - - task: PowerShell@2 - displayName: Checkout selected Samples branch - inputs: - targetType: inline - script: | - if ($env:SAMPLES_BRANCH -notmatch '^(main|release/[A-Za-z0-9][A-Za-z0-9._-]*)$') - { - throw "The triggering branch must be 'main' or a protected 'release/*' branch; received '$env:SAMPLES_BRANCH'." - } - - git clone --branch $env:SAMPLES_BRANCH --depth 1 --single-branch ` - https://github.com/microsoft/WindowsAppSDK-Samples.git ` - $env:SAMPLES_CHECKOUT_DIRECTORY - - if ($LASTEXITCODE -ne 0) - { - throw "Failed to check out Samples branch '$env:SAMPLES_BRANCH'." - } - env: - SAMPLES_BRANCH: $(EffectiveSamplesBranch) - SAMPLES_CHECKOUT_DIRECTORY: $(SamplesCheckoutDirectory) + - ${{ if ne(parameters.HydrationMode, 'WindowsAppSDKVersion') }}: + - task: PowerShell@2 + displayName: Checkout selected Samples branch + inputs: + targetType: inline + script: | + if ($env:SAMPLES_BRANCH -notmatch '^(main|release/[A-Za-z0-9][A-Za-z0-9._-]*)$') + { + throw "The selected branch must be 'main' or a protected 'release/*' branch; received '$env:SAMPLES_BRANCH'." + } + + git clone --branch $env:SAMPLES_BRANCH --depth 1 --single-branch ` + https://github.com/microsoft/WindowsAppSDK-Samples.git ` + $env:SAMPLES_CHECKOUT_DIRECTORY + + if ($LASTEXITCODE -ne 0) + { + throw "Failed to check out Samples branch '$env:SAMPLES_BRANCH'." + } + env: + SAMPLES_BRANCH: $(EffectiveSamplesBranch) + SAMPLES_CHECKOUT_DIRECTORY: $(SamplesCheckoutDirectory) - task: NuGetToolInstaller@1 displayName: Use NuGet - - task: UseDotNet@2 - displayName: Install sample .NET SDKs - inputs: - packageType: sdk - useGlobalJson: true - workingDirectory: '$(SamplesCheckoutDirectory)\Samples' - - - task: PowerShell@2 - displayName: Restore without credentials - inputs: - filePath: '$(Build.SourcesDirectory)\eng\RestorePublicSamplesPackages.ps1' - arguments: > - -SamplesRoot "$(SamplesCheckoutDirectory)\Samples" - -NuGetConfigPath "$(SamplesFeedConfig)" - -PackagesDirectory "$(Pipeline.Workspace)\anonymous-packages" + - ${{ if ne(parameters.HydrationMode, 'WindowsAppSDKVersion') }}: + - task: UseDotNet@2 + displayName: Install sample .NET SDKs + inputs: + packageType: sdk + useGlobalJson: true + workingDirectory: '$(SamplesCheckoutDirectory)\Samples' + + - ${{ if ne(parameters.HydrationMode, 'WindowsAppSDKVersion') }}: + - task: PowerShell@2 + displayName: Restore branch without credentials + inputs: + filePath: '$(Build.SourcesDirectory)\eng\RestorePublicSamplesPackages.ps1' + arguments: > + -SamplesRoot "$(SamplesCheckoutDirectory)\Samples" + -NuGetConfigPath "$(SamplesFeedConfig)" + -PackagesDirectory "$(Pipeline.Workspace)\anonymous-packages" + + - ${{ if eq(parameters.HydrationMode, 'WindowsAppSDKVersion') }}: + - task: PowerShell@2 + displayName: Restore Microsoft.WindowsAppSDK without credentials + inputs: + filePath: '$(Build.SourcesDirectory)\eng\RestoreWindowsAppSDKPackage.ps1' + arguments: > + -NuGetConfigPath "$(SamplesFeedConfig)" + -PackagesDirectory "$(Pipeline.Workspace)\anonymous-packages" + env: + WINDOWS_APP_SDK_VERSION: ${{ parameters.WindowsAppSDKVersion }} diff --git a/eng/README.md b/eng/README.md index cffd60736..e1c663802 100644 --- a/eng/README.md +++ b/eng/README.md @@ -4,9 +4,9 @@ ## Pipeline setup -Create the pipeline in the public `shine-oss/WinAppSDK-Samples` Azure DevOps project and point it at this repository and YAML file. Configure the pipeline with a maximum of one concurrent run because the feed upstream setting is shared state. +Create the pipeline in the public `shine-oss/WinAppSDK-Samples` Azure DevOps project and point it at this repository and YAML file. -The pipeline derives the branch to hydrate from `Build.SourceBranch`. Updates to `main` hydrate `main`, while updates to a matching `release/*` branch hydrate that release branch. Branches outside protected `main` and `release/*` are rejected. The pipeline clones the triggering branch separately and uses `eng/SamplesFeed.nuget.config`. +Automatic runs use `Branch` mode and derive the branch to hydrate from `Build.SourceBranch`. Updates to `main` hydrate `main`, while updates to a matching `release/*` branch hydrate that release branch. Branches outside protected `main` and `release/*` are rejected. The pipeline clones the selected branch separately and uses `eng/SamplesFeed.nuget.config`. The `WinAppSDK-Samples Build Service (shine-oss)` identity needs: @@ -17,6 +17,14 @@ The `WinAppSDK-Samples Build Service (shine-oss)` identity needs: Do not enable pull-request triggers for the hydration job. It receives `System.AccessToken`, so it must only execute scripts from protected branches. Pull requests continue to use the existing sample build pipelines; package-version changes become anonymously available after the protected-branch hydration run completes. Restrict permission to queue the pipeline to trusted maintainers. +## Modes + +| Mode | Behavior | +| --- | --- | +| `Branch` | Hydrates and anonymously validates the complete dependency closure for the triggering branch. For a manual run, `SamplesBranch` may select `main` or a protected `release/*` branch; blank defaults to the queued branch, normally `main`. | +| `ValidateOnly` | Restores a selected branch anonymously without enabling or changing feed upstreams. | +| `WindowsAppSDKVersion` | Requires `WindowsAppSDKVersion`, then hydrates and anonymously validates that `Microsoft.WindowsAppSDK` package version and its NuGet dependency closure. | + ## Flow 1. Clone the protected branch whose update triggered the pipeline. @@ -25,11 +33,4 @@ Restrict permission to queue the pipeline to trusted maintainers. 4. Disable the upstream in an `always()` cleanup step. 5. Use a fresh job without `NuGetAuthenticate` to verify that the complete closure restores anonymously. -## Initial rollout - -1. Push an automation-only trusted topic branch containing this YAML, the `eng` scripts, and the hydration config. Do not change the branch's existing `Samples/nuget.config` yet. -2. Create the pipeline from `SamplesFeed-Hydration.yml` on that topic branch, then queue it with both the YAML branch and `SamplesBranch` set to the trusted topic branch. This validates dependency corrections included in the automation PR without changing the public branch's NuGet sources. -3. Require both hydration and anonymous validation to pass, merge the automation PR, and retarget the pipeline's default YAML branch to `main`. -4. Only then merge the separate change that removes nuget.org/package source mapping and makes `WinAppSDK-SampleDeps` the branch's single checked-in source. - If hydration fails, first confirm that the cleanup step removed the upstream before retrying. diff --git a/eng/RestoreWindowsAppSDKPackage.ps1 b/eng/RestoreWindowsAppSDKPackage.ps1 new file mode 100644 index 000000000..267cb058c --- /dev/null +++ b/eng/RestoreWindowsAppSDKPackage.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +param( + [string]$PackageVersion = $env:WINDOWS_APP_SDK_VERSION, + [string]$NuGetConfigPath = (Join-Path $PSScriptRoot "SamplesFeed.nuget.config"), + [Parameter(Mandatory)] + [string]$PackagesDirectory +) + +Set-StrictMode -Version 3.0 +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($PackageVersion) -or + $PackageVersion -notmatch '^[0-9A-Za-z][0-9A-Za-z.+-]*$') +{ + throw "WindowsAppSDKVersion must be a valid non-empty NuGet version." +} + +$nuget = Get-Command nuget -ErrorAction Stop +New-Item -ItemType Directory -Path $PackagesDirectory -Force | Out-Null + +Write-Host "Restoring Microsoft.WindowsAppSDK $PackageVersion and its dependency closure." +& $nuget.Source install Microsoft.WindowsAppSDK ` + -Version $PackageVersion ` + -OutputDirectory $PackagesDirectory ` + -ConfigFile $NuGetConfigPath ` + -NoHttpCache ` + -NonInteractive ` + -DirectDownload ` + -DependencyVersion Lowest ` + -Verbosity quiet + +if ($LASTEXITCODE -ne 0) +{ + throw "NuGet install failed for 'Microsoft.WindowsAppSDK $PackageVersion' with exit code $LASTEXITCODE." +} + +Write-Host "Restored Microsoft.WindowsAppSDK $PackageVersion and its dependency closure."