Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 170 additions & 105 deletions SamplesFeed-Hydration.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -9,132 +27,179 @@ trigger:
- Samples/**
- SamplesFeed-Hydration.yml
- eng/RestorePublicSamplesPackages.ps1
- eng/RestoreWindowsAppSDKPackage.ps1
- eng/SamplesFeed.nuget.config
- eng/SetSamplesFeedUpstream.ps1

pr: none

variables:
# Define SamplesBranch as a queue-time variable to override this main fallback.
EffectiveSamplesBranch: $[coalesce(variables.SamplesBranch, 'main')]
${{ 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'

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._-]*|copilot/samples-feed-hydration-bootstrap)$')
{
throw "SamplesBranch must be 'main', a protected 'release/*' branch, or the trusted bootstrap 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._-]*|copilot/samples-feed-hydration-bootstrap)$')
{
throw "SamplesBranch must be 'main', a protected 'release/*' branch, or the trusted bootstrap 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 }}
23 changes: 12 additions & 11 deletions eng/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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.
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:

Expand All @@ -15,21 +15,22 @@ 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.

## 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 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.
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.
37 changes: 37 additions & 0 deletions eng/RestoreWindowsAppSDKPackage.ps1
Original file line number Diff line number Diff line change
@@ -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."