Skip to content
Draft
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
65 changes: 65 additions & 0 deletions eng/pipelines/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ parameters:
displayName: 'Publish as Pre-Release'
type: boolean
default: false
- name: publishReleaseToWinGet
displayName: 'Publish release branch builds to WinGet'
type: boolean
default: false

trigger:
batch: true
Expand Down Expand Up @@ -50,10 +54,34 @@ pr:
variables:
- template: /eng/pipelines/common-variables.yml@self
- template: /eng/common/templates-official/variables/pool-providers.yml@self

# True on main, release/*, internal/release/* — used to gate secret-group loading and WinGet publishing.
- name: _IsPublishBranch
value: ${{ or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/internal/release/')) }}
# Publish to WinGet on main (always, non-PR) or release branches (only when publishReleaseToWinGet is true)
- name: _PublishToWinGet
value: ${{ or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), and(or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/internal/release/')), eq(parameters.publishReleaseToWinGet, true))) }}

# Use the release package id on release/* and internal/release/* branches, prerelease everywhere else
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/internal/release/')) }}:
- name: _WinGetTemplateDir
value: microsoft.aspire
- name: _WinGetPackageIdentifier
value: Microsoft.Aspire
- ${{ else }}:
- name: _WinGetTemplateDir
value: microsoft.aspire.prerelease
- name: _WinGetPackageIdentifier
value: Microsoft.Aspire.Prerelease

# Variable group containing VscePublishToken for VS Code Marketplace publishing
- ${{ if eq(parameters.publishVSCodeExtension, true) }}:
- group: Aspire-Release-Secrets

# Load secret variable groups for WinGet publishing
- ${{ if eq(variables._IsPublishBranch, 'True') }}:
- group: Aspire-Secrets

- name: _BuildConfig
value: Release
- name: Build.Arcade.ArtifactsPath
Expand Down Expand Up @@ -297,3 +325,40 @@ extends:
LclPackageId: 'LCL-JUNO-PROD-ASPIRE'
MirrorRepo: aspire
MirrorBranch: main

# ----------------------------------------------------------------
# Publish WinGet manifests (Windows)
# ----------------------------------------------------------------
- stage: publish_winget
displayName: Publish to WinGet
dependsOn:
- build
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'), eq(variables._IsPublishBranch, 'True'))
jobs:
- template: /eng/common/templates-official/jobs/jobs.yml@self
parameters:
enableMicrobuild: false
enablePublishUsingPipelines: false
enablePublishBuildAssets: false
enablePublishBuildArtifacts: true
enableTelemetry: true
workspace:
clean: all
jobs:
- job: WinGet_Publish
timeoutInMinutes: 30
pool:
name: NetCore1ESPool-Internal
image: 1es-windows-2022
os: windows
steps:
- checkout: self
fetchDepth: 1
- template: /eng/pipelines/templates/winget.yml@self
parameters:
publishToWinGet: ${{ variables._PublishToWinGet }}
wingetToken: $(aspire-winget-bot-pat)
buildConfig: $(_BuildConfig)
version: $(aspireVersion)
templateDir: ${{ variables._WinGetTemplateDir }}
packageIdentifier: ${{ variables._WinGetPackageIdentifier }}
276 changes: 276 additions & 0 deletions eng/pipelines/templates/winget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
parameters:
- name: publishToWinGet
type: boolean
default: false
- name: wingetToken
type: string
default: ''
- name: buildConfig
type: string
default: 'Release'
- name: version
type: string
default: ''
- name: templateDir
type: string
default: 'microsoft.aspire'
- name: packageIdentifier
type: string
default: 'Microsoft.Aspire'

steps:
- pwsh: |
$ErrorActionPreference = 'Stop'
$version = '${{ parameters.version }}'

if ([string]::IsNullOrWhiteSpace($version)) {
Write-Error "Version parameter is required"
exit 1
}

Write-Host "Version: $version"
Write-Host "##vso[task.setvariable variable=CliVersion]$version"
displayName: 🟣Set version ${{ parameters.version }}

- pwsh: |
$ErrorActionPreference = 'Stop'
Write-Host "Downloading wingetcreate..."
Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile "$(Build.StagingDirectory)/wingetcreate.exe"
Write-Host "wingetcreate downloaded successfully"
displayName: 🟣Install wingetcreate

- pwsh: |
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

# Check if winget is already available
$wingetPath = Get-Command winget -ErrorAction SilentlyContinue
if ($wingetPath) {
Write-Host "winget is already installed at: $($wingetPath.Source)"
winget --version
exit 0
}

Write-Host "Installing Microsoft.WinGet.Client module..."
Install-Module -Name Microsoft.WinGet.Client -Repository PSGallery -Force -Scope AllUsers

Write-Host "Installing WinGet using Repair-WinGetPackageManager..."
Repair-WinGetPackageManager -Latest -Force -AllUsers

# Verify installation
$wingetPath = Get-Command winget -ErrorAction SilentlyContinue
if ($wingetPath) {
Write-Host "winget installed successfully at: $($wingetPath.Source)"
winget --version
} else {
Write-Error "winget installation failed - command not found"
exit 1
}
displayName: 🟣Install winget CLI

- pwsh: |
$ErrorActionPreference = 'Stop'
$version = '$(CliVersion)'
$outputPath = '$(Build.StagingDirectory)/winget-manifests'
$templateDir = '$(Build.SourcesDirectory)/eng/winget/${{ parameters.templateDir }}'

Write-Host "Generating WinGet manifests for Aspire.Cli version $version"
Write-Host "Using template directory: $templateDir"

& "$(Build.SourcesDirectory)/eng/winget/generate-manifests.ps1" `
-Version $version `
-TemplateDir $templateDir `
-OutputPath $outputPath `
-ValidateUrls

if ($LASTEXITCODE -ne 0) {
Write-Error "generate-manifests.ps1 failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}

Write-Host "Manifest files generated:"
Get-ChildItem -Path $outputPath -Recurse | Format-Table FullName
displayName: 🟣Generate WinGet manifests

- pwsh: |
$ErrorActionPreference = 'Stop'
$manifestPath = '$(Build.StagingDirectory)/winget-manifests'
$version = '$(CliVersion)'

# Find the versioned manifest folder
$versionedManifestPath = Get-ChildItem -Path $manifestPath -Directory -Recurse |
Where-Object { $_.Name -eq $version } |
Select-Object -First 1 -ExpandProperty FullName

if (-not $versionedManifestPath) {
$versionedManifestPath = $manifestPath
}

Write-Host "Testing WinGet manifests at: $versionedManifestPath"
Write-Host ""

# Enable local manifest files
Write-Host "Enabling local manifest files in winget settings..."
winget settings --enable LocalManifestFiles
if ($LASTEXITCODE -ne 0) {
Write-Host "##[warning]Failed to enable local manifests. This may require admin privileges."
}

# Validate manifests using winget validate
Write-Host ""
Write-Host "Running winget validate..."
winget validate --manifest $versionedManifestPath
if ($LASTEXITCODE -ne 0) {
Write-Error "winget validate failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}

Write-Host "✅ winget validate passed"
displayName: 🟣Validate WinGet manifests

- pwsh: |
$ErrorActionPreference = 'Stop'
$manifestPath = '$(Build.StagingDirectory)/winget-manifests'
$version = '$(CliVersion)'

# Find the versioned manifest folder
$versionedManifestPath = Get-ChildItem -Path $manifestPath -Directory -Recurse |
Where-Object { $_.Name -eq $version } |
Select-Object -First 1 -ExpandProperty FullName

if (-not $versionedManifestPath) {
$versionedManifestPath = $manifestPath
}

Write-Host "Testing manifest install/uninstall at: $versionedManifestPath"
Write-Host ""

# Verify aspire is NOT available before install
Write-Host "Verifying aspire is not already installed..."
if (Get-Command aspire -ErrorAction SilentlyContinue) {
Write-Error "aspire command is already available before install - test environment is not clean"
exit 1
}
Write-Host " Confirmed: aspire is not in PATH"

# Test install
Write-Host ""
Write-Host "Installing Aspire.Cli from local manifest..."
winget install --manifest $versionedManifestPath --accept-package-agreements --accept-source-agreements
if ($LASTEXITCODE -ne 0) {
Write-Error "winget install failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}
Write-Host "✅ Install succeeded"

# Refresh PATH from registry to pick up changes made by winget
Write-Host ""
Write-Host "Refreshing PATH environment variable..."
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")

# Verify aspire is now available in PATH.
# Use a new pwsh process so it inherits the system/user PATH that winget updated
# rather than relying on the current process's stale $env:Path.
$failed = $false
Write-Host "Verifying aspire CLI is in PATH (new process)..."
try {
$aspireInfo = pwsh -NoProfile -Command '
$cmd = Get-Command aspire -ErrorAction SilentlyContinue
if (-not $cmd) { Write-Error "aspire not found in PATH"; exit 1 }
Write-Host " Path: $($cmd.Source)"
$v = & aspire --version 2>&1
if ($LASTEXITCODE -ne 0) { Write-Error "aspire --version failed: $v"; exit $LASTEXITCODE }
Write-Host " Version: $v"
'
if ($LASTEXITCODE -ne 0) {
throw "Child process exited with code $LASTEXITCODE"
}
Write-Host "✅ aspire CLI verified"
} catch {
Write-Host "##[error]Failed to verify aspire CLI: $_"
$failed = $true
}

# Test uninstall (always attempt cleanup even if verification failed)
Write-Host ""
Write-Host "Uninstalling Aspire.Cli..."
winget uninstall --manifest $versionedManifestPath --accept-source-agreements
if ($LASTEXITCODE -ne 0) {
if ($failed) {
Write-Host "##[warning]winget uninstall also failed with exit code $LASTEXITCODE (ignoring since verification already failed)"
} else {
Write-Error "winget uninstall failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}
} else {
Write-Host "✅ Uninstall succeeded"
}

if ($failed) {
exit 1
}
displayName: 🟣Test WinGet manifest install/uninstall

- task: 1ES.PublishBuildArtifacts@1
displayName: 🟣Publish WinGet manifests
condition: always()
inputs:
PathtoPublish: '$(Build.StagingDirectory)/winget-manifests'
ArtifactName: winget-manifests

- ${{ if and(eq(parameters.publishToWinGet, true), ne(parameters.wingetToken, '')) }}:
- pwsh: |
$ErrorActionPreference = 'Stop'
$version = '$(CliVersion)'
$manifestPath = '$(Build.StagingDirectory)/winget-manifests'

# Extract installer URLs from the generated manifest
$installerYaml = Get-ChildItem -Path $manifestPath -Filter "*.installer.yaml" -Recurse | Select-Object -First 1
if (-not $installerYaml) {
Write-Error "No installer.yaml found in $manifestPath"
exit 1
}

$urls = (Get-Content $installerYaml.FullName | Select-String -Pattern '^\s*InstallerUrl:\s*(.+)$' -AllMatches).Matches |
ForEach-Object { $_.Groups[1].Value.Trim() }

if ($urls.Count -eq 0) {
Write-Error "No InstallerUrl entries found in $($installerYaml.Name)"
exit 1
}

$token = $env:WINGET_CREATE_GITHUB_TOKEN
if ([string]::IsNullOrWhiteSpace($token)) {
Write-Error "WINGET_CREATE_GITHUB_TOKEN is not set or empty"
exit 1
}

Write-Host "Submitting WinGet manifest update for Aspire version $version"
Write-Host "Installer URLs: $($urls -join ', ')"

# wingetcreate reads the token from WINGET_CREATE_GITHUB_TOKEN env var automatically
# See: https://github.com/microsoft/winget-create/blob/main/doc/token.md
$output = & "$(Build.StagingDirectory)/wingetcreate.exe" update ${{ parameters.packageIdentifier }} `
--urls @urls `
--version $version `
--submit 2>&1

$exitCode = $LASTEXITCODE
$outputText = $output -join "`n"
Write-Host $outputText

if ($exitCode -ne 0) {
Write-Error "wingetcreate failed with exit code $exitCode"
exit $exitCode
}

# wingetcreate may exit 0 despite errors (e.g. invalid token)
if ($outputText -match 'Token was invalid|error|failed') {
Write-Error "wingetcreate reported errors despite exit code 0"
exit 1
}

Write-Host "Successfully submitted WinGet manifest update for Aspire $version"
displayName: 🟣Submit to WinGet
env:
WINGET_CREATE_GITHUB_TOKEN: ${{ parameters.wingetToken }}
Loading
Loading