Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.240405.15\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.240405.15\build\native\Microsoft.Windows.CppWinRT.props')" />
<Import Project="..\packages\Microsoft.WindowsAppSDK.1.7.241114004-experimental1\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.1.7.241114004-experimental1\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\BackgroundTask_WinAppSDK\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.2161\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\BackgroundTask_WinAppSDK\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.2161\build\Microsoft.Windows.SDK.BuildTools.props')" />
<Import Project="..\BackgroundTask_WinAppSDK\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4948\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\BackgroundTask_WinAppSDK\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4948\build\Microsoft.Windows.SDK.BuildTools.props')" />
<Import Project="..\BackgroundTask_WinAppSDK\packages\Microsoft.Windows.CppWinRT.2.0.241115.3\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\BackgroundTask_WinAppSDK\packages\Microsoft.Windows.CppWinRT.2.0.241115.3\build\native\Microsoft.Windows.CppWinRT.props')" />
<Import Project="..\BackgroundTask_WinAppSDK\packages\Microsoft.WindowsAppSDK.1.7.240912003\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\BackgroundTask_WinAppSDK\packages\Microsoft.WindowsAppSDK.1.7.240912003\build\native\Microsoft.WindowsAppSDK.props')" />
<PropertyGroup Label="Globals">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<package id="Microsoft.Web.WebView2" version="1.0.2792.45" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.240405.15" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.240803.1" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.2161" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.4948" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.7.241114004-experimental1" targetFramework="native" />
</packages>
140 changes: 140 additions & 0 deletions SamplesFeed-Hydration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
trigger:
batch: true
branches:
include:
- main
- release/*
paths:
include:
- Samples/**
- SamplesFeed-Hydration.yml
- eng/RestorePublicSamplesPackages.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')]
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)

- job: ValidateAnonymousRestore
displayName: Validate anonymous public restore
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)

- 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"
35 changes: 35 additions & 0 deletions eng/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Samples feed automation

`SamplesFeed-Hydration.yml` keeps the public `WinAppSDK-SampleDeps` feed populated without leaving its NuGet Gallery upstream enabled.

## 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 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 `WinAppSDK-Samples Build Service (shine-oss)` identity needs:

- permission to use the pipeline OAuth token;
- the feed `Administrator` role while toggling upstream settings;
- 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.

## Flow

1. Clone the branch selected by `SamplesBranch`.
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.
86 changes: 86 additions & 0 deletions eng/RestorePublicSamplesPackages.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
[CmdletBinding()]
param(
[string]$SamplesRoot = (Join-Path $PSScriptRoot "..\Samples"),
[string]$NuGetConfigPath = (Join-Path $PSScriptRoot "..\Samples\nuget.config"),
[Parameter(Mandatory)]
[string]$PackagesDirectory
)

Set-StrictMode -Version 3.0
$ErrorActionPreference = "Stop"

$nuget = Get-Command nuget -ErrorAction Stop
$solutions = @(Get-ChildItem -Path $SamplesRoot -Recurse -File -Filter *.sln)
if ($solutions.Count -eq 0)
{
throw "No solutions were found under '$SamplesRoot'."
}

New-Item -ItemType Directory -Path $PackagesDirectory -Force | Out-Null
$originalPackagesDirectory = $env:NUGET_PACKAGES
$env:NUGET_PACKAGES = $PackagesDirectory

try
{
foreach ($solution in $solutions)
{
Write-Host "Restoring $($solution.FullName)"
& $nuget.Source restore $solution.FullName `
-ConfigFile $NuGetConfigPath `
-PackagesDirectory $PackagesDirectory `
-NoHttpCache `
-NonInteractive `
-ForceEnglishOutput `
-Verbosity quiet

if ($LASTEXITCODE -ne 0)
{
throw "NuGet restore failed for '$($solution.FullName)' with exit code $LASTEXITCODE."
}
}

foreach ($cmakeFile in Get-ChildItem -Path $SamplesRoot -Recurse -File -Filter CMakeLists.txt)
{
$content = Get-Content -Path $cmakeFile.FullName -Raw
$packageMatches = [regex]::Matches(
$content,
'set\(\s*(?<prefix>[A-Za-z0-9_]+)_NUGET_ID\s+"(?<id>[^"]+)"')

foreach ($packageMatch in $packageMatches)
{
$prefix = [regex]::Escape($packageMatch.Groups["prefix"].Value)
$versionMatch = [regex]::Match(
$content,
"set\(\s*${prefix}_NUGET_VERSION\s+`"(?<version>[^`"]+)`"")

if (!$versionMatch.Success)
{
throw "No matching NuGet version was found for '$($packageMatch.Groups["id"].Value)' in '$($cmakeFile.FullName)'."
}

$packageId = $packageMatch.Groups["id"].Value
$packageVersion = $versionMatch.Groups["version"].Value
Write-Host "Installing CMake dependency $packageId $packageVersion"
& $nuget.Source install $packageId `
-Version $packageVersion `
-OutputDirectory $PackagesDirectory `
-ConfigFile $NuGetConfigPath `
-NoHttpCache `
-NonInteractive `
-DirectDownload `
-DependencyVersion Ignore `
-Verbosity quiet

if ($LASTEXITCODE -ne 0)
{
throw "NuGet install failed for '$packageId $packageVersion' with exit code $LASTEXITCODE."
}
}
}
}
finally
{
$env:NUGET_PACKAGES = $originalPackagesDirectory
}

Write-Host "Restored $($solutions.Count) sample solutions and all declared CMake NuGet dependencies."
7 changes: 7 additions & 0 deletions eng/SamplesFeed.nuget.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="WinAppSDK-SampleDeps" value="https://pkgs.dev.azure.com/shine-oss/WinAppSDK-Samples/_packaging/WinAppSDK-SampleDeps/nuget/v3/index.json" />
</packageSources>
</configuration>
70 changes: 70 additions & 0 deletions eng/SetSamplesFeedUpstream.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateSet("Enable", "Disable")]
[string]$Action,

[string]$AccessToken = $env:SYSTEM_ACCESSTOKEN,

[string]$Organization = "shine-oss",
[string]$Project = "WinAppSDK-Samples",
[string]$FeedId = "4c0236bb-5661-46ec-a487-808515ccdbd2"
)

Set-StrictMode -Version 3.0
$ErrorActionPreference = "Stop"

if ([string]::IsNullOrWhiteSpace($AccessToken))
{
throw "An Azure Pipelines OAuth token is required."
}

$projectName = [Uri]::EscapeDataString($Project)
$feedUri = "https://feeds.dev.azure.com/$Organization/$projectName/_apis/packaging/feeds/$FeedId`?api-version=7.0"
$headers = @{
Authorization = "Bearer $AccessToken"
}

$upstreamSources = @()
if ($Action -eq "Enable")
{
$upstreamSources = @(
@{
name = "NuGet Gallery"
protocol = "nuget"
location = "https://api.nuget.org/v3/index.json"
displayLocation = "https://api.nuget.org/v3/index.json"
upstreamSourceType = "public"
}
)
}

$body = @{
upstreamSources = $upstreamSources
} | ConvertTo-Json -Depth 4

Invoke-RestMethod `
-Method Patch `
-Uri $feedUri `
-Headers $headers `
-ContentType "application/json" `
-Body $body | Out-Null

$feed = Invoke-RestMethod -Method Get -Uri $feedUri -Headers $headers
$activeUpstreams = @(
$feed.upstreamSources | Where-Object {
!$_.PSObject.Properties["deletedDate"] -or !$_.deletedDate
}
)

if ($Action -eq "Enable" -and $activeUpstreams.Count -ne 1)
{
throw "Expected one active upstream after enabling NuGet Gallery, but found $($activeUpstreams.Count)."
}

if ($Action -eq "Disable" -and $activeUpstreams.Count -ne 0)
{
throw "Expected no active upstreams after disabling NuGet Gallery, but found $($activeUpstreams.Count)."
}

Write-Host "$Action completed for feed '$($feed.name)'. Active upstream count: $($activeUpstreams.Count)."