diff --git a/NuGet.config b/NuGet.config index 05156838a3a..2822bc5d52a 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,15 +4,12 @@ - - - @@ -50,7 +47,6 @@ - diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 1c5e155ba59..b68df94935f 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -183,33 +183,33 @@ - + https://github.com/dotnet/arcade - 488413fe104056170673a048a07906314e101e5d + 8b0ca8dba65be0853690ce98ae8f950a25ff8421 - + https://github.com/dotnet/arcade - 488413fe104056170673a048a07906314e101e5d + 8b0ca8dba65be0853690ce98ae8f950a25ff8421 - + https://github.com/dotnet/arcade - 488413fe104056170673a048a07906314e101e5d + 8b0ca8dba65be0853690ce98ae8f950a25ff8421 - + https://github.com/dotnet/arcade - 488413fe104056170673a048a07906314e101e5d + 8b0ca8dba65be0853690ce98ae8f950a25ff8421 - + https://github.com/dotnet/arcade - 488413fe104056170673a048a07906314e101e5d + 8b0ca8dba65be0853690ce98ae8f950a25ff8421 - + https://github.com/dotnet/arcade - 488413fe104056170673a048a07906314e101e5d + 8b0ca8dba65be0853690ce98ae8f950a25ff8421 - + https://github.com/dotnet/arcade - 488413fe104056170673a048a07906314e101e5d + 8b0ca8dba65be0853690ce98ae8f950a25ff8421 diff --git a/eng/Versions.props b/eng/Versions.props index 52b0289a71d..d69d72a7634 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,7 +3,7 @@ 13 1 - 2 + 3 $(MajorVersion).$(MinorVersion).$(PatchVersion) preview.1 net8.0 @@ -37,9 +37,9 @@ 0.20.7 0.20.7 - 11.0.0-beta.25509.1 - 11.0.0-beta.25509.1 - 11.0.0-beta.25509.1 + 10.0.0-beta.26160.1 + 10.0.0-beta.26160.1 + 10.0.0-beta.26160.1 10.0.1 10.1.0 diff --git a/eng/common/SetupNugetSources.ps1 b/eng/common/SetupNugetSources.ps1 index fc8d618014e..65ed3a8adef 100644 --- a/eng/common/SetupNugetSources.ps1 +++ b/eng/common/SetupNugetSources.ps1 @@ -1,6 +1,7 @@ # This script adds internal feeds required to build commits that depend on internal package sources. For instance, -# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. In addition also enables -# disabled internal Maestro (darc-int*) feeds. +# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. Similarly, +# dotnet-eng-internal and dotnet-tools-internal are added if dotnet-eng and dotnet-tools are present. +# In addition, this script also enables disabled internal Maestro (darc-int*) feeds. # # Optionally, this script also adds a credential entry for each of the internal feeds if supplied. # @@ -173,4 +174,16 @@ foreach ($dotnetVersion in $dotnetVersions) { } } +# Check for dotnet-eng and add dotnet-eng-internal if present +$dotnetEngSource = $sources.SelectSingleNode("add[@key='dotnet-eng']") +if ($dotnetEngSource -ne $null) { + AddOrEnablePackageSource -Sources $sources -DisabledPackageSources $disabledSources -SourceName "dotnet-eng-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-eng-internal/nuget/$feedSuffix" -Creds $creds -Username $userName -pwd $Password +} + +# Check for dotnet-tools and add dotnet-tools-internal if present +$dotnetToolsSource = $sources.SelectSingleNode("add[@key='dotnet-tools']") +if ($dotnetToolsSource -ne $null) { + AddOrEnablePackageSource -Sources $sources -DisabledPackageSources $disabledSources -SourceName "dotnet-tools-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-tools-internal/nuget/$feedSuffix" -Creds $creds -Username $userName -pwd $Password +} + $doc.Save($filename) diff --git a/eng/common/SetupNugetSources.sh b/eng/common/SetupNugetSources.sh index dd2564aef01..b2163abbe71 100755 --- a/eng/common/SetupNugetSources.sh +++ b/eng/common/SetupNugetSources.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # This script adds internal feeds required to build commits that depend on internal package sources. For instance, -# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. In addition also enables -# disabled internal Maestro (darc-int*) feeds. +# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. Similarly, +# dotnet-eng-internal and dotnet-tools-internal are added if dotnet-eng and dotnet-tools are present. +# In addition, this script also enables disabled internal Maestro (darc-int*) feeds. # # Optionally, this script also adds a credential entry for each of the internal feeds if supplied. # @@ -66,10 +67,8 @@ EnableInternalPackageSource() { grep -i " /dev/null if [ "$?" == "0" ]; then echo "Enabling internal source '$PackageSourceName'." - # Remove the disabled entry - local OldDisableValue="" - local NewDisableValue="" - sed -i.bak "s|$OldDisableValue|$NewDisableValue|" "$ConfigFile" + # Remove the disabled entry (including any surrounding comments or whitespace on the same line) + sed -i.bak "//d" "$ConfigFile" # Add the source name to PackageSources for credential handling PackageSources+=("$PackageSourceName") @@ -175,6 +174,18 @@ for DotNetVersion in ${DotNetVersions[@]} ; do fi done +# Check for dotnet-eng and add dotnet-eng-internal if present +grep -i " /dev/null +if [ "$?" == "0" ]; then + AddOrEnablePackageSource "dotnet-eng-internal" "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-eng-internal/nuget/$FeedSuffix" +fi + +# Check for dotnet-tools and add dotnet-tools-internal if present +grep -i " /dev/null +if [ "$?" == "0" ]; then + AddOrEnablePackageSource "dotnet-tools-internal" "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-tools-internal/nuget/$FeedSuffix" +fi + # I want things split line by line PrevIFS=$IFS IFS=$'\n' diff --git a/eng/common/build.ps1 b/eng/common/build.ps1 index c10aba98ac6..8cfee107e7a 100644 --- a/eng/common/build.ps1 +++ b/eng/common/build.ps1 @@ -30,7 +30,6 @@ Param( [string] $runtimeSourceFeedKey = '', [switch] $excludePrereleaseVS, [switch] $nativeToolsOnMachine, - [switch] $restoreMaui, [switch] $help, [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties ) @@ -77,7 +76,6 @@ function Print-Usage() { Write-Host " -nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" Write-Host " -buildCheck Sets /check msbuild parameter" Write-Host " -fromVMR Set when building from within the VMR" - Write-Host " -restoreMaui Restore the MAUI workload after restore (only on Windows/macOS)" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." diff --git a/eng/common/build.sh b/eng/common/build.sh index 09d1f8e6d9c..9767bb411a4 100755 --- a/eng/common/build.sh +++ b/eng/common/build.sh @@ -44,7 +44,6 @@ usage() echo " --warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" echo " --buildCheck Sets /check msbuild parameter" echo " --fromVMR Set when building from within the VMR" - echo " --restoreMaui Restore the MAUI workload after restore (only on macOS)" echo "" echo "Command line arguments not listed above are passed thru to msbuild." echo "Arguments can also be passed in with a single hyphen." @@ -77,7 +76,6 @@ sign=false public=false ci=false clean=false -restore_maui=false warn_as_error=true node_reuse=true @@ -94,7 +92,7 @@ runtime_source_feed='' runtime_source_feed_key='' properties=() -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in -help|-h) @@ -185,9 +183,6 @@ while [[ $# -gt 0 ]]; do -buildcheck) build_check=true ;; - -restoremaui|-restore-maui) - restore_maui=true - ;; -runtimesourcefeed) runtime_source_feed=$2 shift diff --git a/eng/common/core-templates/job/job.yml b/eng/common/core-templates/job/job.yml index cb4ccc023a3..5ce51840619 100644 --- a/eng/common/core-templates/job/job.yml +++ b/eng/common/core-templates/job/job.yml @@ -19,8 +19,6 @@ parameters: # publishing defaults artifacts: '' enableMicrobuild: false - enablePreviewMicrobuild: false - microbuildPluginVersion: 'latest' enableMicrobuildForMacAndLinux: false microbuildUseESRP: true enablePublishBuildArtifacts: false @@ -130,8 +128,6 @@ jobs: - template: /eng/common/core-templates/steps/install-microbuild.yml parameters: enableMicrobuild: ${{ parameters.enableMicrobuild }} - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildPluginVersion: ${{ parameters.microbuildPluginVersion }} enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} microbuildUseESRP: ${{ parameters.microbuildUseESRP }} continueOnError: ${{ parameters.continueOnError }} @@ -157,8 +153,6 @@ jobs: - template: /eng/common/core-templates/steps/cleanup-microbuild.yml parameters: enableMicrobuild: ${{ parameters.enableMicrobuild }} - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildPluginVersion: ${{ parameters.microbuildPluginVersion }} enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index 37dff559fc1..da6ed568bd6 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -80,7 +80,7 @@ jobs: # If it's not devdiv, it's dnceng ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: name: NetCore1ESPool-Publishing-Internal - image: windows.vs2019.amd64 + image: windows.vs2022.amd64 os: windows steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: @@ -120,6 +120,14 @@ jobs: - task: NuGetAuthenticate@1 + # Populate internal runtime variables. + - template: /eng/common/templates/steps/enable-internal-sources.yml + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + parameters: + legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) + + - template: /eng/common/templates/steps/enable-internal-runtimes.yml + - task: AzureCLI@2 displayName: Publish Build Assets inputs: @@ -132,6 +140,9 @@ jobs: /p:IsAssetlessBuild=${{ parameters.isAssetlessBuild }} /p:MaestroApiEndpoint=https://maestro.dot.net /p:OfficialBuildId=$(OfficialBuildId) + -runtimeSourceFeed https://ci.dot.net/internal + -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' + condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/core-templates/job/source-build.yml b/eng/common/core-templates/job/source-build.yml index d805d5faeb9..1997c2ae00d 100644 --- a/eng/common/core-templates/job/source-build.yml +++ b/eng/common/core-templates/job/source-build.yml @@ -60,19 +60,19 @@ jobs: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] - demands: ImageOverride -equals build.ubuntu.2004.amd64 + demands: ImageOverride -equals build.azurelinux.3.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] - image: 1es-mariner-2 + image: build.azurelinux.3.amd64 os: linux ${{ else }}: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] - demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open + demands: ImageOverride -equals build.azurelinux.3.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] - demands: ImageOverride -equals Build.Ubuntu.2204.Amd64 + demands: ImageOverride -equals build.azurelinux.3.amd64 ${{ if ne(parameters.platform.pool, '') }}: pool: ${{ parameters.platform.pool }} diff --git a/eng/common/core-templates/job/source-index-stage1.yml b/eng/common/core-templates/job/source-index-stage1.yml index 30530359a5d..76baf5c2725 100644 --- a/eng/common/core-templates/job/source-index-stage1.yml +++ b/eng/common/core-templates/job/source-index-stage1.yml @@ -3,7 +3,7 @@ parameters: sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "eng/common/build.ps1 -restore -build -binarylog -ci" preSteps: [] binlogPath: artifacts/log/Debug/Build.binlog - condition: '' + condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') dependsOn: '' pool: '' is1ESPipeline: '' @@ -25,10 +25,10 @@ jobs: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $(DncEngPublicBuildPool) - image: windows.vs2022.amd64.open + image: windows.vs2026preview.scout.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $(DncEngInternalBuildPool) - image: windows.vs2022.amd64 + image: windows.vs2026preview.scout.amd64 steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: @@ -41,4 +41,4 @@ jobs: - template: /eng/common/core-templates/steps/source-index-stage1-publish.yml parameters: - binLogPath: ${{ parameters.binLogPath }} \ No newline at end of file + binLogPath: ${{ parameters.binLogPath }} diff --git a/eng/common/core-templates/post-build/post-build.yml b/eng/common/core-templates/post-build/post-build.yml index f6f87fe5c67..b942a79ef02 100644 --- a/eng/common/core-templates/post-build/post-build.yml +++ b/eng/common/core-templates/post-build/post-build.yml @@ -127,11 +127,11 @@ stages: ${{ else }}: ${{ if eq(parameters.is1ESPipeline, true) }}: name: $(DncEngInternalBuildPool) - image: windows.vs2022.amd64 + image: windows.vs2026preview.scout.amd64 os: windows ${{ else }}: name: $(DncEngInternalBuildPool) - demands: ImageOverride -equals windows.vs2022.amd64 + demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml @@ -175,7 +175,7 @@ stages: os: windows ${{ else }}: name: $(DncEngInternalBuildPool) - demands: ImageOverride -equals windows.vs2022.amd64 + demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: @@ -236,7 +236,7 @@ stages: os: windows ${{ else }}: name: $(DncEngInternalBuildPool) - demands: ImageOverride -equals windows.vs2022.amd64 + demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: @@ -293,11 +293,11 @@ stages: ${{ else }}: ${{ if eq(parameters.is1ESPipeline, true) }}: name: NetCore1ESPool-Publishing-Internal - image: windows.vs2019.amd64 + image: windows.vs2022.amd64 os: windows ${{ else }}: name: NetCore1ESPool-Publishing-Internal - demands: ImageOverride -equals windows.vs2019.amd64 + demands: ImageOverride -equals windows.vs2022.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: @@ -307,6 +307,18 @@ stages: - task: NuGetAuthenticate@1 + # Populate internal runtime variables. + - template: /eng/common/templates/steps/enable-internal-sources.yml + parameters: + legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) + + - template: /eng/common/templates/steps/enable-internal-runtimes.yml + + # Darc is targeting 8.0, so make sure it's installed + - task: UseDotNet@2 + inputs: + version: 8.0.x + - task: AzureCLI@2 displayName: Publish Using Darc inputs: @@ -323,3 +335,5 @@ stages: -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}' + -runtimeSourceFeed https://ci.dot.net/internal + -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' diff --git a/eng/common/core-templates/steps/generate-sbom.yml b/eng/common/core-templates/steps/generate-sbom.yml index 003f7eae0fa..c05f6502797 100644 --- a/eng/common/core-templates/steps/generate-sbom.yml +++ b/eng/common/core-templates/steps/generate-sbom.yml @@ -5,7 +5,7 @@ # IgnoreDirectories - Directories to ignore for SBOM generation. This will be passed through to the CG component detector. parameters: - PackageVersion: 11.0.0 + PackageVersion: 10.0.0 BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' PackageName: '.NET' ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom diff --git a/eng/common/core-templates/steps/install-microbuild-impl.yml b/eng/common/core-templates/steps/install-microbuild-impl.yml deleted file mode 100644 index 9fdf3a11677..00000000000 --- a/eng/common/core-templates/steps/install-microbuild-impl.yml +++ /dev/null @@ -1,34 +0,0 @@ -parameters: - - name: microbuildTaskInputs - type: object - default: {} - - - name: microbuildEnv - type: object - default: {} - - - name: enablePreviewMicrobuild - type: boolean - default: false - - - name: condition - type: string - - - name: continueOnError - type: boolean - -steps: -- ${{ if eq(parameters.enablePreviewMicrobuild, 'true') }}: - - task: MicroBuildSigningPluginPreview@4 - displayName: Install Preview MicroBuild plugin (Windows) - inputs: ${{ parameters.microbuildTaskInputs }} - env: ${{ parameters.microbuildEnv }} - continueOnError: ${{ parameters.continueOnError }} - condition: ${{ parameters.condition }} -- ${{ else }}: - - task: MicroBuildSigningPlugin@4 - displayName: Install MicroBuild plugin (Windows) - inputs: ${{ parameters.microbuildTaskInputs }} - env: ${{ parameters.microbuildEnv }} - continueOnError: ${{ parameters.continueOnError }} - condition: ${{ parameters.condition }} \ No newline at end of file diff --git a/eng/common/core-templates/steps/install-microbuild.yml b/eng/common/core-templates/steps/install-microbuild.yml index bdebec0eaa9..553fce66b94 100644 --- a/eng/common/core-templates/steps/install-microbuild.yml +++ b/eng/common/core-templates/steps/install-microbuild.yml @@ -4,8 +4,6 @@ parameters: # Enable install tasks for MicroBuild on Mac and Linux # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT' enableMicrobuildForMacAndLinux: false - # Enable preview version of MB signing plugin - enablePreviewMicrobuild: false # Determines whether the ESRP service connection information should be passed to the signing plugin. # This overlaps with _SignType to some degree. We only need the service connection for real signing. # It's important that the service connection not be passed to the MicroBuildSigningPlugin task in this place. @@ -13,11 +11,8 @@ parameters: # Unfortunately, _SignType can't be used to exclude the use of the service connection in non-real sign scenarios. The # variable is not available in template expression. _SignType has a very large proliferation across .NET, so replacing it is tough. microbuildUseESRP: true - # Location of the MicroBuild output folder - # NOTE: There's something that relies on this being in the "default" source directory for tasks such as Signing to work properly. - microBuildOutputFolder: '$(Build.SourcesDirectory)' - # Microbuild version - microbuildPluginVersion: 'latest' + # Microbuild installation directory + microBuildOutputFolder: $(Agent.TempDirectory)/MicroBuild continueOnError: false @@ -30,8 +25,27 @@ steps: inputs: packageType: sdk version: 8.0.x - installationPath: ${{ parameters.microBuildOutputFolder }}/.dotnet - workingDirectory: ${{ parameters.microBuildOutputFolder }} + installationPath: ${{ parameters.microBuildOutputFolder }}/.dotnet-microbuild + condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) + + - script: | + set -euo pipefail + + # UseDotNet@2 prepends the dotnet executable path to the PATH variable, so we can call dotnet directly + version=$(dotnet --version) + cat << 'EOF' > ${{ parameters.microBuildOutputFolder }}/global.json + { + "sdk": { + "version": "$version", + "paths": [ + "${{ parameters.microBuildOutputFolder }}/.dotnet-microbuild" + ], + "errorMessage": "The .NET SDK version $version is required to install the MicroBuild signing plugin." + } + } + EOF + displayName: 'Add global.json to MicroBuild Installation path' + workingDirectory: ${{ parameters.microBuildOutputFolder }} condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) - script: | @@ -55,45 +69,42 @@ steps: # YAML expansion, and Windows vs. Linux/Mac uses different service connections. However, # we can avoid including the MB install step if not enabled at all. This avoids a bunch of # extra pipeline authorizations, since most pipelines do not sign on non-Windows. - - template: /eng/common/core-templates/steps/install-microbuild-impl.yml@self - parameters: - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildTaskInputs: + - task: MicroBuildSigningPlugin@4 + displayName: Install MicroBuild plugin (Windows) + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + ${{ if eq(parameters.microbuildUseESRP, true) }}: + ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ${{ else }}: + ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca + env: + TeamName: $(_TeamName) + MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test')) + + - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, true) }}: + - task: MicroBuildSigningPlugin@4 + displayName: Install MicroBuild plugin (non-Windows) + inputs: signType: $(_SignType) zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json - version: ${{ parameters.microbuildPluginVersion }} + workingDirectory: ${{ parameters.microBuildOutputFolder }} ${{ if eq(parameters.microbuildUseESRP, true) }}: ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 ${{ else }}: - ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca - microbuildEnv: + ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc + env: TeamName: $(_TeamName) MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} SYSTEM_ACCESSTOKEN: $(System.AccessToken) continueOnError: ${{ parameters.continueOnError }} - condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test')) - - - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, true) }}: - - template: /eng/common/core-templates/steps/install-microbuild-impl.yml@self - parameters: - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildTaskInputs: - signType: $(_SignType) - zipSources: false - feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json - version: ${{ parameters.microbuildPluginVersion }} - ${{ if eq(parameters.microbuildUseESRP, true) }}: - ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 - ${{ else }}: - ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc - microbuildEnv: - TeamName: $(_TeamName) - MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - continueOnError: ${{ parameters.continueOnError }} - condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real')) + condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real')) diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 10f825e270a..a9ea99ba6aa 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -26,10 +26,11 @@ steps: # If the file exists - sensitive data for redaction will be sourced from it # (single entry per line, lines starting with '# ' are considered comments and skipped) arguments: -InputPath '$(System.DefaultWorkingDirectory)/PostBuildLogs' - -BinlogToolVersion ${{parameters.BinlogToolVersion}} + -BinlogToolVersion '${{parameters.BinlogToolVersion}}' -TokensFilePath '$(System.DefaultWorkingDirectory)/eng/BinlogSecretsRedactionFile.txt' + -runtimeSourceFeed https://ci.dot.net/internal + -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' '$(publishing-dnceng-devdiv-code-r-build-re)' - '$(MaestroAccessToken)' '$(dn-bot-all-orgs-artifact-feeds-rw)' '$(akams-client-id)' '$(microsoft-symbol-server-pat)' diff --git a/eng/common/core-templates/steps/source-build.yml b/eng/common/core-templates/steps/source-build.yml index acf16ed3496..b9c86c18ae4 100644 --- a/eng/common/core-templates/steps/source-build.yml +++ b/eng/common/core-templates/steps/source-build.yml @@ -24,7 +24,7 @@ steps: # in the default public locations. internalRuntimeDownloadArgs= if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then - internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey '$(dotnetbuilds-internal-container-read-token-base64)'' fi buildConfig=Release diff --git a/eng/common/core-templates/steps/source-index-stage1-publish.yml b/eng/common/core-templates/steps/source-index-stage1-publish.yml index eff4573c6e5..e9a694afa58 100644 --- a/eng/common/core-templates/steps/source-index-stage1-publish.yml +++ b/eng/common/core-templates/steps/source-index-stage1-publish.yml @@ -1,6 +1,6 @@ parameters: - sourceIndexUploadPackageVersion: 2.0.0-20250906.1 - sourceIndexProcessBinlogPackageVersion: 1.0.1-20250906.1 + sourceIndexUploadPackageVersion: 2.0.0-20250818.1 + sourceIndexProcessBinlogPackageVersion: 1.0.1-20250818.1 sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json binlogPath: artifacts/log/Debug/Build.binlog diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh index 9f5ad6b763b..e889f439b8d 100755 --- a/eng/common/darc-init.sh +++ b/eng/common/darc-init.sh @@ -5,7 +5,7 @@ darcVersion='' versionEndpoint='https://maestro.dot.net/api/assets/darc-version?api-version=2020-02-20' verbosity='minimal' -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in --darcversion) diff --git a/eng/common/dotnet-install.sh b/eng/common/dotnet-install.sh index 61f302bb677..7b9d97e3bd4 100755 --- a/eng/common/dotnet-install.sh +++ b/eng/common/dotnet-install.sh @@ -18,7 +18,7 @@ architecture='' runtime='dotnet' runtimeSourceFeed='' runtimeSourceFeedKey='' -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in -version|-v) diff --git a/eng/common/dotnet.sh b/eng/common/dotnet.sh index f6d24871c1d..2ef68235675 100644 --- a/eng/common/dotnet.sh +++ b/eng/common/dotnet.sh @@ -19,7 +19,7 @@ source $scriptroot/tools.sh InitializeDotNetCli true # install # Invoke acquired SDK with args if they are provided -if [[ $# -gt 0 ]]; then +if [[ $# > 0 ]]; then __dotnetDir=${_InitializeDotNetCli} dotnetPath=${__dotnetDir}/dotnet ${dotnetPath} "$@" diff --git a/eng/common/generate-locproject.ps1 b/eng/common/generate-locproject.ps1 index 792dd256c01..fa1cdc2b300 100644 --- a/eng/common/generate-locproject.ps1 +++ b/eng/common/generate-locproject.ps1 @@ -89,36 +89,36 @@ $locJson = @{ @{ LanguageSet = $LanguageSet LocItems = @( - $locFiles | ForEach-Object { - $outputPath = "$(($_.DirectoryName | Resolve-Path -Relative) + "\")" - $continue = $true - foreach ($exclusion in $exclusions.Exclusions) { - if ($_.FullName.Contains($exclusion)) - { - $continue = $false - } - } - $sourceFile = ($_.FullName | Resolve-Path -Relative) - if (!$CreateNeutralXlfs -and $_.Extension -eq '.xlf') { - Remove-Item -Path $sourceFile - } - if ($continue) - { - if ($_.Directory.Name -eq 'en' -and $_.Extension -eq '.json') { - return @{ - SourceFile = $sourceFile - CopyOption = "LangIDOnPath" - OutputPath = "$($_.Directory.Parent.FullName | Resolve-Path -Relative)\" + $locFiles | ForEach-Object { + $outputPath = "$(($_.DirectoryName | Resolve-Path -Relative) + "\")" + $continue = $true + foreach ($exclusion in $exclusions.Exclusions) { + if ($_.FullName.Contains($exclusion)) + { + $continue = $false } - } else { - return @{ - SourceFile = $sourceFile - CopyOption = "LangIDOnName" - OutputPath = $outputPath + } + $sourceFile = ($_.FullName | Resolve-Path -Relative) + if (!$CreateNeutralXlfs -and $_.Extension -eq '.xlf') { + Remove-Item -Path $sourceFile + } + if ($continue) + { + if ($_.Directory.Name -eq 'en' -and $_.Extension -eq '.json') { + return @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnPath" + OutputPath = "$($_.Directory.Parent.FullName | Resolve-Path -Relative)\" + } + } else { + return @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnName" + OutputPath = $outputPath + } } } } - } ) }, @{ @@ -126,24 +126,24 @@ $locJson = @{ CloneLanguageSet = "WiX_CloneLanguages" LssFiles = @( "wxl_loc.lss" ) LocItems = @( - $wxlFilesV3 | ForEach-Object { - $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" - $continue = $true - foreach ($exclusion in $exclusions.Exclusions) { - if ($_.FullName.Contains($exclusion)) { - $continue = $false + $wxlFilesV3 | ForEach-Object { + $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" + $continue = $true + foreach ($exclusion in $exclusions.Exclusions) { + if ($_.FullName.Contains($exclusion)) { + $continue = $false + } } - } - $sourceFile = ($_.FullName | Resolve-Path -Relative) - if ($continue) - { - return @{ - SourceFile = $sourceFile - CopyOption = "LangIDOnPath" - OutputPath = $outputPath + $sourceFile = ($_.FullName | Resolve-Path -Relative) + if ($continue) + { + return @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnPath" + OutputPath = $outputPath + } } } - } ) }, @{ @@ -151,24 +151,24 @@ $locJson = @{ CloneLanguageSet = "WiX_CloneLanguages" LssFiles = @( "P210WxlSchemaV4.lss" ) LocItems = @( - $wxlFilesV5 | ForEach-Object { - $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" - $continue = $true - foreach ($exclusion in $exclusions.Exclusions) { - if ($_.FullName.Contains($exclusion)) { - $continue = $false + $wxlFilesV5 | ForEach-Object { + $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" + $continue = $true + foreach ($exclusion in $exclusions.Exclusions) { + if ($_.FullName.Contains($exclusion)) { + $continue = $false + } } - } - $sourceFile = ($_.FullName | Resolve-Path -Relative) - if ($continue) - { - return @{ - SourceFile = $sourceFile - CopyOption = "LangIDOnPath" - OutputPath = $outputPath + $sourceFile = ($_.FullName | Resolve-Path -Relative) + if ($continue) + { + return @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnPath" + OutputPath = $outputPath + } } } - } ) }, @{ @@ -176,28 +176,28 @@ $locJson = @{ CloneLanguageSet = "VS_macOS_CloneLanguages" LssFiles = @( ".\eng\common\loc\P22DotNetHtmlLocalization.lss" ) LocItems = @( - $macosHtmlFiles | ForEach-Object { - $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" - $continue = $true - foreach ($exclusion in $exclusions.Exclusions) { - if ($_.FullName.Contains($exclusion)) { - $continue = $false - } - } - $sourceFile = ($_.FullName | Resolve-Path -Relative) - $lciFile = $sourceFile + ".lci" - if ($continue) { - $result = @{ - SourceFile = $sourceFile - CopyOption = "LangIDOnPath" - OutputPath = $outputPath + $macosHtmlFiles | ForEach-Object { + $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" + $continue = $true + foreach ($exclusion in $exclusions.Exclusions) { + if ($_.FullName.Contains($exclusion)) { + $continue = $false + } } - if (Test-Path $lciFile -PathType Leaf) { - $result["LciFile"] = $lciFile + $sourceFile = ($_.FullName | Resolve-Path -Relative) + $lciFile = $sourceFile + ".lci" + if ($continue) { + $result = @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnPath" + OutputPath = $outputPath + } + if (Test-Path $lciFile -PathType Leaf) { + $result["LciFile"] = $lciFile + } + return $result } - return $result } - } ) } ) diff --git a/eng/common/internal-feed-operations.ps1 b/eng/common/internal-feed-operations.ps1 index 92b77347d99..c282d3ae403 100644 --- a/eng/common/internal-feed-operations.ps1 +++ b/eng/common/internal-feed-operations.ps1 @@ -26,7 +26,7 @@ function SetupCredProvider { $url = 'https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.ps1' Write-Host "Writing the contents of 'installcredprovider.ps1' locally..." - Invoke-WebRequest $url -OutFile installcredprovider.ps1 + Invoke-WebRequest $url -UseBasicParsing -OutFile installcredprovider.ps1 Write-Host 'Installing plugin...' .\installcredprovider.ps1 -Force diff --git a/eng/common/internal-feed-operations.sh b/eng/common/internal-feed-operations.sh index 6299e7effd4..9378223ba09 100755 --- a/eng/common/internal-feed-operations.sh +++ b/eng/common/internal-feed-operations.sh @@ -100,7 +100,7 @@ operation='' authToken='' repoName='' -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in --operation) diff --git a/eng/common/native/install-dependencies.sh b/eng/common/native/install-dependencies.sh index f7bd4af0c8d..477a44f335b 100644 --- a/eng/common/native/install-dependencies.sh +++ b/eng/common/native/install-dependencies.sh @@ -30,8 +30,6 @@ case "$os" in elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ]; then pkg_mgr="$(command -v tdnf 2>/dev/null || command -v dnf)" $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio - elif [ "$ID" = "amzn" ]; then - dnf install -y cmake llvm lld lldb clang python libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio elif [ "$ID" = "alpine" ]; then apk add build-base cmake bash curl clang llvm-dev lld lldb krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio else diff --git a/eng/common/post-build/nuget-verification.ps1 b/eng/common/post-build/nuget-verification.ps1 index ac5c69ffcac..eea88e653c9 100644 --- a/eng/common/post-build/nuget-verification.ps1 +++ b/eng/common/post-build/nuget-verification.ps1 @@ -65,7 +65,7 @@ if ($NuGetExePath) { Write-Host "Downloading nuget.exe from $nugetExeUrl..." $ProgressPreference = 'SilentlyContinue' try { - Invoke-WebRequest $nugetExeUrl -OutFile $downloadedNuGetExe + Invoke-WebRequest $nugetExeUrl -UseBasicParsing -OutFile $downloadedNuGetExe $ProgressPreference = 'Continue' } catch { $ProgressPreference = 'Continue' diff --git a/eng/common/post-build/redact-logs.ps1 b/eng/common/post-build/redact-logs.ps1 index b7fc1959150..472d5bb562c 100644 --- a/eng/common/post-build/redact-logs.ps1 +++ b/eng/common/post-build/redact-logs.ps1 @@ -7,8 +7,9 @@ param( # File with strings to redact - separated by newlines. # For comments start the line with '# ' - such lines are ignored [Parameter(Mandatory=$false)][string] $TokensFilePath, - [Parameter(ValueFromRemainingArguments=$true)][String[]]$TokensToRedact -) + [Parameter(ValueFromRemainingArguments=$true)][String[]]$TokensToRedact, + [Parameter(Mandatory=$false)][string] $runtimeSourceFeed, + [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey) try { $ErrorActionPreference = 'Stop' diff --git a/eng/common/sdk-task.ps1 b/eng/common/sdk-task.ps1 index 4655af7a2d8..b64b66a6275 100644 --- a/eng/common/sdk-task.ps1 +++ b/eng/common/sdk-task.ps1 @@ -9,6 +9,8 @@ Param( [switch][Alias('nobl')]$excludeCIBinaryLog, [switch]$noWarnAsError, [switch] $help, + [string] $runtimeSourceFeed = '', + [string] $runtimeSourceFeedKey = '', [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties ) @@ -68,7 +70,7 @@ try { $GlobalJson.tools | Add-Member -Name "vs" -Value (ConvertFrom-Json "{ `"version`": `"16.5`" }") -MemberType NoteProperty } if( -not ($GlobalJson.tools.PSObject.Properties.Name -match "xcopy-msbuild" )) { - $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "17.14.16" -MemberType NoteProperty + $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "18.0.0" -MemberType NoteProperty } if ($GlobalJson.tools."xcopy-msbuild".Trim() -ine "none") { $xcopyMSBuildToolsFolder = InitializeXCopyMSBuild $GlobalJson.tools."xcopy-msbuild" -install $true diff --git a/eng/common/templates/steps/vmr-sync.yml b/eng/common/templates/steps/vmr-sync.yml index 599afb6186b..eb619c50268 100644 --- a/eng/common/templates/steps/vmr-sync.yml +++ b/eng/common/templates/steps/vmr-sync.yml @@ -38,27 +38,6 @@ steps: displayName: Label PR commit workingDirectory: $(Agent.BuildDirectory)/repo -- script: | - vmr_sha=$(grep -oP '(?<=Sha=")[^"]*' $(Agent.BuildDirectory)/repo/eng/Version.Details.xml) - echo "##vso[task.setvariable variable=vmr_sha]$vmr_sha" - displayName: Obtain the vmr sha from Version.Details.xml (Unix) - condition: ne(variables['Agent.OS'], 'Windows_NT') - workingDirectory: $(Agent.BuildDirectory)/repo - -- powershell: | - [xml]$xml = Get-Content -Path $(Agent.BuildDirectory)/repo/eng/Version.Details.xml - $vmr_sha = $xml.SelectSingleNode("//Source").Sha - Write-Output "##vso[task.setvariable variable=vmr_sha]$vmr_sha" - displayName: Obtain the vmr sha from Version.Details.xml (Windows) - condition: eq(variables['Agent.OS'], 'Windows_NT') - workingDirectory: $(Agent.BuildDirectory)/repo - -- script: | - git fetch --all - git checkout $(vmr_sha) - displayName: Checkout VMR at correct sha for repo flow - workingDirectory: ${{ parameters.vmrPath }} - - script: | git config --global user.name "dotnet-maestro[bot]" git config --global user.email "dotnet-maestro[bot]@users.noreply.github.com" diff --git a/eng/common/templates/variables/pool-providers.yml b/eng/common/templates/variables/pool-providers.yml index e0b19c14a07..18693ea120d 100644 --- a/eng/common/templates/variables/pool-providers.yml +++ b/eng/common/templates/variables/pool-providers.yml @@ -23,7 +23,7 @@ # # pool: # name: $(DncEngInternalBuildPool) -# demands: ImageOverride -equals windows.vs2019.amd64 +# demands: ImageOverride -equals windows.vs2022.amd64 variables: - ${{ if eq(variables['System.TeamProject'], 'internal') }}: - template: /eng/common/templates-official/variables/pool-providers.yml diff --git a/eng/common/templates/vmr-build-pr.yml b/eng/common/templates/vmr-build-pr.yml index ce3c29a62fa..2f3694fa132 100644 --- a/eng/common/templates/vmr-build-pr.yml +++ b/eng/common/templates/vmr-build-pr.yml @@ -34,6 +34,7 @@ resources: type: github name: dotnet/dotnet endpoint: dotnet + ref: refs/heads/main # Set to whatever VMR branch the PR build should insert into stages: - template: /eng/pipelines/templates/stages/vmr-build.yml@vmr diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 4bc50bd568c..977a2d4b103 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -277,7 +277,7 @@ function GetDotNetInstallScript([string] $dotnetRoot) { Retry({ Write-Host "GET $uri" - Invoke-WebRequest $uri -OutFile $installScript + Invoke-WebRequest $uri -UseBasicParsing -OutFile $installScript }) } @@ -394,8 +394,8 @@ function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements = # If the version of msbuild is going to be xcopied, # use this version. Version matches a package here: - # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/17.14.16 - $defaultXCopyMSBuildVersion = '17.14.16' + # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/18.0.0 + $defaultXCopyMSBuildVersion = '18.0.0' if (!$vsRequirements) { if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') { @@ -510,7 +510,7 @@ function InitializeXCopyMSBuild([string]$packageVersion, [bool]$install) { Write-Host "Downloading $packageName $packageVersion" $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit Retry({ - Invoke-WebRequest "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/flat2/$packageName/$packageVersion/$packageName.$packageVersion.nupkg" -OutFile $packagePath + Invoke-WebRequest "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/flat2/$packageName/$packageVersion/$packageName.$packageVersion.nupkg" -UseBasicParsing -OutFile $packagePath }) if (!(Test-Path $packagePath)) { @@ -556,23 +556,30 @@ function LocateVisualStudio([object]$vsRequirements = $null){ Write-Host "Downloading vswhere $vswhereVersion" $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit Retry({ - Invoke-WebRequest "https://netcorenativeassets.blob.core.windows.net/resource-packages/external/windows/vswhere/$vswhereVersion/vswhere.exe" -OutFile $vswhereExe + Invoke-WebRequest "https://netcorenativeassets.blob.core.windows.net/resource-packages/external/windows/vswhere/$vswhereVersion/vswhere.exe" -UseBasicParsing -OutFile $vswhereExe }) } - if (!$vsRequirements) { $vsRequirements = $GlobalJson.tools.vs } + if (!$vsRequirements) { + if (Get-Member -InputObject $GlobalJson.tools -Name 'vs' -ErrorAction SilentlyContinue) { + $vsRequirements = $GlobalJson.tools.vs + } else { + $vsRequirements = $null + } + } + $args = @('-latest', '-format', 'json', '-requires', 'Microsoft.Component.MSBuild', '-products', '*') if (!$excludePrereleaseVS) { $args += '-prerelease' } - if (Get-Member -InputObject $vsRequirements -Name 'version') { + if ($vsRequirements -and (Get-Member -InputObject $vsRequirements -Name 'version' -ErrorAction SilentlyContinue)) { $args += '-version' $args += $vsRequirements.version } - if (Get-Member -InputObject $vsRequirements -Name 'components') { + if ($vsRequirements -and (Get-Member -InputObject $vsRequirements -Name 'components' -ErrorAction SilentlyContinue)) { foreach ($component in $vsRequirements.components) { $args += '-requires' $args += $component @@ -817,6 +824,11 @@ function MSBuild-Core() { $cmdArgs = "$($buildTool.Command) /m /nologo /clp:Summary /v:$verbosity /nr:$nodeReuse /p:ContinuousIntegrationBuild=$ci" + # Add -mt flag for MSBuild multithreaded mode if enabled via environment variable + if ($env:MSBUILD_MT_ENABLED -eq "1") { + $cmdArgs += ' -mt' + } + if ($warnAsError) { $cmdArgs += ' /warnaserror /p:TreatWarningsAsErrors=true' } diff --git a/eng/common/tools.sh b/eng/common/tools.sh index c1841c9dfd0..1b296f646c2 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -526,7 +526,13 @@ function MSBuild-Core { } } - RunBuildTool "$_InitializeBuildToolCommand" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci "$@" + # Add -mt flag for MSBuild multithreaded mode if enabled via environment variable + local mt_switch="" + if [[ "${MSBUILD_MT_ENABLED:-}" == "1" ]]; then + mt_switch="-mt" + fi + + RunBuildTool "$_InitializeBuildToolCommand" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch $mt_switch /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci "$@" } function GetDarc { diff --git a/eng/common/vmr-sync.ps1 b/eng/common/vmr-sync.ps1 index 97302f3205b..b37992d91cf 100644 --- a/eng/common/vmr-sync.ps1 +++ b/eng/common/vmr-sync.ps1 @@ -103,12 +103,20 @@ Set-StrictMode -Version Latest Highlight 'Installing .NET, preparing the tooling..' . .\eng\common\tools.ps1 $dotnetRoot = InitializeDotNetCli -install:$true +$env:DOTNET_ROOT = $dotnetRoot $darc = Get-Darc -$dotnet = "$dotnetRoot\dotnet.exe" Highlight "Starting the synchronization of VMR.." # Synchronize the VMR +$versionDetailsPath = Resolve-Path (Join-Path $PSScriptRoot '..\Version.Details.xml') | Select-Object -ExpandProperty Path +[xml]$versionDetails = Get-Content -Path $versionDetailsPath +$repoName = $versionDetails.SelectSingleNode('//Source').Mapping +if (-not $repoName) { + Fail "Failed to resolve repo mapping from $versionDetailsPath" + exit 1 +} + $darcArgs = ( "vmr", "forwardflow", "--tmp", $tmpDir, @@ -130,9 +138,27 @@ if ($LASTEXITCODE -eq 0) { Highlight "Synchronization succeeded" } else { - Fail "Synchronization of repo to VMR failed!" - Fail "'$vmrDir' is left in its last state (re-run of this script will reset it)." - Fail "Please inspect the logs which contain path to the failing patch file (use -debugOutput to get all the details)." - Fail "Once you make changes to the conflicting VMR patch, commit it locally and re-run this script." - exit 1 + Highlight "Failed to flow code into the local VMR. Falling back to resetting the VMR to match repo contents..." + git -C $vmrDir reset --hard + + $resetArgs = ( + "vmr", "reset", + "${repoName}:HEAD", + "--vmr", $vmrDir, + "--tmp", $tmpDir, + "--additional-remotes", "${repoName}:${repoRoot}" + ) + + & "$darc" $resetArgs + + if ($LASTEXITCODE -eq 0) { + Highlight "Successfully reset the VMR using 'darc vmr reset'" + } + else { + Fail "Synchronization of repo to VMR failed!" + Fail "'$vmrDir' is left in its last state (re-run of this script will reset it)." + Fail "Please inspect the logs which contain path to the failing patch file (use -debugOutput to get all the details)." + Fail "Once you make changes to the conflicting VMR patch, commit it locally and re-run this script." + exit 1 + } } diff --git a/eng/common/vmr-sync.sh b/eng/common/vmr-sync.sh index 44239e331c0..198caec59bd 100644 --- a/eng/common/vmr-sync.sh +++ b/eng/common/vmr-sync.sh @@ -186,6 +186,13 @@ fi # Synchronize the VMR +version_details_path=$(cd "$scriptroot/.."; pwd -P)/Version.Details.xml +repo_name=$(grep -m 1 '/dev/null; then + echo " Uninstalling $TAP_NAME/$caskName..." + brew uninstall --cask "$TAP_NAME/$caskName" + echo " Uninstalled." + fi + done + + if brew tap-info "$TAP_NAME" &>/dev/null; then + echo " Removing tap $TAP_NAME..." + brew untap "$TAP_NAME" + echo " Removed." + fi + + echo "" + echo "Done. Dogfood install removed." + exit 0 +} + +CASK_FILE="" +UNINSTALL=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --uninstall) UNINSTALL=true; shift ;; + --help) usage ;; + -*) echo "Unknown option: $1"; usage ;; + *) CASK_FILE="$1"; shift ;; + esac +done + +if [[ "$UNINSTALL" == true ]]; then + uninstall +fi + +# Auto-detect cask file if not specified +if [[ -z "$CASK_FILE" ]]; then + for candidate in "$SCRIPT_DIR/aspire.rb" "$SCRIPT_DIR/aspire@prerelease.rb"; do + if [[ -f "$candidate" ]]; then + CASK_FILE="$candidate" + break + fi + done + + if [[ -z "$CASK_FILE" ]]; then + echo "Error: No cask file found in $SCRIPT_DIR" + echo "Expected aspire.rb or aspire@prerelease.rb" + exit 1 + fi +fi + +if [[ ! -f "$CASK_FILE" ]]; then + echo "Error: Cask file not found: $CASK_FILE" + exit 1 +fi + +CASK_FILE="$(cd "$(dirname "$CASK_FILE")" && pwd)/$(basename "$CASK_FILE")" +CASK_FILENAME="$(basename "$CASK_FILE")" +CASK_NAME="${CASK_FILENAME%.rb}" + +echo "Aspire CLI Homebrew Dogfood Installer" +echo "======================================" +echo " Cask file: $CASK_FILE" +echo " Cask name: $CASK_NAME" +echo "" + +# Check for conflicts with official installs +for check in "aspire" "aspire@prerelease"; do + if brew list --cask "$check" &>/dev/null; then + echo "Error: '$check' is already installed from the official Homebrew tap." + echo "Uninstall it first with: brew uninstall --cask $check" + exit 1 + fi +done + +# Check for leftover local/aspire tap from pipeline testing +if brew tap-info "local/aspire" &>/dev/null 2>&1; then + echo "Error: A 'local/aspire' tap already exists (likely from a pipeline test run)." + echo "Remove it first with: brew untap local/aspire" + exit 1 +fi + +if brew tap-info "local/aspire-test" &>/dev/null 2>&1; then + echo "Error: A 'local/aspire-test' tap already exists (likely from a pipeline test run)." + echo "Remove it first with: brew untap local/aspire-test" + exit 1 +fi + +# Clean up any previous dogfood tap +if brew tap-info "$TAP_NAME" &>/dev/null 2>&1; then + echo "Removing previous dogfood tap..." + # Uninstall any casks from the old tap first + for old in "aspire" "aspire@prerelease"; do + if brew list --cask "$TAP_NAME/$old" &>/dev/null; then + brew uninstall --cask "$TAP_NAME/$old" || true + fi + done + brew untap "$TAP_NAME" +fi + +# Set up local tap +echo "Setting up local tap ($TAP_NAME)..." +brew tap-new --no-git "$TAP_NAME" +tapOrg="${TAP_NAME%%/*}" +tapRepo="${TAP_NAME##*/}" +tapRoot="$(brew --repository)/Library/Taps/$tapOrg/homebrew-$tapRepo" +tapCaskDir="$tapRoot/Casks" +mkdir -p "$tapCaskDir" +cp "$CASK_FILE" "$tapCaskDir/$CASK_FILENAME" + +# Install +echo "" +echo "Installing $CASK_NAME from local tap..." +# Disable auto-update during install — auto-update can re-index the tap before +# the cask file is picked up, causing a "cask unavailable" error. +HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask "$TAP_NAME/$CASK_NAME" + +# Verify +echo "" +if command -v aspire &>/dev/null; then + echo "Installed successfully!" + echo " Path: $(command -v aspire)" + aspireVersion="$(aspire --version 2>&1)" || true + echo " Version: $aspireVersion" +else + echo "Warning: aspire command not found in PATH after install." + echo "You may need to restart your shell or add the install location to your PATH." +fi + +echo "" +echo "To uninstall: $(basename "$0") --uninstall" diff --git a/eng/homebrew/generate-cask.sh b/eng/homebrew/generate-cask.sh new file mode 100755 index 00000000000..d45960d5f97 --- /dev/null +++ b/eng/homebrew/generate-cask.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generates a Homebrew cask from the template by downloading archives and computing SHA256 hashes. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +usage() { + cat <&2 + echo " URL: $url" >&2 + + tmpfile="$(mktemp)" + trap 'rm -f "$tmpfile"' RETURN + + curl -fsSL "$url" -o "$tmpfile" + local hash + hash="$(shasum -a 256 "$tmpfile" | awk '{print $1}')" + echo " SHA256: $hash" >&2 + echo "$hash" +} + +compute_sha256_from_file() { + local file_path="$1" + local description="$2" + + echo "Computing SHA256 for $description from local file..." >&2 + echo " Path: $file_path" >&2 + + local hash + hash="$(shasum -a 256 "$file_path" | awk '{print $1}')" + echo " SHA256: $hash" >&2 + echo "$hash" +} + +find_local_archive() { + local archive_name="$1" + local archive_path + + archive_path="$(find "$ARCHIVE_ROOT" -type f -name "$archive_name" -print -quit)" + if [[ -z "$archive_path" ]]; then + echo "Error: Could not find local archive '$archive_name' under '$ARCHIVE_ROOT'" >&2 + exit 1 + fi + + echo "$archive_path" +} + +# Check if a URL is accessible (HEAD request) +url_exists() { + curl -o /dev/null -s --head --fail "$1" +} + +echo "Generating Homebrew cask for Aspire version $VERSION (channel: $CHANNEL)" +echo "" + +# macOS tarballs are required +OSX_ARM64_URL="$BASE_URL/aspire-cli-osx-arm64-$VERSION.tar.gz" +OSX_X64_URL="$BASE_URL/aspire-cli-osx-x64-$VERSION.tar.gz" + +if [[ -n "$ARCHIVE_ROOT" && ! -d "$ARCHIVE_ROOT" ]]; then + echo "Error: --archive-root directory does not exist: $ARCHIVE_ROOT" + exit 1 +fi + +# Validate URLs are accessible before downloading (fast-fail) +if [[ "$VALIDATE_URLS" == true ]]; then + echo "Validating tarball URLs..." + failed=false + for url in "$OSX_ARM64_URL" "$OSX_X64_URL"; do + echo " Checking: $url" + if url_exists "$url"; then + echo " OK" + else + echo " ERROR: URL not accessible" + failed=true + fi + done + + if [[ "$failed" == true ]]; then + echo "Error: One or more tarball URLs are not accessible. Ensure the release artifacts have been published." + exit 1 + fi + echo "" +fi + +if [[ -n "$ARCHIVE_ROOT" ]]; then + OSX_ARM64_ARCHIVE="$(find_local_archive "aspire-cli-osx-arm64-$VERSION.tar.gz")" + OSX_X64_ARCHIVE="$(find_local_archive "aspire-cli-osx-x64-$VERSION.tar.gz")" + + SHA256_OSX_ARM64="$(compute_sha256_from_file "$OSX_ARM64_ARCHIVE" "macOS ARM64 tarball")" + SHA256_OSX_X64="$(compute_sha256_from_file "$OSX_X64_ARCHIVE" "macOS x64 tarball")" +else + SHA256_OSX_ARM64="$(compute_sha256 "$OSX_ARM64_URL" "macOS ARM64 tarball")" + SHA256_OSX_X64="$(compute_sha256 "$OSX_X64_URL" "macOS x64 tarball")" +fi + +echo "" +echo "Generating cask from template..." + +# Read template and perform substitutions +content="$(cat "$TEMPLATE")" +content="${content//\$\{VERSION\}/$VERSION}" +content="${content//\$\{ARTIFACT_VERSION\}/$ARTIFACT_VERSION}" +content="${content//\$\{SHA256_OSX_ARM64\}/$SHA256_OSX_ARM64}" +content="${content//\$\{SHA256_OSX_X64\}/$SHA256_OSX_X64}" + +# Write output +mkdir -p "$(dirname "$OUTPUT")" +printf '%s\n' "$content" > "$OUTPUT" + +echo " Created: $OUTPUT" +echo "" +echo "Next steps:" +echo " 1. Validate syntax: ruby -c \"$OUTPUT\"" +echo " 2. Audit cask: brew audit --cask aspire (after installing to a local tap)" +echo " 3. Test install: brew install --cask \"$OUTPUT\"" diff --git a/eng/pipelines/azure-pipelines-unofficial.yml b/eng/pipelines/azure-pipelines-unofficial.yml index 1fa00cd7d35..02380f00653 100644 --- a/eng/pipelines/azure-pipelines-unofficial.yml +++ b/eng/pipelines/azure-pipelines-unofficial.yml @@ -175,3 +175,139 @@ extends: targetRids: - win-x64 - win-arm64 + - pwsh: | + $ErrorActionPreference = 'Stop' + $shippingDir = "$(Build.SourcesDirectory)/artifacts/packages/$(_BuildConfig)/Shipping" + $nupkg = Get-ChildItem -Path $shippingDir -Filter "Aspire.Hosting.AppHost.*.nupkg" -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notmatch '\.symbols\.' } | + Select-Object -First 1 + + if (-not $nupkg) { + Write-Error "Could not find Aspire.Hosting.AppHost nupkg in $shippingDir" + exit 1 + } + + $version = $nupkg.Name -replace '^Aspire\.Hosting\.AppHost\.', '' -replace '\.nupkg$', '' + + if ($version -notmatch '^\d+\.\d+\.\d+') { + Write-Error "Extracted version '$version' does not look like a valid semver. Check the nupkg name." + exit 1 + } + + Write-Host "Detected Aspire version: $version (from $($nupkg.Name))" + Write-Host "##vso[task.setvariable variable=aspireVersion;isOutput=true]$version" + name: computeVersion + displayName: 🟣Compute Aspire version from nupkg + + - pwsh: | + $ErrorActionPreference = 'Stop' + $artifactVersion = dotnet msbuild "$(Build.SourcesDirectory)/src/Aspire.Dashboard/Aspire.Dashboard.csproj" -getProperty:PackageVersion -property:SuppressFinalPackageVersion=true -property:OfficialBuildId=$(BUILD.BUILDNUMBER) + $artifactVersion = $artifactVersion.Trim() + + if ($artifactVersion -notmatch '^\d+\.\d+\.\d+') { + Write-Error "Computed artifact version '$artifactVersion' does not look like a valid semver. Check PackageVersion evaluation." + exit 1 + } + + Write-Host "Detected installer artifact version: $artifactVersion" + Write-Host "##vso[task.setvariable variable=aspireArtifactVersion;isOutput=true]$artifactVersion" + name: computeArtifactVersion + displayName: 🟣Compute installer artifact version + + - pwsh: | + $ErrorActionPreference = 'Stop' + $versionKind = dotnet msbuild "$(Build.SourcesDirectory)/eng/Versions.props" -getProperty:DotNetFinalVersionKind + $versionKind = $versionKind.Trim() + Write-Host "DotNetFinalVersionKind: '$versionKind'" + + if ($versionKind -eq 'release') { + $channel = 'stable' + } else { + $channel = 'prerelease' + } + + Write-Host "Installer channel: $channel" + Write-Host "##vso[task.setvariable variable=installerChannel;isOutput=true]$channel" + name: computeChannel + displayName: 🟣Determine installer channel + + - stage: prepare_installers + displayName: Prepare Installers + dependsOn: + - build + variables: + aspireVersion: $[ stageDependencies.build.Windows.outputs['computeVersion.aspireVersion'] ] + aspireArtifactVersion: $[ stageDependencies.build.Windows.outputs['computeArtifactVersion.aspireArtifactVersion'] ] + installerChannel: $[ stageDependencies.build.Windows.outputs['computeChannel.installerChannel'] ] + condition: | + and( + succeeded(), + ne(variables['Build.Reason'], 'PullRequest'), + or( + ne(dependencies.build.outputs['Windows.computeChannel.installerChannel'], 'stable'), + or( + eq(variables['Build.SourceBranch'], 'refs/heads/main'), + startsWith(variables['Build.SourceBranch'], 'refs/heads/release/') + ) + ) + ) + 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 + displayName: WinGet Manifest + timeoutInMinutes: 30 + pool: + name: NetCore1ESPool-Internal + image: 1es-windows-2022 + os: windows + steps: + - checkout: self + fetchDepth: 1 + - task: DownloadPipelineArtifact@2 + displayName: 🟣Download CLI archives + inputs: + itemPattern: | + **/aspire-cli-*.zip + targetPath: '$(Pipeline.Workspace)/native-archives' + - template: /eng/pipelines/templates/prepare-winget-manifest.yml@self + parameters: + version: $(aspireVersion) + artifactVersion: $(aspireArtifactVersion) + channel: $(installerChannel) + archiveRoot: $(Pipeline.Workspace)/native-archives + validateUrls: false + runInstallTest: false + + - job: Homebrew + displayName: Homebrew Cask + timeoutInMinutes: 30 + pool: + name: Azure Pipelines + vmImage: macOS-latest-internal + os: macOS + steps: + - checkout: self + fetchDepth: 1 + - task: DownloadPipelineArtifact@2 + displayName: 🟣Download CLI archives + inputs: + itemPattern: | + **/aspire-cli-*.tar.gz + targetPath: '$(Pipeline.Workspace)/native-archives' + - template: /eng/pipelines/templates/prepare-homebrew-cask.yml@self + parameters: + version: $(aspireVersion) + artifactVersion: $(aspireArtifactVersion) + channel: $(installerChannel) + archiveRoot: $(Pipeline.Workspace)/native-archives + validateUrls: false + runInstallTest: false diff --git a/eng/pipelines/azure-pipelines.yml b/eng/pipelines/azure-pipelines.yml index 9fd23b5ca1d..17c57111925 100644 --- a/eng/pipelines/azure-pipelines.yml +++ b/eng/pipelines/azure-pipelines.yml @@ -265,6 +265,66 @@ extends: targetRids: - win-x64 - win-arm64 + # Extract the Aspire version from a generated nupkg filename so downstream + # stages can use it. Aspire.Hosting.AppHost has no RID in its filename and + # does not suppress final package version stabilization, so stripping the + # prefix cleanly yields the stable version for installer publishing. + - pwsh: | + $ErrorActionPreference = 'Stop' + $shippingDir = "$(Build.SourcesDirectory)/artifacts/packages/$(_BuildConfig)/Shipping" + $nupkg = Get-ChildItem -Path $shippingDir -Filter "Aspire.Hosting.AppHost.*.nupkg" -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notmatch '\.symbols\.' } | + Select-Object -First 1 + + if (-not $nupkg) { + Write-Error "Could not find Aspire.Hosting.AppHost nupkg in $shippingDir" + exit 1 + } + + $version = $nupkg.Name -replace '^Aspire\.Hosting\.AppHost\.', '' -replace '\.nupkg$', '' + + if ($version -notmatch '^\d+\.\d+\.\d+') { + Write-Error "Extracted version '$version' does not look like a valid semver. Check the nupkg name." + exit 1 + } + + Write-Host "Detected Aspire version: $version (from $($nupkg.Name))" + Write-Host "##vso[task.setvariable variable=aspireVersion;isOutput=true]$version" + name: computeVersion + displayName: 🟣Compute Aspire version from nupkg + + - pwsh: | + $ErrorActionPreference = 'Stop' + $artifactVersion = dotnet msbuild "$(Build.SourcesDirectory)/src/Aspire.Dashboard/Aspire.Dashboard.csproj" -getProperty:PackageVersion -property:SuppressFinalPackageVersion=true -property:OfficialBuildId=$(BUILD.BUILDNUMBER) + $artifactVersion = $artifactVersion.Trim() + + if ($artifactVersion -notmatch '^\d+\.\d+\.\d+') { + Write-Error "Computed artifact version '$artifactVersion' does not look like a valid semver. Check PackageVersion evaluation." + exit 1 + } + + Write-Host "Detected installer artifact version: $artifactVersion" + Write-Host "##vso[task.setvariable variable=aspireArtifactVersion;isOutput=true]$artifactVersion" + name: computeArtifactVersion + displayName: 🟣Compute installer artifact version + + # Determine stable vs prerelease for installer pipelines. + - pwsh: | + $ErrorActionPreference = 'Stop' + $versionKind = dotnet msbuild "$(Build.SourcesDirectory)/eng/Versions.props" -getProperty:DotNetFinalVersionKind + $versionKind = $versionKind.Trim() + Write-Host "DotNetFinalVersionKind: '$versionKind'" + + if ($versionKind -eq 'release') { + $channel = 'stable' + } else { + $channel = 'prerelease' + } + + Write-Host "Installer channel: $channel" + Write-Host "##vso[task.setvariable variable=installerChannel;isOutput=true]$channel" + name: computeChannel + displayName: 🟣Determine installer channel - ${{ if and(notin(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranch'], 'refs/heads/main')) }}: - template: /eng/common/templates-official/job/onelocbuild.yml@self parameters: @@ -272,3 +332,71 @@ extends: LclPackageId: 'LCL-JUNO-PROD-ASPIRE' MirrorRepo: aspire MirrorBranch: main + + # ---------------------------------------------------------------- + # Generate and test installer packages (WinGet + Homebrew). + # Artifacts are consumed by release-publish-nuget.yml for stable + # publishing. No publishing happens from this pipeline. + # ---------------------------------------------------------------- + - stage: prepare_installers + displayName: Prepare Installers + dependsOn: + - build + variables: + aspireVersion: $[ stageDependencies.build.Windows.outputs['computeVersion.aspireVersion'] ] + aspireArtifactVersion: $[ stageDependencies.build.Windows.outputs['computeArtifactVersion.aspireArtifactVersion'] ] + installerChannel: $[ stageDependencies.build.Windows.outputs['computeChannel.installerChannel'] ] + condition: | + and( + succeeded(), + ne(variables['Build.Reason'], 'PullRequest'), + or( + ne(dependencies.build.outputs['Windows.computeChannel.installerChannel'], 'stable'), + or( + eq(variables['Build.SourceBranch'], 'refs/heads/main'), + startsWith(variables['Build.SourceBranch'], 'refs/heads/release/') + ) + ) + ) + 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 + displayName: WinGet Manifest + timeoutInMinutes: 30 + pool: + name: NetCore1ESPool-Internal + image: 1es-windows-2022 + os: windows + steps: + - checkout: self + fetchDepth: 1 + - template: /eng/pipelines/templates/prepare-winget-manifest.yml@self + parameters: + version: $(aspireVersion) + artifactVersion: $(aspireArtifactVersion) + channel: $(installerChannel) + + - job: Homebrew + displayName: Homebrew Cask + timeoutInMinutes: 30 + pool: + name: Azure Pipelines + vmImage: macOS-latest-internal + os: macOS + steps: + - checkout: self + fetchDepth: 1 + - template: /eng/pipelines/templates/prepare-homebrew-cask.yml@self + parameters: + version: $(aspireVersion) + artifactVersion: $(aspireArtifactVersion) + channel: $(installerChannel) diff --git a/eng/pipelines/release-publish-nuget.yml b/eng/pipelines/release-publish-nuget.yml new file mode 100644 index 00000000000..22455eed2ff --- /dev/null +++ b/eng/pipelines/release-publish-nuget.yml @@ -0,0 +1,551 @@ +# Release Pipeline: Publish NuGet Packages and Promote to GA Channel +# +# This pipeline automates the release process for dotnet/aspire: +# 1. Downloads signed packages from a specified build +# 2. Publishes packages to NuGet.org +# 3. Promotes the build to the Aspire GA channel via darc +# 4. Submits WinGet manifests and Homebrew cask PRs +# +# For full documentation, see: docs/release-process.md + +trigger: none # Manual trigger only +pr: none # Not triggered by PRs + +parameters: + - name: GaChannelName + displayName: 'GA Channel Name' + type: string + default: 'Aspire 9.x GA' + + - name: DryRun + displayName: 'Dry Run (skip actual publish - for testing)' + type: boolean + default: false + + # Idempotency flags for re-running after partial failures + - name: SkipNuGetPublish + displayName: 'Skip NuGet Publishing (set true if already completed)' + type: boolean + default: false + + - name: SkipChannelPromotion + displayName: 'Skip Channel Promotion (set true if already completed)' + type: boolean + default: false + + - name: SkipWinGetPublish + displayName: 'Skip WinGet Publishing (set true if already completed)' + type: boolean + default: false + + - name: SkipHomebrewPublish + displayName: 'Skip Homebrew Publishing (set true if already completed)' + type: boolean + default: false + +variables: + - template: /eng/pipelines/common-variables.yml@self + - template: /eng/common/templates-official/variables/pool-providers.yml@self + # Variable group containing secrets (VscePublishToken for future use) + # Note: NuGet publishing uses service connection 'NuGet.org - dotnet/aspire' instead of API key + - group: Aspire-Release-Secrets + # Variable group containing aspire-winget-bot-pat and aspire-homebrew-bot-pat for WinGet and Homebrew publishing + - group: Aspire-Secrets + +resources: + repositories: + - repository: 1ESPipelineTemplates + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + pipelines: + - pipeline: aspire-build + source: dotnet-aspire # Name of the main build pipeline + project: internal # AzDO project name + trigger: none # Manual trigger only - no automatic releases + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates + parameters: + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + # Required for publishing to external feeds like nuget.org + # Pattern from dotnet-workload-versions/official.yml + # Note: 'Permissive,CFSClean' blocks NuGet.org - must use just 'Permissive' + settings: + networkIsolationPolicy: Permissive + + stages: + # ===== STAGE 1: PREPARE ARTIFACTS ===== + # This buildJob downloads artifacts from the source build and re-publishes them. + # 1ES PT will inject SBOM generation tasks, making the artifacts compliant for releaseJob. + - stage: PrepareArtifacts + displayName: 'Prepare Artifacts with SBOM' + jobs: + - job: PrepareJob + displayName: 'Download and Re-publish Artifacts' + timeoutInMinutes: 30 + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + variables: + SourceBuildId: $(resources.pipeline.aspire-build.runID) + templateContext: + outputs: + - output: pipelineArtifact + displayName: 'Publish PackageArtifacts with SBOM' + targetPath: '$(Pipeline.Workspace)/packages/PackageArtifacts' + artifactName: 'PackageArtifacts' + - output: pipelineArtifact + displayName: 'Publish WinGet Manifests' + targetPath: '$(Pipeline.Workspace)/installers/winget-manifests-stable' + artifactName: 'winget-manifests-stable' + - output: pipelineArtifact + displayName: 'Publish Homebrew Cask' + targetPath: '$(Pipeline.Workspace)/installers/homebrew-cask-stable' + artifactName: 'homebrew-cask-stable' + steps: + - checkout: none + + - powershell: | + Write-Host "=== Prepare Artifacts Stage ===" + Write-Host "Source Build ID: $(resources.pipeline.aspire-build.runID)" + Write-Host "Source Build Name: $(resources.pipeline.aspire-build.runName)" + Write-Host "This stage downloads artifacts and re-publishes them so 1ES PT can generate SBOM." + Write-Host "Installer-only mode: ${{ and(eq(parameters.SkipNuGetPublish, true), eq(parameters.SkipChannelPromotion, true)) }}" + Write-Host "===============================" + displayName: 'Log Stage Info' + + # Download artifacts from the source build pipeline + - ${{ if not(and(eq(parameters.SkipNuGetPublish, true), eq(parameters.SkipChannelPromotion, true))) }}: + - download: aspire-build + displayName: 'Download PackageArtifacts from Source Build' + artifact: PackageArtifacts + patterns: '**/*.nupkg' + + - download: aspire-build + displayName: 'Download WinGet Manifests from Source Build' + artifact: winget-manifests-stable + + - download: aspire-build + displayName: 'Download Homebrew Cask from Source Build' + artifact: homebrew-cask-stable + + # Move artifacts to expected location for output + - ${{ if not(and(eq(parameters.SkipNuGetPublish, true), eq(parameters.SkipChannelPromotion, true))) }}: + - powershell: | + $sourcePath = "$(Pipeline.Workspace)/aspire-build/PackageArtifacts" + $targetPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" + + Write-Host "Moving artifacts from $sourcePath to $targetPath" + + if (!(Test-Path $targetPath)) { + New-Item -ItemType Directory -Path $targetPath -Force | Out-Null + } + + # Copy all nupkg files + $packages = Get-ChildItem -Path $sourcePath -Filter "*.nupkg" -Recurse + Write-Host "Found $($packages.Count) packages to copy" + + foreach ($pkg in $packages) { + Copy-Item $pkg.FullName -Destination $targetPath -Force + Write-Host " Copied: $($pkg.Name)" + } + + Write-Host "✓ Artifacts prepared for SBOM generation" + displayName: 'Prepare Artifacts for Publishing' + + - ${{ if and(eq(parameters.SkipNuGetPublish, true), eq(parameters.SkipChannelPromotion, true)) }}: + - powershell: | + $targetPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" + + if (!(Test-Path $targetPath)) { + New-Item -ItemType Directory -Path $targetPath -Force | Out-Null + } + + Write-Host "Installer-only run detected; created empty PackageArtifacts placeholder." + displayName: 'Prepare Empty PackageArtifacts Placeholder' + + # Copy installer artifacts to expected locations for output + - powershell: | + $artifacts = @( + @{ Name = 'winget-manifests-stable'; Source = '$(Pipeline.Workspace)/aspire-build/winget-manifests-stable'; Target = '$(Pipeline.Workspace)/installers/winget-manifests-stable' }, + @{ Name = 'homebrew-cask-stable'; Source = '$(Pipeline.Workspace)/aspire-build/homebrew-cask-stable'; Target = '$(Pipeline.Workspace)/installers/homebrew-cask-stable' } + ) + + foreach ($artifact in $artifacts) { + Write-Host "Copying $($artifact.Name) from $($artifact.Source) to $($artifact.Target)" + + if (!(Test-Path $artifact.Target)) { + New-Item -ItemType Directory -Path $artifact.Target -Force | Out-Null + } + + $files = Get-ChildItem -Path $artifact.Source -Recurse -File + Write-Host " Found $($files.Count) files" + foreach ($file in $files) { + $relativePath = $file.FullName.Substring($artifact.Source.Length + 1) + $destPath = Join-Path $artifact.Target $relativePath + $destDir = Split-Path $destPath -Parent + if (!(Test-Path $destDir)) { + New-Item -ItemType Directory -Path $destDir -Force | Out-Null + } + Copy-Item $file.FullName -Destination $destPath -Force + Write-Host " Copied: $relativePath" + } + } + + Write-Host "✓ Installer artifacts prepared" + displayName: 'Prepare Installer Artifacts' + + # ===== STAGE 2: RELEASE ===== + # This releaseJob consumes the artifacts with SBOM and publishes to NuGet.org + - stage: Release + displayName: 'Release to NuGet and Promote' + dependsOn: PrepareArtifacts + jobs: + - job: ReleaseJob + displayName: 'Validate, Publish, and Promote' + timeoutInMinutes: 120 + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + variables: + BarBuildId: '' + # templateContext type: releaseJob enables proper network access for external publishing + # inputs.pipelineArtifact consumes artifacts from PrepareArtifacts stage (which have SBOM) + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + artifactName: PackageArtifacts + targetPath: '$(Pipeline.Workspace)/packages/PackageArtifacts' + steps: + # ===== VALIDATION ===== + # Note: No checkout - releaseJob doesn't allow it. All scripts are inlined. + # Artifacts are downloaded via templateContext inputs above. + + - powershell: | + Write-Host "=== Release Pipeline Parameters ===" + Write-Host "Source Build Pipeline: aspire-build" + Write-Host "Source Build ID: $(resources.pipeline.aspire-build.runID)" + Write-Host "Source Build Name: $(resources.pipeline.aspire-build.runName)" + Write-Host "Source Build URI: $(resources.pipeline.aspire-build.runURI)" + Write-Host "GA Channel: ${{ parameters.GaChannelName }}" + Write-Host "Dry Run: ${{ parameters.DryRun }}" + Write-Host "Skip NuGet Publish: ${{ parameters.SkipNuGetPublish }}" + Write-Host "Skip Channel Promotion: ${{ parameters.SkipChannelPromotion }}" + Write-Host "Installer-only mode: ${{ and(eq(parameters.SkipNuGetPublish, true), eq(parameters.SkipChannelPromotion, true)) }}" + Write-Host "===================================" + displayName: 'Validate Parameters' + + # ===== EXTRACT BAR BUILD ID ===== + - ${{ if eq(parameters.SkipChannelPromotion, false) }}: + - powershell: | + $buildId = "$(resources.pipeline.aspire-build.runID)" + $org = "$(System.CollectionUri)" + $project = "internal" + + Write-Host "Fetching build tags for build: $buildId" + + # Use Azure DevOps REST API to get build tags + $uri = "${org}${project}/_apis/build/builds/${buildId}/tags?api-version=7.0" + Write-Host "API URI: $uri" + + try { + $response = Invoke-RestMethod -Uri $uri -Headers @{ + Authorization = "Bearer $(System.AccessToken)" + } -Method Get + + Write-Host "Build tags found: $($response.value -join ', ')" + + # Find the BAR ID tag + $barIdTag = $response.value | Where-Object { $_ -match 'BAR ID - (\d+)' } + + if ($barIdTag -and $barIdTag -match 'BAR ID - (\d+)') { + $barBuildId = $Matches[1] + Write-Host "✓ Extracted BAR Build ID: $barBuildId" + Write-Host "##vso[task.setvariable variable=BarBuildId]$barBuildId" + } else { + Write-Error "Could not find BAR ID tag in build $buildId. Tags found: $($response.value -join ', ')" + exit 1 + } + } catch { + Write-Error "Failed to fetch build tags: $_" + exit 1 + } + displayName: 'Extract BAR Build ID' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + # ===== DOWNLOAD PACKAGES ===== + # Artifacts are downloaded automatically via templateContext.inputs above + + - ${{ if eq(parameters.SkipNuGetPublish, false) }}: + - task: UseDotNet@2 + displayName: 'Install .NET 10 SDK' + inputs: + packageType: 'sdk' + version: '10.0.x' + + - powershell: | + $packagesPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" + Write-Host "=== Package Inventory ===" + + $packages = Get-ChildItem -Path $packagesPath -Filter "*.nupkg" -Recurse + Write-Host "Found $($packages.Count) packages:" + + foreach ($pkg in $packages) { + $sizeMB = [math]::Round($pkg.Length / 1MB, 2) + Write-Host " - $($pkg.Name) ($sizeMB MB)" + } + + if ($packages.Count -eq 0) { + Write-Error "No packages found in artifacts!" + exit 1 + } + + Write-Host "===========================" + displayName: 'List Packages' + + # ===== VERIFY SIGNATURES ===== + - powershell: | + $packagesPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" + Write-Host "=== Verifying Package Signatures ===" + + $packages = Get-ChildItem -Path $packagesPath -Filter "*.nupkg" -Recurse + $failedVerification = @() + + foreach ($package in $packages) { + Write-Host "Verifying: $($package.Name)" + + # Use $ErrorActionPreference to prevent PowerShell from treating stderr as terminating error + $originalErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + $result = dotnet nuget verify $package.FullName 2>&1 + $verifyExitCode = $LASTEXITCODE + $ErrorActionPreference = $originalErrorActionPreference + + if ($verifyExitCode -ne 0) { + Write-Host " ❌ Signature verification FAILED" + Write-Host $result + $failedVerification += $package.Name + } else { + Write-Host " ✓ Signature valid" + } + } + + if ($failedVerification.Count -gt 0) { + Write-Host "" + Write-Host "=== SIGNATURE VERIFICATION FAILED ===" + Write-Host "The following packages failed signature verification:" + foreach ($pkg in $failedVerification) { + Write-Host " - $pkg" + } + Write-Error "Package signature verification failed. Aborting release." + exit 1 + } + + Write-Host "" + Write-Host "✓ All $($packages.Count) packages passed signature verification" + Write-Host "===========================" + displayName: 'Verify Package Signatures' + + # ===== PUBLISH TO NUGET ===== + # Dry Run: List packages that would be published (skip actual publish) + - ${{ if and(eq(parameters.DryRun, true), eq(parameters.SkipNuGetPublish, false)) }}: + - powershell: | + $packagesPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" + Write-Host "=== DRY RUN MODE ===" + Write-Host "⚠️ The following packages would be published to NuGet.org:" + Write-Host "" + $packages = Get-ChildItem -Path $packagesPath -Filter "*.nupkg" -Recurse + foreach ($pkg in $packages) { + $sizeMB = [math]::Round($pkg.Length / 1MB, 2) + Write-Host " - $($pkg.Name) ($sizeMB MB)" + } + Write-Host "" + Write-Host "Total: $($packages.Count) packages" + Write-Host "=== DRY RUN - No packages were actually published ===" + displayName: 'Dry Run - List Packages (No Publish)' + + # Actual Publish: Use 1ES.PublishNuget task (only when not dry run and not skipped) + # Pattern from dotnet-diagnostics-internal-components and dotnet-macios: + # - useDotNetTask: false (required - DotNetCoreCLI@2 doesn't support encrypted API keys) + # - nuGetFeedType: external + publishFeedCredentials for NuGet.org + - ${{ if and(eq(parameters.DryRun, false), eq(parameters.SkipNuGetPublish, false)) }}: + - task: 1ES.PublishNuget@1 + displayName: 'Push Packages to NuGet.org' + inputs: + useDotNetTask: false + packagesToPush: '$(Pipeline.Workspace)/packages/PackageArtifacts/*.nupkg' + packageParentPath: '$(Pipeline.Workspace)/packages/PackageArtifacts' + nuGetFeedType: external + publishFeedCredentials: 'NuGet.org - dotnet/aspire' + + # Skip message when SkipNuGetPublish is true + - ${{ if eq(parameters.SkipNuGetPublish, true) }}: + - powershell: | + Write-Host "=== Skipping NuGet Publishing (SkipNuGetPublish=true) ===" + displayName: 'Skip NuGet Publish (flagged)' + + # ===== PROMOTE TO CHANNEL ===== + - ${{ if eq(parameters.SkipChannelPromotion, false) }}: + - powershell: | + Write-Host "Installing darc CLI..." + + # Query Maestro API for the correct darc version + $versionEndpoint = 'https://maestro.dot.net/api/assets/darc-version?api-version=2020-02-20' + $darcVersion = (Invoke-WebRequest -Uri $versionEndpoint -UseBasicParsing).Content + Write-Host "Darc version: $darcVersion" + + # Install darc as a .NET global tool + $arcadeServicesSource = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json' + Write-Host "Installing darc from $arcadeServicesSource..." + + dotnet tool install microsoft.dotnet.darc --version $darcVersion --add-source "$arcadeServicesSource" -g + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to install darc CLI" + exit 1 + } + + Write-Host "✓ Darc CLI installed successfully" + displayName: 'Install darc' + + - task: AzureCLI@2 + displayName: 'Promote Build to Channel' + inputs: + azureSubscription: 'Darc: Maestro Production' + scriptType: 'pscore' + scriptLocation: 'inlineScript' + inlineScript: | + $barBuildId = "$(BarBuildId)" + $channelName = "${{ parameters.GaChannelName }}" + $dryRun = [System.Convert]::ToBoolean("${{ parameters.DryRun }}") + $azdoToken = $env:SYSTEM_ACCESSTOKEN + + Write-Host "=== Channel Promotion ===" + Write-Host "BAR Build ID: $barBuildId" + Write-Host "Target Channel: $channelName" + + if ($dryRun) { + Write-Host "⚠️ DRY RUN MODE - Build will not actually be promoted" + Write-Host "[DRY RUN] Would run: darc add-build-to-channel --id $barBuildId --channel '$channelName' --azdev-pat --ci" + exit 0 + } + + Write-Host "Promoting build $barBuildId to channel '$channelName'..." + + # darc is installed as a global tool, so it's available in PATH + # --azdev-pat is required for darc to access Azure DevOps build artifacts + darc add-build-to-channel ` + --id $barBuildId ` + --channel "$channelName" ` + --azdev-pat "$azdoToken" ` + --ci + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to promote build to channel" + exit 1 + } + + Write-Host "✓ Build successfully promoted to '$channelName'" + Write-Host "===========================" + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + # ===== SUMMARY ===== + - powershell: | + Write-Host "" + Write-Host "╔═══════════════════════════════════════════════════════════════╗" + Write-Host "║ RELEASE SUMMARY ║" + Write-Host "╠═══════════════════════════════════════════════════════════════╣" + Write-Host "║ Source Build: $(resources.pipeline.aspire-build.runName)" + Write-Host "║ Source Build ID: $(resources.pipeline.aspire-build.runID)" + if ("${{ parameters.SkipChannelPromotion }}" -eq "true") { + Write-Host "║ BAR Build ID: (SKIPPED)" + } else { + Write-Host "║ BAR Build ID: $(BarBuildId)" + } + Write-Host "║ GA Channel: ${{ parameters.GaChannelName }}" + Write-Host "║ Dry Run: ${{ parameters.DryRun }}" + Write-Host "╠═══════════════════════════════════════════════════════════════╣" + Write-Host "║ NuGet Publish: ${{ parameters.SkipNuGetPublish }}" -NoNewline + if ("${{ parameters.SkipNuGetPublish }}" -eq "true") { + Write-Host " (SKIPPED)" + } else { + Write-Host " (EXECUTED)" + } + Write-Host "║ Channel Promo: ${{ parameters.SkipChannelPromotion }}" -NoNewline + if ("${{ parameters.SkipChannelPromotion }}" -eq "true") { + Write-Host " (SKIPPED)" + } else { + Write-Host " (EXECUTED)" + } + Write-Host "║ WinGet: (runs in parallel after this job)" + Write-Host "║ Homebrew: (runs in parallel after this job)" + Write-Host "╚═══════════════════════════════════════════════════════════════╝" + Write-Host "" + displayName: 'Print Summary' + condition: always() + + # ===== WINGET PUBLISHING ===== + - job: WinGetJob + displayName: 'Submit WinGet Release Manifests' + dependsOn: ReleaseJob + condition: | + and( + in(dependencies.ReleaseJob.result, 'Succeeded', 'SucceededWithIssues'), + eq('${{ parameters.SkipWinGetPublish }}', 'false') + ) + timeoutInMinutes: 30 + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + artifactName: winget-manifests-stable + targetPath: '$(Pipeline.Workspace)/winget/winget-manifests-stable' + steps: + - template: /eng/pipelines/templates/publish-winget.yml@self + parameters: + manifestArtifactPath: $(Pipeline.Workspace)/winget/winget-manifests-stable + packageIdentifier: Microsoft.Aspire + dryRun: ${{ parameters.DryRun }} + + # ===== HOMEBREW PUBLISHING ===== + - job: HomebrewJob + displayName: 'Submit Homebrew Release Cask' + dependsOn: ReleaseJob + condition: | + and( + in(dependencies.ReleaseJob.result, 'Succeeded', 'SucceededWithIssues'), + eq('${{ parameters.SkipHomebrewPublish }}', 'false') + ) + timeoutInMinutes: 30 + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + artifactName: homebrew-cask-stable + targetPath: '$(Pipeline.Workspace)/homebrew/homebrew-cask-stable' + steps: + - template: /eng/pipelines/templates/publish-homebrew.yml@self + parameters: + caskArtifactPath: $(Pipeline.Workspace)/homebrew/homebrew-cask-stable + channel: stable + dryRun: ${{ parameters.DryRun }} diff --git a/eng/pipelines/templates/prepare-homebrew-cask.yml b/eng/pipelines/templates/prepare-homebrew-cask.yml new file mode 100644 index 00000000000..4c783f6b671 --- /dev/null +++ b/eng/pipelines/templates/prepare-homebrew-cask.yml @@ -0,0 +1,213 @@ +parameters: + - name: version + type: string + default: '' + - name: channel + type: string + - name: artifactVersion + type: string + default: '' + - name: archiveRoot + type: string + default: '' + - name: validateUrls + type: boolean + default: true + - name: runInstallTest + type: boolean + default: true + +steps: + - bash: | + set -euo pipefail + version='${{ parameters.version }}' + channel='${{ parameters.channel }}' + artifactVersion='${{ parameters.artifactVersion }}' + + if [ -z "$version" ]; then + echo "##[error]Version parameter is required" + exit 1 + fi + + if [ -z "$channel" ]; then + echo "##[error]Channel parameter is required (stable or prerelease)" + exit 1 + fi + + if [ "$channel" != "stable" ] && [ "$channel" != "prerelease" ]; then + echo "##[error]Channel must be 'stable' or 'prerelease', got: $channel" + exit 1 + fi + + echo "Version: $version" + echo "Channel: $channel" + echo "Artifact version: ${artifactVersion:-$version}" + + echo "##vso[task.setvariable variable=CliVersion]$version" + echo "##vso[task.setvariable variable=CliChannel]$channel" + echo "##vso[task.setvariable variable=CliArtifactVersion]${artifactVersion:-$version}" + displayName: 🟣Set version ${{ parameters.version }} + + - bash: | + set -euo pipefail + version="$(CliVersion)" + channel="$(CliChannel)" + artifactVersion="$(CliArtifactVersion)" + outputDir="$(Build.StagingDirectory)/homebrew-cask" + mkdir -p "$outputDir" + + if [ "$channel" = "prerelease" ]; then + outputFile="$outputDir/aspire@prerelease.rb" + else + outputFile="$outputDir/aspire.rb" + fi + + echo "Generating Homebrew cask for Aspire version $version (channel: $channel)" + + args=( + --version "$version" + --artifact-version "$artifactVersion" + --channel "$channel" + --output "$outputFile" + ) + + if [ -n "${ARCHIVE_ROOT:-}" ]; then + args+=(--archive-root "$ARCHIVE_ROOT") + fi + + validateUrlsNormalized="$(printf '%s' "${VALIDATE_URLS:-false}" | tr '[:upper:]' '[:lower:]')" + if [ "$validateUrlsNormalized" = "true" ]; then + args+=(--validate-urls) + fi + + "$(Build.SourcesDirectory)/eng/homebrew/generate-cask.sh" "${args[@]}" + + echo "" + echo "Generated cask:" + cat "$outputFile" + displayName: 🟣Generate Homebrew cask + env: + ARCHIVE_ROOT: '${{ parameters.archiveRoot }}' + VALIDATE_URLS: '${{ parameters.validateUrls }}' + + - bash: | + set -euo pipefail + channel="$(CliChannel)" + + if [ "$channel" = "prerelease" ]; then + caskFile="aspire@prerelease.rb" + caskName="aspire@prerelease" + else + caskFile="aspire.rb" + caskName="aspire" + fi + + caskPath="$(Build.StagingDirectory)/homebrew-cask/$caskFile" + + echo "Validating Ruby syntax..." + ruby -c "$caskPath" + echo "Ruby syntax OK" + + echo "" + echo "Auditing cask via local tap..." + brew tap-new --no-git local/aspire + mkdir -p "$(brew --repository)/Library/Taps/local/homebrew-aspire/Casks" + cp "$caskPath" "$(brew --repository)/Library/Taps/local/homebrew-aspire/Casks/$caskFile" + + auditCode=0 + brew audit --cask "local/aspire/$caskName" || auditCode=$? + + brew untap local/aspire + + if [ $auditCode -ne 0 ]; then + echo "##[error]brew audit failed" + exit $auditCode + fi + echo "brew audit passed" + displayName: 🟣Validate cask syntax and audit + + - bash: | + set -euo pipefail + channel="$(CliChannel)" + + if [ "$channel" = "prerelease" ]; then + caskFile="aspire@prerelease.rb" + caskName="aspire@prerelease" + else + caskFile="aspire.rb" + caskName="aspire" + fi + + caskPath="$(Build.StagingDirectory)/homebrew-cask/$caskFile" + + echo "Testing cask install/uninstall: $caskPath" + echo "" + + # Verify aspire is NOT already installed + echo "Verifying aspire is not already installed..." + if command -v aspire &>/dev/null; then + echo "##[error]aspire command is already available before install — test environment is not clean" + exit 1 + fi + echo " Confirmed: aspire is not in PATH" + + # Set up a local tap so brew accepts the cask + echo "" + echo "Setting up local tap..." + brew tap-new --no-git local/aspire-test + tapCaskDir="$(brew --repository)/Library/Taps/local/homebrew-aspire-test/Casks" + mkdir -p "$tapCaskDir" + cp "$caskPath" "$tapCaskDir/$caskFile" + + # Install from local tap + echo "" + echo "Installing aspire from local tap..." + brew install --cask "local/aspire-test/$caskName" + echo "✅ Install succeeded" + + # Verify aspire is now available and runs + echo "" + echo "Verifying aspire CLI is in PATH..." + if ! command -v aspire &>/dev/null; then + echo "##[error]aspire command not found in PATH after install" + # Show brew info for diagnostics + brew info --cask "local/aspire-test/$caskName" || true + brew untap local/aspire-test + exit 1 + fi + + echo " Path: $(command -v aspire)" + aspireVersion="$(aspire --version 2>&1)" || true + echo " Version: $aspireVersion" + echo "✅ aspire CLI verified" + + # Uninstall + echo "" + echo "Uninstalling aspire..." + brew uninstall --cask "local/aspire-test/$caskName" + echo "✅ Uninstall succeeded" + + # Clean up tap + brew untap local/aspire-test + + # Verify aspire is removed + if command -v aspire &>/dev/null; then + echo "##[warning]aspire command still found in PATH after uninstall" + else + echo " Confirmed: aspire is no longer in PATH" + fi + displayName: 🟣Test cask install/uninstall + condition: and(succeeded(), eq('${{ parameters.runInstallTest }}', 'true')) + + - bash: | + set -euo pipefail + cp "$(Build.SourcesDirectory)/eng/homebrew/dogfood.sh" "$(Build.StagingDirectory)/homebrew-cask/dogfood.sh" + chmod +x "$(Build.StagingDirectory)/homebrew-cask/dogfood.sh" + displayName: 🟣Include dogfood script in artifact + + - task: 1ES.PublishBuildArtifacts@1 + displayName: 🟣Publish Homebrew cask + condition: always() + inputs: + PathtoPublish: '$(Build.StagingDirectory)/homebrew-cask' + ArtifactName: homebrew-cask-$(CliChannel) diff --git a/eng/pipelines/templates/prepare-winget-manifest.yml b/eng/pipelines/templates/prepare-winget-manifest.yml new file mode 100644 index 00000000000..386837a2749 --- /dev/null +++ b/eng/pipelines/templates/prepare-winget-manifest.yml @@ -0,0 +1,230 @@ +parameters: + - name: version + type: string + default: '' + - name: channel + type: string + - name: artifactVersion + type: string + default: '' + - name: archiveRoot + type: string + default: '' + - name: validateUrls + type: boolean + default: true + - name: runInstallTest + type: boolean + default: true + +steps: + - pwsh: | + $ErrorActionPreference = 'Stop' + $version = '${{ parameters.version }}' + $channel = '${{ parameters.channel }}' + $artifactVersion = '${{ parameters.artifactVersion }}' + + if ([string]::IsNullOrWhiteSpace($version)) { + Write-Error "Version parameter is required" + exit 1 + } + + if ([string]::IsNullOrWhiteSpace($channel)) { + Write-Error "Channel parameter is required (stable or prerelease)" + exit 1 + } + + if ($channel -eq 'stable') { + $templateDir = 'microsoft.aspire' + $artifactName = 'winget-manifests-stable' + } elseif ($channel -eq 'prerelease') { + $templateDir = 'microsoft.aspire.prerelease' + $artifactName = 'winget-manifests-prerelease' + } else { + Write-Error "Channel must be 'stable' or 'prerelease', got: $channel" + exit 1 + } + + Write-Host "Version: $version" + Write-Host "Channel: $channel" + Write-Host "Artifact version: $(if ([string]::IsNullOrWhiteSpace($artifactVersion)) { $version } else { $artifactVersion })" + Write-Host "Template dir: $templateDir" + Write-Host "Artifact name: $artifactName" + Write-Host "##vso[task.setvariable variable=CliVersion]$version" + Write-Host "##vso[task.setvariable variable=CliArtifactVersion]$(if ([string]::IsNullOrWhiteSpace($artifactVersion)) { $version } else { $artifactVersion })" + Write-Host "##vso[task.setvariable variable=WinGetTemplateDir]$templateDir" + Write-Host "##vso[task.setvariable variable=WinGetArtifactName]$artifactName" + displayName: 🟣Set version ${{ parameters.version }} + + - pwsh: | + Write-Host "Installing Microsoft.WinGet.Client from PSGallery..." + Install-PSResource -Name Microsoft.WinGet.Client -Repository PSGallery -TrustRepository + + Write-Host "Microsoft.WinGet.Client installed. Listing installed version:" + Get-Module -ListAvailable Microsoft.WinGet.Client | Select-Object Name, Version | Format-Table + + Write-Host "Installing WinGet..." + Repair-WinGetPackageManager -Latest -Force -AllUsers + + Write-Host "winget installed:" + winget --version + displayName: 🟣Install winget CLI + + - pwsh: | + $ErrorActionPreference = 'Stop' + $version = '$(CliVersion)' + $artifactVersion = '$(CliArtifactVersion)' + $outputPath = '$(Build.StagingDirectory)/winget-manifests' + $templateDir = '$(Build.SourcesDirectory)/eng/winget/$(WinGetTemplateDir)' + + Write-Host "Generating WinGet manifests for Aspire.Cli version $version" + Write-Host "Using template directory: $templateDir" + + $args = @{ + Version = $version + ArtifactVersion = $artifactVersion + TemplateDir = $templateDir + OutputPath = $outputPath + } + + if (-not [string]::IsNullOrWhiteSpace('${{ parameters.archiveRoot }}')) { + $args.ArchiveRoot = '${{ parameters.archiveRoot }}' + } + + if ('${{ parameters.validateUrls }}' -eq 'true') { + $args.ValidateUrls = $true + } + + & "$(Build.SourcesDirectory)/eng/winget/generate-manifests.ps1" @args + + 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 + + # Find the versioned manifest folder and expose as a pipeline variable + $versionedManifestPath = Get-ChildItem -Path $outputPath -Directory -Recurse | + Where-Object { $_.Name -eq $version } | + Select-Object -First 1 -ExpandProperty FullName + + if (-not $versionedManifestPath) { + $versionedManifestPath = $outputPath + } + + Write-Host "Versioned manifest path: $versionedManifestPath" + Write-Host "##vso[task.setvariable variable=VersionedManifestPath]$versionedManifestPath" + displayName: 🟣Generate WinGet manifests + + - pwsh: | + $ErrorActionPreference = 'Stop' + $versionedManifestPath = '$(VersionedManifestPath)' + + 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' + $versionedManifestPath = '$(VersionedManifestPath)' + + 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 + condition: and(succeeded(), eq('${{ parameters.runInstallTest }}', 'true')) + + - pwsh: | + Copy-Item "$(Build.SourcesDirectory)/eng/winget/dogfood.ps1" "$(Build.StagingDirectory)/winget-manifests/dogfood.ps1" + displayName: 🟣Include dogfood script in artifact + + - task: 1ES.PublishBuildArtifacts@1 + displayName: 🟣Publish WinGet manifests + condition: always() + inputs: + PathtoPublish: '$(Build.StagingDirectory)/winget-manifests' + ArtifactName: $(WinGetArtifactName) diff --git a/eng/pipelines/templates/publish-homebrew.yml b/eng/pipelines/templates/publish-homebrew.yml new file mode 100644 index 00000000000..cf87d031025 --- /dev/null +++ b/eng/pipelines/templates/publish-homebrew.yml @@ -0,0 +1,188 @@ +# Reusable template for submitting a Homebrew cask PR to Homebrew/homebrew-cask. +# Used by the release flow (release-publish-nuget.yml). +# Analogous to publish-winget.yml for WinGet manifests. +# +# NOTE: Cask syntax validation and brew audit are performed during build +# in prepare-homebrew-cask.yml. This template only handles submission. + +parameters: + - name: caskArtifactPath + type: string + displayName: 'Path to the directory containing the generated .rb cask file' + - name: version + type: string + default: '' + displayName: 'Package version (used in PR title). If empty, extracted from cask file.' + - name: channel + type: string + default: 'stable' + values: + - stable + - prerelease + - name: dryRun + type: boolean + default: false + displayName: 'Skip actual submit (for testing)' + +steps: + - powershell: | + $ErrorActionPreference = 'Stop' + $caskDir = '${{ parameters.caskArtifactPath }}' + $channel = '${{ parameters.channel }}' + + if ($channel -eq 'prerelease') { + $caskFile = 'aspire@prerelease.rb' + } else { + $caskFile = 'aspire.rb' + } + + $caskPath = Join-Path $caskDir $caskFile + + Write-Host "=== Homebrew Cask ===" + Write-Host "Channel: $channel" + Write-Host "Cask file: $caskPath" + Write-Host "=====================" + + if (!(Test-Path $caskPath)) { + Write-Host "##[error]Cask file not found: $caskPath" + Get-ChildItem -Path $caskDir -ErrorAction SilentlyContinue | Format-Table Name + exit 1 + } + + Write-Host "" + Write-Host "=== Cask Contents ===" + Get-Content $caskPath + Write-Host "=====================" + + # Extract version from cask file content + $version = '${{ parameters.version }}' + if ([string]::IsNullOrWhiteSpace($version)) { + $match = Select-String -Path $caskPath -Pattern '^\s*version\s+"([^"]+)"' | Select-Object -First 1 + if ($match) { + $version = $match.Matches[0].Groups[1].Value + Write-Host "Extracted version from cask: $version" + } else { + Write-Error "Could not extract version from cask file" + exit 1 + } + } else { + Write-Host "Using provided version: $version" + } + + Write-Host "##vso[task.setvariable variable=HomebrewCaskFile]$caskFile" + Write-Host "##vso[task.setvariable variable=HomebrewCaskPath]$caskPath" + Write-Host "##vso[task.setvariable variable=HomebrewVersion]$version" + displayName: '🟣Locate Homebrew cask file' + + - powershell: | + $ErrorActionPreference = 'Stop' + $caskPath = '$(HomebrewCaskPath)' + $version = '$(HomebrewVersion)' + + Write-Host "Validating download URLs from cask file..." + + # Extract url lines from the cask file + $urls = @() + Get-Content $caskPath | ForEach-Object { + if ($_ -match 'url\s+"([^"]+)"') { + $urls += $Matches[1] + } + } + + if ($urls.Count -eq 0) { + Write-Host "##[error]No URLs found in cask file: $caskPath" + exit 1 + } + + # Expand #{version} and #{arch} placeholders for both architectures + $failed = $false + foreach ($rawUrl in $urls) { + foreach ($arch in @('arm64', 'x64')) { + $expanded = $rawUrl -replace '#\{version\}', $version + $expanded = $expanded -replace '#\{arch\}', $arch + + try { + $response = Invoke-WebRequest -Uri $expanded -Method Head -MaximumRedirection 5 -TimeoutSec 15 -UseBasicParsing + Write-Host " OK ($($response.StatusCode)): $expanded" + } catch { + $code = 'ERR' + if ($_.Exception.Response) { $code = $_.Exception.Response.StatusCode.value__ } + Write-Host "##[error]FAILED ($code): $expanded — $($_.Exception.Message)" + $failed = $true + } + } + } + + if ($failed) { + Write-Error "One or more download URLs are not accessible. Aborting before PR submission." + exit 1 + } + Write-Host "All download URLs are accessible." + displayName: '🟣Validate download URLs from cask' + + - powershell: | + $ErrorActionPreference = 'Stop' + $version = '$(HomebrewVersion)' + $channel = '${{ parameters.channel }}' + $caskFile = '$(HomebrewCaskFile)' + $caskPath = '$(HomebrewCaskPath)' + $caskName = $caskFile -replace '\.rb$', '' + $token = $env:HOMEBREW_CASK_GITHUB_TOKEN + + if ([string]::IsNullOrWhiteSpace($token)) { + Write-Error "GitHub token for Homebrew cask PR is not set. Ensure the variable group is configured." + exit 1 + } + + Write-Host "Submitting cask PR to Homebrew/homebrew-cask" + + # Configure gh CLI + $token | gh auth login --with-token + + $botUser = gh api user --jq '.login' + Write-Host "Authenticated as: $botUser" + + # Fork and clone (idempotent — reuses existing fork) + $repoDir = "$(Build.StagingDirectory)/homebrew-cask-repo" + gh repo fork Homebrew/homebrew-cask --clone --default-branch-only -- $repoDir 2>$null + if ($LASTEXITCODE -ne 0) { + git clone "https://github.com/${botUser}/homebrew-cask.git" $repoDir + } + + Push-Location $repoDir + try { + git remote add upstream https://github.com/Homebrew/homebrew-cask.git 2>$null + git fetch upstream master + git checkout -B "aspire-${version}" upstream/master + + # Casks are organized by first letter: Casks/a/aspire.rb + $firstLetter = $caskName.Substring(0, 1) + $targetDir = "Casks/$firstLetter" + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + Copy-Item $caskPath "$targetDir/$caskFile" + + git config user.name "dotnet-bot" + git config user.email "dotnet-bot@microsoft.com" + git add "$targetDir/$caskFile" + git commit -m "$caskName $version" + git push --force origin "aspire-${version}" + + # Create PR (or update existing) + $existingPR = gh pr list --repo Homebrew/homebrew-cask --head "${botUser}:aspire-${version}" --json number --jq '.[0].number' 2>$null + if ($existingPR) { + Write-Host "PR #$existingPR already exists — updated branch" + } else { + gh pr create ` + --repo Homebrew/homebrew-cask ` + --title "$caskName $version" ` + --body "Update $caskName to version $version." ` + --head "${botUser}:aspire-${version}" + Write-Host "PR created successfully" + } + } finally { + Pop-Location + } + displayName: '🟣Submit PR to Homebrew/homebrew-cask' + condition: and(succeeded(), eq('${{ parameters.dryRun }}', 'false')) + env: + HOMEBREW_CASK_GITHUB_TOKEN: $(aspire-homebrew-bot-pat) diff --git a/eng/pipelines/templates/publish-winget.yml b/eng/pipelines/templates/publish-winget.yml new file mode 100644 index 00000000000..74dc0cf78b4 --- /dev/null +++ b/eng/pipelines/templates/publish-winget.yml @@ -0,0 +1,182 @@ +# Reusable template for submitting WinGet manifests via wingetcreate. +# Used by both the prerelease flow (azure-pipelines.yml) and the release flow (release-publish-nuget.yml). + +parameters: + - name: manifestArtifactPath + type: string + displayName: 'Path to the directory containing WinGet manifest .yaml files' + - name: version + type: string + default: '' + displayName: 'Package version (used in PR title). If empty, extracted from manifest directory structure.' + - name: packageIdentifier + type: string + displayName: 'WinGet package identifier (e.g. Microsoft.Aspire or Microsoft.Aspire.Prerelease)' + - name: dryRun + type: boolean + default: false + displayName: 'Skip actual submit (for testing)' + +steps: + - powershell: | + $ErrorActionPreference = 'Stop' + $manifestRoot = '${{ parameters.manifestArtifactPath }}' + + Write-Host "=== WinGet Manifests ===" + Get-ChildItem -Path $manifestRoot -Recurse | Format-Table FullName + Write-Host "========================" + + # Find the versioned manifest directory (deepest directory containing .yaml files) + $manifestDir = Get-ChildItem -Path $manifestRoot -Filter "*.yaml" -Recurse | + Select-Object -First 1 -ExpandProperty DirectoryName + + if (-not $manifestDir) { + Write-Error "No .yaml manifest files found in: $manifestRoot" + exit 1 + } + + Write-Host "Manifest directory: $manifestDir" + Write-Host "##vso[task.setvariable variable=WinGetManifestDir]$manifestDir" + + # Extract version from manifest directory name (e.g., .../Microsoft.Aspire/13.2.0/) + $version = '${{ parameters.version }}' + if ([string]::IsNullOrWhiteSpace($version)) { + $version = Split-Path $manifestDir -Leaf + Write-Host "Extracted version from directory: $version" + } else { + Write-Host "Using provided version: $version" + } + Write-Host "##vso[task.setvariable variable=WinGetVersion]$version" + + Write-Host "" + Write-Host "=== Manifest files ===" + Get-ChildItem -Path $manifestDir | ForEach-Object { + $sizeKB = [math]::Round($_.Length / 1KB, 2) + Write-Host " $($_.Name) ($sizeKB KB)" + } + Write-Host "======================" + displayName: '🟣Locate WinGet manifest directory' + + - powershell: | + $ErrorActionPreference = 'Stop' + $manifestDir = '$(WinGetManifestDir)' + + Write-Host "Validating installer URLs from WinGet manifest..." + + # Find the installer yaml file + $installerYaml = Get-ChildItem -Path $manifestDir -Filter "*.installer.yaml" | Select-Object -First 1 + if (-not $installerYaml) { + Write-Error "No *.installer.yaml found in $manifestDir" + exit 1 + } + + Write-Host "Parsing URLs from: $($installerYaml.Name)" + + # Extract InstallerUrl values + $urls = Select-String -Path $installerYaml.FullName -Pattern 'InstallerUrl:\s*(.+)' | + ForEach-Object { $_.Matches[0].Groups[1].Value.Trim() } + + if (-not $urls -or $urls.Count -eq 0) { + Write-Error "No InstallerUrl entries found in $($installerYaml.Name)" + exit 1 + } + + $failed = $false + foreach ($url in $urls) { + try { + $response = Invoke-WebRequest -Uri $url -Method Head -MaximumRedirection 5 -TimeoutSec 15 -UseBasicParsing + $code = $response.StatusCode + Write-Host " OK ($code): $url" + } + catch { + $code = 'ERR' + if ($_.Exception.Response) { $code = $_.Exception.Response.StatusCode.value__ } + Write-Host "##[error]FAILED ($code): $url — $($_.Exception.Message)" + $failed = $true + } + } + + if ($failed) { + Write-Error "One or more installer URLs are not accessible. Aborting before PR submission." + exit 1 + } + Write-Host "All installer URLs are accessible." + displayName: '🟣Validate installer URLs from manifest' + + - powershell: | + $ErrorActionPreference = 'Stop' + $manifestDir = '$(WinGetManifestDir)' + + Write-Host "Validating ReleaseNotesUrl from locale manifest..." + + $localeYaml = Get-ChildItem -Path $manifestDir -Filter "*.locale.*.yaml" | Select-Object -First 1 + if (-not $localeYaml) { + Write-Error "No *.locale.*.yaml found in $manifestDir" + exit 1 + } + + $match = Select-String -Path $localeYaml.FullName -Pattern 'ReleaseNotesUrl:\s*(.+)' | Select-Object -First 1 + if (-not $match) { + Write-Error "No ReleaseNotesUrl found in $($localeYaml.Name)" + exit 1 + } + + $releaseNotesUrl = $match.Matches[0].Groups[1].Value.Trim() + Write-Host " URL: $releaseNotesUrl" + + try { + $response = Invoke-WebRequest -Uri $releaseNotesUrl -Method Head -MaximumRedirection 5 -TimeoutSec 15 -UseBasicParsing + Write-Host " OK ($($response.StatusCode))" + } + catch { + $code = 'ERR' + if ($_.Exception.Response) { $code = $_.Exception.Response.StatusCode.value__ } + Write-Error "ReleaseNotesUrl is not accessible ($code): $releaseNotesUrl — $($_.Exception.Message)" + exit 1 + } + + Write-Host "ReleaseNotesUrl is valid." + displayName: '🟣Validate ReleaseNotesUrl' + + - powershell: | + $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' + + - powershell: | + $ErrorActionPreference = 'Stop' + $manifestDir = '$(WinGetManifestDir)' + $version = '$(WinGetVersion)' + $packageId = '${{ parameters.packageIdentifier }}' + $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 manifests for $packageId version $version" + Write-Host "Manifest directory: $manifestDir" + Write-Host "Manifest files:" + Get-ChildItem -Path $manifestDir -Filter "*.yaml" | ForEach-Object { Write-Host " - $($_.Name)" } + + $output = & "$(Build.StagingDirectory)/wingetcreate.exe" submit $manifestDir ` + --token $token ` + --prtitle "Update $packageId to version $version" 2>&1 + + $exitCode = $LASTEXITCODE + $outputText = ($output -join "`n").Replace($token, '***') + Write-Host $outputText + + if ($exitCode -ne 0) { + Write-Error "wingetcreate submit failed with exit code $exitCode" + exit $exitCode + } + + Write-Host "Successfully submitted WinGet manifest for $packageId $version" + displayName: '🟣Submit to WinGet' + condition: and(succeeded(), eq('${{ parameters.dryRun }}', 'false')) + env: + WINGET_CREATE_GITHUB_TOKEN: $(aspire-winget-bot-pat) diff --git a/eng/restore-toolset.sh b/eng/restore-toolset.sh deleted file mode 100644 index 8a7bb526c06..00000000000 --- a/eng/restore-toolset.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env bash - -# Install MAUI workload if -restoreMaui was passed -# Only on macOS (MAUI doesn't support Linux, Windows uses .cmd) - -if [[ "$restore_maui" == true ]]; then - # Check if we're on macOS - if [[ "$(uname -s)" == "Darwin" ]]; then - echo "" - echo "Installing MAUI workload..." - - dotnet_sh="$repo_root/dotnet.sh" - - if "$dotnet_sh" workload install maui; then - echo "MAUI workload installed successfully." - echo "" - else - echo "" - echo "WARNING: Failed to install MAUI workload. You may need to run this command manually:" - echo " $dotnet_sh workload install maui" - echo "" - echo "The MAUI playground may not work without the MAUI workload installed." - echo "" - fi - else - echo "Skipping MAUI workload installation on Linux (not supported)." - fi - - if ! command -v python3 >/dev/null 2>&1; then - echo "python3 is required to generate AspireWithMaui.slnx" - exit 1 - fi - - # Generate AspireWithMaui.slnx from the base Aspire.slnx - echo "" - echo "Generating AspireWithMaui.slnx..." - - source_slnx="$repo_root/Aspire.slnx" - output_path="$repo_root/playground/AspireWithMaui" - output_slnx="$output_path/AspireWithMaui.slnx" - - if [ ! -f "$source_slnx" ]; then - echo "WARNING: Source solution file not found: $source_slnx" - else - mkdir -p "$output_path" - - python3 <<'PY' "$source_slnx" "$output_slnx" -import codecs -import os -import re -import sys - -source = os.path.abspath(sys.argv[1]) -target = os.path.abspath(sys.argv[2]) -source_dir = os.path.dirname(source) -target_dir = os.path.dirname(target) - -with open(source, 'rb') as handle: - text = handle.read().decode('utf-8-sig') - -maui_folder_marker = 'playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj' -if maui_folder_marker not in text: - folder_block = ( - '\r\n \r\n' - ' \r\n' - ' \r\n' - ' \r\n' - ' \r\n' - ' \r\n' - ' \r\n' - ) - text = text.replace('\r\n', f'{folder_block}', 1) - -tests_folder_pattern = re.compile(r'(\r?\n)(.*?)( )', re.DOTALL) -match = tests_folder_pattern.search(text) -desired_line = ' \r\n' -desired_path = 'tests/Aspire.Hosting.Maui.Tests/Aspire.Hosting.Maui.Tests.csproj' -if match: - body = match.group(2) - if desired_path not in body: - lines = body.splitlines(keepends=True) - - def extract_path(line: str) -> str: - hit = re.search(r'Path="([^"]+)"', line) - return hit.group(1) if hit else '' - - inserted = False - for index, line in enumerate(lines): - existing_path = extract_path(line) - if existing_path and desired_path.lower() < existing_path.lower(): - lines.insert(index, desired_line) - inserted = True - break - if not inserted: - lines.append(desired_line) - - new_body = ''.join(lines) - text = text[:match.start(2)] + new_body + text[match.end(2):] - -def resolve_relative(value: str) -> str: - normalized = value.replace('\\', '/').replace('/', os.sep) - if os.path.isabs(normalized): - absolute = os.path.normpath(normalized) - else: - absolute = os.path.normpath(os.path.join(source_dir, normalized)) - relative = os.path.relpath(absolute, target_dir) - return relative.replace(os.sep, '/') - -def substitute(match: re.Match) -> str: - original = match.group(1) - return f'Path="{resolve_relative(original)}"' - -text = re.sub(r'Path="([^"]+)"', substitute, text) - -with open(target, 'wb') as handle: - handle.write(codecs.BOM_UTF8) - handle.write(text.encode('utf-8')) -PY - - echo "Generated AspireWithMaui.slnx at: $output_slnx" - fi -fi diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index 4ac9cf20097..20e12869b9b 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -8,6 +8,9 @@ param( [Parameter(HelpMessage = "Version of the Aspire CLI to download")] [string]$Version = "", + [Parameter(HelpMessage = "ci.dot.net folder version when it differs from the CLI archive filename version")] + [string]$ArtifactVersion = "", + [Parameter(HelpMessage = "Quality to download")] [ValidateSet("", "release", "staging", "dev")] [string]$Quality = "", @@ -133,11 +136,13 @@ DESCRIPTION: The default quality is '$($Script:Config.DefaultQuality)'. Pass a specific version to get CLI for that version. + Use -ArtifactVersion when the ci.dot.net folder version differs from -Version. PARAMETERS: -InstallPath Directory to install the CLI (default: %USERPROFILE%\.aspire\bin on Windows, `$HOME/.aspire/bin on Unix) -Quality Quality to download (default: $($Script:Config.DefaultQuality)) -Version Version of the Aspire CLI to download (default: unset) + -ArtifactVersion ci.dot.net folder version when it differs from -Version (default: -Version) -OS Operating system (default: auto-detect) -Architecture Architecture (default: auto-detect) -InstallExtension Install VS Code extension along with the CLI @@ -722,13 +727,16 @@ function Get-AspireExtension { [Parameter()] [string]$Version, + [Parameter()] + [string]$ArtifactVersion, + [Parameter()] [string]$Quality ) Write-Message "Downloading Aspire VS Code extension" -Level Info - $extensionUrl = Get-AspireExtensionUrl -Version $Version -Quality $Quality + $extensionUrl = Get-AspireExtensionUrl -Version $Version -ArtifactVersion $ArtifactVersion -Quality $Quality $extensionArchive = Join-Path $TempDir $Script:ExtensionArtifactName try { @@ -822,6 +830,9 @@ function Get-AspireExtensionUrl { [Parameter()] [string]$Version, + [Parameter()] + [string]$ArtifactVersion = $Version, + [Parameter()] [string]$Quality ) @@ -844,7 +855,7 @@ function Get-AspireExtensionUrl { else { # Version-based URL $baseUrl = $Script:Config.BaseUrls["versioned"] - return "$baseUrl/$Version/aspire-vscode-$Version.$extension" + return "$baseUrl/$ArtifactVersion/aspire-vscode-$Version.$extension" } } @@ -859,6 +870,9 @@ function Get-AspireCliUrl { [Parameter()] [string]$Quality, + [Parameter()] + [string]$ArtifactVersion = $Version, + [Parameter(Mandatory = $true)] [string]$RuntimeIdentifier, @@ -894,9 +908,9 @@ function Get-AspireCliUrl { $checksumFilename = "$archiveFilename.sha512" return [PSCustomObject]@{ - ArchiveUrl = "$($Script:Config.BaseUrls["versioned"])/$Version/$archiveFilename" + ArchiveUrl = "$($Script:Config.BaseUrls["versioned"])/$ArtifactVersion/$archiveFilename" ArchiveFilename = $archiveFilename - ChecksumUrl = "$($Script:Config.BaseUrls["versioned-checksums"])/$Version/$checksumFilename" + ChecksumUrl = "$($Script:Config.BaseUrls["versioned-checksums"])/$ArtifactVersion/$checksumFilename" ChecksumFilename = $checksumFilename } } @@ -910,6 +924,7 @@ function Install-AspireCli { [Parameter(Mandatory = $true)] [string]$InstallPath, [string]$Version, + [string]$ArtifactVersion, [string]$Quality, [string]$OS, [string]$Architecture @@ -953,7 +968,7 @@ function Install-AspireCli { # Construct the runtime identifier and URLs $runtimeIdentifier = "$targetOS-$targetArch" $extension = if ($targetOS -eq "win") { "zip" } else { "tar.gz" } - $urls = Get-AspireCliUrl -Version $Version -Quality $Quality -RuntimeIdentifier $runtimeIdentifier -Extension $extension + $urls = Get-AspireCliUrl -Version $Version -ArtifactVersion $ArtifactVersion -Quality $Quality -RuntimeIdentifier $runtimeIdentifier -Extension $extension $archivePath = Join-Path $tempDir $urls.ArchiveFilename $checksumPath = Join-Path $tempDir $urls.ChecksumFilename @@ -989,7 +1004,7 @@ function Install-AspireCli { if (Test-VSCodeCLIDependency -UseInsiders:$UseInsiders) { try { - $extensionArchive = Get-AspireExtension -TempDir $tempDir -Version $Version -Quality $Quality + $extensionArchive = Get-AspireExtension -TempDir $tempDir -Version $Version -ArtifactVersion $ArtifactVersion -Quality $Quality Install-AspireExtension -ExtensionArchive $extensionArchive -UseInsiders:$UseInsiders } catch { @@ -1037,11 +1052,19 @@ function Start-AspireCliInstallation { throw "Cannot specify both -Version and -Quality. Use -Version for a specific version or -Quality for a quality level." } + if (-not [string]::IsNullOrWhiteSpace($ArtifactVersion) -and [string]::IsNullOrWhiteSpace($Version)) { + throw "Cannot specify -ArtifactVersion without -Version." + } + # Set default quality if not specified and no version is provided if ([string]::IsNullOrWhiteSpace($Version) -and [string]::IsNullOrWhiteSpace($Quality)) { $Quality = $Script:Config.DefaultQuality } + if ([string]::IsNullOrWhiteSpace($ArtifactVersion)) { + $ArtifactVersion = $Version + } + # Additional parameter validation if (-not [string]::IsNullOrWhiteSpace($OS) -and $OS -notin $Script:Config.SupportedOperatingSystems) { throw "Unsupported OS '$OS'. Supported values are: $($Script:Config.SupportedOperatingSystems -join ', ')" @@ -1073,7 +1096,7 @@ function Start-AspireCliInstallation { } # Download and install the Aspire CLI - $targetOS = Install-AspireCli -InstallPath $resolvedInstallPath -Version $Version -Quality $Quality -OS $OS -Architecture $Architecture + $targetOS = Install-AspireCli -InstallPath $resolvedInstallPath -Version $Version -ArtifactVersion $ArtifactVersion -Quality $Quality -OS $OS -Architecture $Architecture # Update PATH environment variables unless -SkipPath is specified if ($SkipPath) { diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index 69b19cacd9d..4a4ad38b73e 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -18,6 +18,7 @@ readonly RESET='\033[0m' # Variables (defaults set after parsing arguments) INSTALL_PATH="" VERSION="" +ARTIFACT_VERSION="" QUALITY="" OS="" ARCH="" @@ -47,6 +48,7 @@ DESCRIPTION: The default quality is `${DEFAULT_QUALITY}`. Pass a specific version to get CLI for that version. + Use --artifact-version when the ci.dot.net folder version differs from the archive filename version. USAGE: ./get-aspire-cli.sh [OPTIONS] @@ -54,6 +56,7 @@ USAGE: -i, --install-path PATH Directory to install the CLI (default: $HOME/.aspire/bin) -q, --quality QUALITY Quality to download (default: ${DEFAULT_QUALITY}). Supported values: dev, staging, release --version VERSION Version of the Aspire CLI to download (default: unset) + --artifact-version VERSION ci.dot.net folder version when it differs from --version (default: --version) --os OS Operating system (default: auto-detect) --arch ARCH Architecture (default: auto-detect) --install-extension Install VS Code extension along with the CLI @@ -105,6 +108,15 @@ parse_args() { VERSION="$2" shift 2 ;; + --artifact-version) + if [[ $# -lt 2 || -z "$2" ]]; then + say_error "Option '$1' requires a non-empty value" + say_info "Use --help for usage information." + exit 1 + fi + ARTIFACT_VERSION="$2" + shift 2 + ;; -q|--quality) if [[ $# -lt 2 || -z "$2" ]]; then say_error "Option '$1' requires a non-empty value" @@ -663,6 +675,7 @@ test_vscode_cli() { construct_aspire_extension_url() { local version="$1" local quality="$2" + local artifact_version="${3:-$1}" local base_url local extension="vsix.zip" @@ -688,7 +701,7 @@ construct_aspire_extension_url() { else # When version is set, use ci.dot.net URL base_url="https://ci.dot.net/public/aspire/" - printf "${base_url}${version}/aspire-vscode-${version}.${extension}" + printf "${base_url}${artifact_version}/aspire-vscode-${version}.${extension}" fi } @@ -699,6 +712,7 @@ construct_aspire_cli_url() { local rid="$3" local extension="$4" local checksum="${5:-false}" + local artifact_version="${6:-$1}" local base_url local filename @@ -736,7 +750,7 @@ construct_aspire_cli_url() { base_url="https://ci.dot.net/public/aspire/" fi - printf "${base_url}/${version}/aspire-cli-${rid}-${version}.${extension}" + printf "${base_url}/${artifact_version}/aspire-cli-${rid}-${version}.${extension}" fi } @@ -745,11 +759,12 @@ download_aspire_extension() { local temp_dir="$1" local version="$2" local quality="$3" + local artifact_version="${4:-$2}" local url extension_archive say_info "Downloading Aspire VS Code extension" - if ! url=$(construct_aspire_extension_url "$version" "$quality"); then + if ! url=$(construct_aspire_extension_url "$version" "$quality" "$artifact_version"); then return 1 fi @@ -866,10 +881,10 @@ download_and_install_archive() { fi # Construct the URLs using the new function - if ! url=$(construct_aspire_cli_url "$VERSION" "$QUALITY" "$runtimeIdentifier" "$extension"); then + if ! url=$(construct_aspire_cli_url "$VERSION" "$QUALITY" "$runtimeIdentifier" "$extension" "false" "$ARTIFACT_VERSION"); then return 1 fi - if ! checksum_url=$(construct_aspire_cli_url "$VERSION" "$QUALITY" "$runtimeIdentifier" "$extension" "true"); then + if ! checksum_url=$(construct_aspire_cli_url "$VERSION" "$QUALITY" "$runtimeIdentifier" "$extension" "true" "$ARTIFACT_VERSION"); then return 1 fi @@ -914,7 +929,7 @@ download_and_install_archive() { say_info "Installing VS Code extension" if test_vscode_cli; then - if extension_archive=$(download_aspire_extension "$temp_dir" "$VERSION" "$QUALITY"); then + if extension_archive=$(download_aspire_extension "$temp_dir" "$VERSION" "$QUALITY" "$ARTIFACT_VERSION"); then if ! install_aspire_extension "$extension_archive"; then say_warn "Failed to install VS Code extension" say_warn "The CLI was installed successfully, but the extension installation failed" @@ -945,6 +960,16 @@ if [[ -n "$VERSION" && -n "$QUALITY" ]]; then exit 1 fi +if [[ -n "$ARTIFACT_VERSION" && -z "$VERSION" ]]; then + say_error "Cannot specify --artifact-version without --version." + say_info "Use --help for usage information." + exit 1 +fi + +if [[ -z "$ARTIFACT_VERSION" ]]; then + ARTIFACT_VERSION="$VERSION" +fi + # Initialize default values after parsing arguments if [[ -z "$QUALITY" ]]; then # Default quality if not provided diff --git a/eng/winget/README.md b/eng/winget/README.md new file mode 100644 index 00000000000..d42547f9af3 --- /dev/null +++ b/eng/winget/README.md @@ -0,0 +1,56 @@ +# WinGet Distribution for Aspire CLI + +## Overview + +Aspire CLI is distributed via [WinGet](https://learn.microsoft.com/windows/package-manager/) for Windows (x64, arm64). Manifest PRs are submitted to the upstream [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs) repository using [wingetcreate](https://github.com/microsoft/winget-create). + +### Install commands + +```powershell +winget install Microsoft.Aspire # stable +# winget install Microsoft.Aspire.Prerelease # preview (not yet supported) +``` + +## Contents + +| Directory / File | Description | +|--------------------------------|----------------------------------------------------------------------------------| +| `microsoft.aspire/` | Manifest templates for stable releases | +| `microsoft.aspire.prerelease/` | Manifest templates for prerelease builds | +| `generate-manifests.ps1` | Downloads installers, computes SHA256 hashes, generates manifests from templates | + +Each manifest set contains three YAML files following the [WinGet manifest schema v1.10](https://learn.microsoft.com/windows/package-manager/package/manifest): + +| File | Purpose | +|-------------------------------------|-------------------------------------------------| +| `Aspire.yaml.template` | Version manifest | +| `Aspire.installer.yaml.template` | Installer manifest (URLs, SHA256, architecture) | +| `Aspire.locale.en-US.yaml.template` | Locale manifest (description, tags, license) | + +### Pipeline templates + +| File | Description | +|--------------------------------------------------------|-------------------------------------------------| +| `eng/pipelines/templates/prepare-winget-manifest.yml` | Generates, validates, and tests the manifests | +| `eng/pipelines/templates/publish-winget.yml` | Submits the manifests via `wingetcreate submit` | + +## Supported Platforms + +Windows only (x64, arm64). Installers are zip archives containing a portable `aspire.exe`. + +## Artifact URLs + +```text +https://ci.dot.net/public/aspire/{ARTIFACT_VERSION}/aspire-cli-win-{arch}-{VERSION}.zip +``` + +Where arch is `x64` or `arm64`. + +## CI Pipeline + +| Pipeline | Prepares | Publishes | +|---------------------------------------|------------------------------------------------|-----------------------| +| `azure-pipelines.yml` (prepare stage) | Stable manifests (artifacts only) | — | +| `release-publish-nuget.yml` (release) | — | Stable manifests only | + +Publishing submits a PR to `microsoft/winget-pkgs` using `wingetcreate submit`. diff --git a/eng/winget/dogfood.ps1 b/eng/winget/dogfood.ps1 new file mode 100644 index 00000000000..4b482f22c66 --- /dev/null +++ b/eng/winget/dogfood.ps1 @@ -0,0 +1,157 @@ +<# +.SYNOPSIS + Installs the Aspire CLI from local WinGet manifest files for dogfooding. + +.DESCRIPTION + This script installs (or uninstalls) the Aspire CLI using local WinGet manifest files, + allowing you to test builds before they are published to microsoft/winget-pkgs. + +.PARAMETER ManifestPath + Path to the directory containing the WinGet manifest YAML files. + Defaults to auto-detecting the manifest directory relative to this script. + +.PARAMETER Uninstall + Uninstall a previously dogfooded Aspire CLI. + +.EXAMPLE + .\dogfood.ps1 + # Auto-detects manifests in the script directory and installs + +.EXAMPLE + .\dogfood.ps1 -ManifestPath .\manifests\m\Microsoft\Aspire\9.2.0 + # Install from a specific manifest directory + +.EXAMPLE + .\dogfood.ps1 -Uninstall + # Uninstall the dogfooded Aspire CLI +#> + +[CmdletBinding()] +param( + [Parameter(Position = 0)] + [string]$ManifestPath, + + [switch]$Uninstall +) + +$ErrorActionPreference = 'Stop' + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +if ($Uninstall) { + Write-Host "Uninstalling dogfooded Aspire CLI..." + Write-Host "" + + # Try to find the package via winget + $packages = @("Microsoft.Aspire", "Microsoft.Aspire.Prerelease") + foreach ($pkg in $packages) { + Write-Host "Checking for $pkg..." + $result = winget list --id $pkg --accept-source-agreements 2>&1 + if ($LASTEXITCODE -eq 0 -and $result -match $pkg) { + Write-Host " Found $pkg, uninstalling..." + winget uninstall --id $pkg --accept-source-agreements + if ($LASTEXITCODE -eq 0) { + Write-Host " Uninstalled $pkg." + } else { + Write-Warning " Failed to uninstall $pkg (exit code: $LASTEXITCODE)" + } + } + } + + Write-Host "" + Write-Host "Done." + exit 0 +} + +# Auto-detect manifest path if not specified +if (-not $ManifestPath) { + # Look for versioned manifest directories under the script directory + # Convention: manifests/m/Microsoft/Aspire/{Version}/ or manifests/m/Microsoft/Aspire/Prerelease/{Version}/ + $candidates = Get-ChildItem -Path $ScriptDir -Directory -Recurse -Depth 6 | + Where-Object { + Test-Path (Join-Path $_.FullName "*.installer.yaml") + } | + Select-Object -First 1 + + if ($candidates) { + $ManifestPath = $candidates.FullName + } else { + Write-Error "No manifest directory found under $ScriptDir. Specify -ManifestPath explicitly." + exit 1 + } +} + +if (-not (Test-Path $ManifestPath)) { + Write-Error "Manifest path not found: $ManifestPath" + exit 1 +} + +# Verify it contains manifest files +$manifestFiles = Get-ChildItem -Path $ManifestPath -Filter "*.yaml" +if ($manifestFiles.Count -eq 0) { + Write-Error "No .yaml manifest files found in: $ManifestPath" + exit 1 +} + +Write-Host "Aspire CLI WinGet Dogfood Installer" +Write-Host "=====================================" +Write-Host " Manifest path: $ManifestPath" +Write-Host " Manifest files:" +foreach ($f in $manifestFiles) { + Write-Host " - $($f.Name)" +} +Write-Host "" + +# Enable local manifest files +Write-Host "Enabling local manifest files in winget settings..." +winget settings --enable LocalManifestFiles +if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to enable local manifests. You may need to run this as Administrator." +} + +# Validate +Write-Host "" +Write-Host "Validating manifests..." +winget validate --manifest $ManifestPath +if ($LASTEXITCODE -ne 0) { + Write-Error "Manifest validation failed. Fix the manifests and try again." + exit $LASTEXITCODE +} +Write-Host "Validation passed." + +# Install +Write-Host "" +Write-Host "Installing Aspire CLI from local manifest..." +winget install --manifest $ManifestPath --accept-package-agreements --accept-source-agreements +if ($LASTEXITCODE -ne 0) { + Write-Error "Installation failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE +} + +# Refresh PATH +$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + +# Verify in a new process to pick up PATH changes +Write-Host "" +Write-Host "Verifying installation..." +$verifyResult = 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" +' 2>&1 + +if ($LASTEXITCODE -eq 0) { + Write-Host $verifyResult + Write-Host "" + Write-Host "Installed successfully!" +} else { + Write-Host $verifyResult + Write-Host "" + Write-Warning "aspire command not found in PATH. You may need to restart your shell." +} + +Write-Host "" +Write-Host "To uninstall: .\dogfood.ps1 -Uninstall" diff --git a/eng/winget/generate-manifests.ps1 b/eng/winget/generate-manifests.ps1 new file mode 100644 index 00000000000..4bfbe85ab26 --- /dev/null +++ b/eng/winget/generate-manifests.ps1 @@ -0,0 +1,354 @@ +<# +.SYNOPSIS + Generates WinGet manifest files for Aspire CLI from templates. + +.DESCRIPTION + This script generates the required WinGet manifest files (version, locale, and installer) + from templates by substituting version numbers, URLs, and computing SHA256 hashes. + Installer URLs are derived from the installer version, artifact version, and RIDs using the ci.dot.net URL pattern. + +.PARAMETER Version + The package version and installer filename version (e.g., "13.2.0"). + +.PARAMETER ArtifactVersion + The version segment used in the ci.dot.net artifact path. Defaults to Version. + +.PARAMETER TemplateDir + The directory containing the manifest templates to use. + Use "microsoft.aspire" for release builds or "microsoft.aspire.prerelease" for prerelease builds. + +.PARAMETER Rids + Comma-separated list of Runtime Identifiers for the installer architectures. + Defaults to "win-x64,win-arm64". + +.PARAMETER OutputPath + The directory where the manifest files will be written. + Defaults to a path derived from the PackageIdentifier in the templates, + e.g., "./manifests/m/Microsoft/Aspire/{Version}" for Microsoft.Aspire + or "./manifests/m/Microsoft/Aspire/Prerelease/{Version}" for Microsoft.Aspire.Prerelease. + +.PARAMETER ArchiveRoot + Root directory containing locally built CLI archives. When specified, SHA256 hashes + are computed from matching local files instead of downloading from installer URLs. + +.PARAMETER ReleaseNotesUrl + URL to the release notes page. If not specified, derived from the version + (e.g., "13.2.0" -> "https://aspire.dev/whats-new/aspire-13-2/"). + +.PARAMETER ValidateUrls + When specified, verifies that all installer URLs are accessible (HTTP HEAD request) + before downloading them to compute SHA256 hashes. + +.EXAMPLE + ./generate-manifests.ps1 -Version "13.3.0-preview.1.26111.5" ` + -TemplateDir "./eng/winget/microsoft.aspire.prerelease" + +.EXAMPLE + ./generate-manifests.ps1 -Version "13.2.0" ` + -ArtifactVersion "13.2.0-preview.1.26111.5" ` + -TemplateDir "./eng/winget/microsoft.aspire" ` + -Rids "win-x64,win-arm64" -ValidateUrls +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$Version, + + [Parameter(Mandatory = $false)] + [string]$ArtifactVersion, + + [Parameter(Mandatory = $true)] + [string]$TemplateDir, + + [Parameter(Mandatory = $false)] + [string]$Rids = "win-x64,win-arm64", + + [Parameter(Mandatory = $false)] + [string]$OutputPath, + + [Parameter(Mandatory = $false)] + [string]$ArchiveRoot, + + [Parameter(Mandatory = $false)] + [string]$ReleaseNotesUrl, + + [Parameter(Mandatory = $false)] + [switch]$ValidateUrls +) + +$ErrorActionPreference = 'Stop' + +if ([string]::IsNullOrWhiteSpace($ArtifactVersion)) { + $ArtifactVersion = $Version +} + +# Determine script paths +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +# Validate template directory +if (-not (Test-Path $TemplateDir)) { + Write-Error "Template directory not found: $TemplateDir" + exit 1 +} + +if ($ArchiveRoot -and -not (Test-Path $ArchiveRoot)) { + Write-Error "Archive root directory not found: $ArchiveRoot" + exit 1 +} + +# Extract PackageIdentifier from the version template +$versionTemplatePath = Join-Path $TemplateDir "Aspire.yaml.template" +if (-not (Test-Path $versionTemplatePath)) { + Write-Error "Version template not found: $versionTemplatePath" + exit 1 +} + +$PackageIdentifier = $null +foreach ($line in Get-Content -Path $versionTemplatePath) { + if ($line -match '^\s*PackageIdentifier:\s*(.+)\s*$') { + $PackageIdentifier = $Matches[1].Trim() + break + } +} + +if (-not $PackageIdentifier) { + Write-Error "Could not extract PackageIdentifier from $versionTemplatePath" + exit 1 +} + +Write-Host "Package identifier: $PackageIdentifier" + +# Derive the output directory from the PackageIdentifier +# Convention: manifests/{first-letter-lowercase}/{Segment1}/{Segment2}/.../{Version} +# e.g. Microsoft.Aspire -> manifests/m/Microsoft/Aspire/{Version} +# e.g. Microsoft.Aspire.Prerelease -> manifests/m/Microsoft/Aspire/Prerelease/{Version} +if (-not $OutputPath) { + $idSegments = $PackageIdentifier.Split('.') + $firstLetter = $idSegments[0].Substring(0, 1).ToLowerInvariant() + $pathSegments = @("manifests", $firstLetter) + $idSegments + @($Version) + $OutputPath = Join-Path $ScriptDir ($pathSegments -join [System.IO.Path]::DirectorySeparatorChar) +} + +Write-Host "Generating WinGet manifests for Aspire version $Version" +Write-Host "Output directory: $OutputPath" + +# Create output directory +if (-not (Test-Path $OutputPath)) { + New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null +} + +# Parse RIDs +$ridList = $Rids.Split(',') | ForEach-Object { $_.Trim() } +Write-Host "RIDs: $($ridList -join ', ')" + +# Map RID to winget architecture name +function Get-ArchitectureFromRid { + param([string]$Rid) + + # Extract the architecture portion after the OS prefix (e.g., "win-x64" -> "x64") + if ($Rid -match '^win-(.+)$') { + return $Matches[1] + } + + Write-Error "Unsupported RID format: $Rid (expected 'win-')" + exit 1 +} + +# Build installer URL from version and RID +# Pattern: https://ci.dot.net/public/aspire/{artifactVersion}/aspire-cli-{rid}-{version}.zip +function Get-InstallerUrl { + param( + [string]$Version, + [string]$ArtifactVersion, + [string]$Rid + ) + + return "https://ci.dot.net/public/aspire/$ArtifactVersion/aspire-cli-$Rid-$Version.zip" +} + +# Function to compute SHA256 hash of a file downloaded from URL +function Get-RemoteFileSha256 { + param( + [string]$Url, + [string]$Description + ) + + Write-Host "Downloading $Description to compute SHA256..." + Write-Host " URL: $Url" + + $tempFile = [System.IO.Path]::GetTempFileName() + try { + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest -Uri $Url -OutFile $tempFile -UseBasicParsing -TimeoutSec 120 + + $hash = (Get-FileHash -Path $tempFile -Algorithm SHA256).Hash.ToUpperInvariant() + Write-Host " SHA256: $hash" + return $hash + } + finally { + if (Test-Path $tempFile) { + Remove-Item $tempFile -Force + } + } +} + +function Get-LocalArchivePath { + param( + [string]$ArchiveRoot, + [string]$Rid, + [string]$Version + ) + + $archiveName = "aspire-cli-$Rid-$Version.zip" + $match = Get-ChildItem -Path $ArchiveRoot -File -Recurse -Filter $archiveName | Select-Object -First 1 + if ($null -eq $match) { + Write-Error "Could not find local archive '$archiveName' under '$ArchiveRoot'" + exit 1 + } + + return $match.FullName +} + +function Get-LocalFileSha256 { + param( + [string]$Path, + [string]$Description + ) + + Write-Host "Computing SHA256 for $Description from local file..." + Write-Host " Path: $Path" + + $hash = (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToUpperInvariant() + Write-Host " SHA256: $hash" + return $hash +} + +# Function to process a template file +function Process-Template { + param( + [string]$TemplatePath, + [string]$OutputFile, + [hashtable]$Substitutions + ) + + $templateName = Split-Path -Leaf $TemplatePath + Write-Host "Processing template: $templateName" + + $content = Get-Content -Path $TemplatePath -Raw + + foreach ($key in $Substitutions.Keys) { + $placeholder = "`${$key}" + $value = $Substitutions[$key] + $content = $content.Replace($placeholder, $value) + } + + Set-Content -Path $OutputFile -Value $content -NoNewline + Write-Host " Created: $OutputFile" +} + +# Build the Installers YAML block and compute hashes +Write-Host "" + +# Build all installer URLs first +$installerEntries = @() +foreach ($rid in $ridList) { + $arch = Get-ArchitectureFromRid -Rid $rid + $url = Get-InstallerUrl -Version $Version -ArtifactVersion $ArtifactVersion -Rid $rid + $installerEntries += @{ Rid = $rid; Architecture = $arch; Url = $url } +} + +# Validate URLs are accessible before downloading (fast-fail) +if ($ValidateUrls) { + Write-Host "Validating installer URLs..." + $failed = $false + foreach ($entry in $installerEntries) { + Write-Host " Checking: $($entry.Url)" + try { + $response = Invoke-WebRequest -Uri $entry.Url -Method Head -UseBasicParsing -TimeoutSec 30 + Write-Host " Status: $($response.StatusCode) OK" + } + catch { + Write-Host " ERROR: URL not accessible: $($_.Exception.Message)" + $failed = $true + } + } + + if ($failed) { + Write-Error "One or more installer URLs are not accessible. Ensure the release artifacts have been published." + exit 1 + } + Write-Host "" +} + +Write-Host "Computing SHA256 hashes..." + +$installersYaml = "Installers:" +foreach ($entry in $installerEntries) { + if ($ArchiveRoot) { + $archivePath = Get-LocalArchivePath -ArchiveRoot $ArchiveRoot -Rid $entry.Rid -Version $Version + $sha256 = Get-LocalFileSha256 -Path $archivePath -Description "$($entry.Rid) installer" + } + else { + $sha256 = Get-RemoteFileSha256 -Url $entry.Url -Description "$($entry.Rid) installer" + } + + $installersYaml += "`n- Architecture: $($entry.Architecture)" + $installersYaml += "`n InstallerUrl: $($entry.Url)" + $installersYaml += "`n InstallerSha256: $sha256" +} + +# Define substitutions +$today = Get-Date -Format "yyyy-MM-dd" +$year = Get-Date -Format "yyyy" +# Derive ReleaseNotesUrl from version if not specified +# Version format: "13.2.0" or "13.3.0-preview.1.26111.5" +# URL pattern: https://aspire.dev/whats-new/aspire-{major}-{minor}/ +if (-not $ReleaseNotesUrl) { + if ($Version -match '^(\d+)\.(\d+)') { + $ReleaseNotesUrl = "https://aspire.dev/whats-new/aspire-$($Matches[1])-$($Matches[2])/" + } else { + $ReleaseNotesUrl = "https://aspire.dev/" + } + Write-Host "Derived ReleaseNotesUrl: $ReleaseNotesUrl" +} + +$substitutions = @{ + "VERSION" = $Version + "INSTALLERS" = $installersYaml + "RELEASE_DATE" = $today + "YEAR" = $year + "RELEASE_NOTES_URL" = $ReleaseNotesUrl +} + +Write-Host "" +Write-Host "Generating manifest files..." + +# Process each template +# Output files are named {PackageIdentifier}.{type}.yaml per winget convention +$templates = @( + @{ Template = "Aspire.yaml.template"; Output = "$PackageIdentifier.yaml" }, + @{ Template = "Aspire.locale.en-US.yaml.template"; Output = "$PackageIdentifier.locale.en-US.yaml" }, + @{ Template = "Aspire.installer.yaml.template"; Output = "$PackageIdentifier.installer.yaml" } +) + +foreach ($template in $templates) { + $templatePath = Join-Path $TemplateDir $template.Template + $outputFile = Join-Path $OutputPath $template.Output + + if (-not (Test-Path $templatePath)) { + Write-Error "Template not found: $templatePath" + exit 1 + } + + Process-Template -TemplatePath $templatePath -OutputFile $outputFile -Substitutions $substitutions +} + +Write-Host "" +Write-Host "Successfully generated WinGet manifests at: $OutputPath" +Write-Host "" +Write-Host "Next steps:" +Write-Host " 1. Validate manifests: winget validate --manifest `"$OutputPath`"" +Write-Host " 2. Submit to winget-pkgs: wingetcreate submit --token YOUR_PAT `"$OutputPath`"" + +exit 0 diff --git a/eng/winget/microsoft.aspire.prerelease/Aspire.installer.yaml.template b/eng/winget/microsoft.aspire.prerelease/Aspire.installer.yaml.template new file mode 100644 index 00000000000..86df516224d --- /dev/null +++ b/eng/winget/microsoft.aspire.prerelease/Aspire.installer.yaml.template @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.10.0.schema.json + +PackageIdentifier: Microsoft.Aspire.Prerelease +PackageVersion: "${VERSION}" +InstallerType: zip +NestedInstallerType: portable +NestedInstallerFiles: +- RelativeFilePath: aspire.exe + PortableCommandAlias: aspire +Commands: +- aspire +InstallModes: +- silent +UpgradeBehavior: uninstallPrevious +ReleaseDate: ${RELEASE_DATE} +${INSTALLERS} +ManifestType: installer +ManifestVersion: 1.10.0 diff --git a/eng/winget/microsoft.aspire.prerelease/Aspire.locale.en-US.yaml.template b/eng/winget/microsoft.aspire.prerelease/Aspire.locale.en-US.yaml.template new file mode 100644 index 00000000000..c000e010071 --- /dev/null +++ b/eng/winget/microsoft.aspire.prerelease/Aspire.locale.en-US.yaml.template @@ -0,0 +1,33 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.10.0.schema.json + +PackageIdentifier: Microsoft.Aspire.Prerelease +PackageVersion: "${VERSION}" +PackageLocale: en-US +Publisher: Microsoft Corporation +PublisherUrl: https://aspire.dev/ +PublisherSupportUrl: https://github.com/dotnet/aspire/issues +PrivacyUrl: https://privacy.microsoft.com/privacystatement +PackageName: Aspire CLI (Prerelease) +PackageUrl: https://aspire.dev/ +License: MIT +LicenseUrl: https://github.com/dotnet/aspire/blob/main/LICENSE.TXT +Copyright: (c) Microsoft ${YEAR} +ShortDescription: Prerelease CLI tool for building observable, production-ready distributed applications with Aspire +Description: > + Aspire CLI (aspire) is a command-line tool for creating, running, and managing + Aspire applications. It provides commands for project scaffolding, local development, + and deployment of cloud-native distributed applications. + This is the prerelease version of the Aspire CLI. +Tags: +- dotnet +- aspire +- cloud-native +- distributed-systems +- developer-tools +- cli +- microservices +- containers +- prerelease +ReleaseNotesUrl: ${RELEASE_NOTES_URL} +ManifestType: defaultLocale +ManifestVersion: 1.10.0 diff --git a/eng/winget/microsoft.aspire.prerelease/Aspire.yaml.template b/eng/winget/microsoft.aspire.prerelease/Aspire.yaml.template new file mode 100644 index 00000000000..a5f400edd78 --- /dev/null +++ b/eng/winget/microsoft.aspire.prerelease/Aspire.yaml.template @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.10.0.schema.json + +PackageIdentifier: Microsoft.Aspire.Prerelease +PackageVersion: "${VERSION}" +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.10.0 diff --git a/eng/winget/microsoft.aspire/Aspire.installer.yaml.template b/eng/winget/microsoft.aspire/Aspire.installer.yaml.template new file mode 100644 index 00000000000..8f2d7050b0f --- /dev/null +++ b/eng/winget/microsoft.aspire/Aspire.installer.yaml.template @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.10.0.schema.json + +PackageIdentifier: Microsoft.Aspire +PackageVersion: "${VERSION}" +InstallerType: zip +NestedInstallerType: portable +NestedInstallerFiles: +- RelativeFilePath: aspire.exe + PortableCommandAlias: aspire +Commands: +- aspire +InstallModes: +- silent +UpgradeBehavior: uninstallPrevious +ReleaseDate: ${RELEASE_DATE} +${INSTALLERS} +ManifestType: installer +ManifestVersion: 1.10.0 diff --git a/eng/winget/microsoft.aspire/Aspire.locale.en-US.yaml.template b/eng/winget/microsoft.aspire/Aspire.locale.en-US.yaml.template new file mode 100644 index 00000000000..e020b5ccccc --- /dev/null +++ b/eng/winget/microsoft.aspire/Aspire.locale.en-US.yaml.template @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.10.0.schema.json + +PackageIdentifier: Microsoft.Aspire +PackageVersion: "${VERSION}" +PackageLocale: en-US +Publisher: Microsoft Corporation +PublisherUrl: https://aspire.dev/ +PublisherSupportUrl: https://github.com/dotnet/aspire/issues +PrivacyUrl: https://privacy.microsoft.com/privacystatement +Moniker: aspire +PackageName: Aspire CLI +PackageUrl: https://aspire.dev/ +License: MIT +LicenseUrl: https://github.com/dotnet/aspire/blob/main/LICENSE.TXT +Copyright: (c) Microsoft ${YEAR} +ShortDescription: CLI tool for building observable, production-ready distributed applications with Aspire +Description: > + Aspire CLI (aspire) is a command-line tool for creating, running, and managing + Aspire applications. It provides commands for project scaffolding, local development, + and deployment of cloud-native distributed applications. +Tags: +- dotnet +- aspire +- cloud-native +- distributed-systems +- developer-tools +- cli +- microservices +- containers +ReleaseNotesUrl: ${RELEASE_NOTES_URL} +ManifestType: defaultLocale +ManifestVersion: 1.10.0 diff --git a/eng/winget/microsoft.aspire/Aspire.yaml.template b/eng/winget/microsoft.aspire/Aspire.yaml.template new file mode 100644 index 00000000000..af60cb2c3e3 --- /dev/null +++ b/eng/winget/microsoft.aspire/Aspire.yaml.template @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.10.0.schema.json + +PackageIdentifier: Microsoft.Aspire +PackageVersion: "${VERSION}" +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.10.0 diff --git a/global.json b/global.json index da2bc9f162c..1820ab18a4f 100644 --- a/global.json +++ b/global.json @@ -33,8 +33,8 @@ "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.2.0", - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.25509.1", - "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.25509.1", - "Microsoft.DotNet.SharedFramework.Sdk": "11.0.0-beta.25509.1" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26160.1", + "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.26160.1", + "Microsoft.DotNet.SharedFramework.Sdk": "10.0.0-beta.26160.1" } } diff --git a/tests/Shared/RepoTesting/Aspire.RepoTesting.targets b/tests/Shared/RepoTesting/Aspire.RepoTesting.targets index 273379cf082..2a4f666b89b 100644 --- a/tests/Shared/RepoTesting/Aspire.RepoTesting.targets +++ b/tests/Shared/RepoTesting/Aspire.RepoTesting.targets @@ -33,7 +33,7 @@ `AspireProjectOrPackageReference` - maps to projects in `src/` or `src/Components/` --> - + @@ -165,6 +165,6 @@ $(MajorVersion).$(MinorVersion).$(PatchVersion) - + diff --git a/tests/Shared/RepoTesting/Directory.Packages.Helix.props b/tests/Shared/RepoTesting/Directory.Packages.Helix.props index b72e811ac6a..5545e2c45e4 100644 --- a/tests/Shared/RepoTesting/Directory.Packages.Helix.props +++ b/tests/Shared/RepoTesting/Directory.Packages.Helix.props @@ -3,81 +3,81 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + +