diff --git a/eng/homebrew/generate-cask.sh b/eng/homebrew/generate-cask.sh index 53727c781f3..e7545943571 100755 --- a/eng/homebrew/generate-cask.sh +++ b/eng/homebrew/generate-cask.sh @@ -16,7 +16,7 @@ Optional: --artifact-version VER Version segment used in the ci.dot.net artifact path (defaults to --version) --output PATH Output file path (default: ./aspire.rb) --archive-root PATH Root directory containing locally built CLI archives to hash - --validate-urls Verify all tarball URLs are accessible before downloading + --skip-url-validation Skip URL validation and downloading; use placeholder SHA256 hashes --help Show this help message EOF exit 1 @@ -26,17 +26,17 @@ VERSION="" ARTIFACT_VERSION="" OUTPUT="" ARCHIVE_ROOT="" -VALIDATE_URLS=false +SKIP_URL_VALIDATION=false while [[ $# -gt 0 ]]; do case "$1" in - --version) VERSION="$2"; shift 2 ;; - --artifact-version) ARTIFACT_VERSION="$2"; shift 2 ;; - --output) OUTPUT="$2"; shift 2 ;; - --archive-root) ARCHIVE_ROOT="$2"; shift 2 ;; - --validate-urls) VALIDATE_URLS=true; shift ;; - --help) usage ;; - *) echo "Unknown option: $1"; usage ;; + --version) VERSION="$2"; shift 2 ;; + --artifact-version) ARTIFACT_VERSION="$2"; shift 2 ;; + --output) OUTPUT="$2"; shift 2 ;; + --archive-root) ARCHIVE_ROOT="$2"; shift 2 ;; + --skip-url-validation) SKIP_URL_VALIDATION=true; shift ;; + --help) usage ;; + *) echo "Unknown option: $1"; usage ;; esac done @@ -134,8 +134,21 @@ if [[ -n "$ARCHIVE_ROOT" && ! -d "$ARCHIVE_ROOT" ]]; then exit 1 fi -# Validate URLs are accessible before downloading (fast-fail) -if [[ "$VALIDATE_URLS" == true ]]; then +if [[ -n "$ARCHIVE_ROOT" ]]; then + echo "Computing SHA256 hashes from local archives in $ARCHIVE_ROOT..." + 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")" +elif [[ "$SKIP_URL_VALIDATION" == true ]]; then + echo "SkipUrlValidation specified — using placeholder SHA256 hashes (tarball URLs will not be validated or downloaded)" + SHA256_OSX_ARM64="$(printf '0%.0s' {1..64})" + SHA256_OSX_X64="$(printf '0%.0s' {1..64})" + echo " osx-arm64: URL=$OSX_ARM64_URL, SHA256=$SHA256_OSX_ARM64 (placeholder)" + echo " osx-x64: URL=$OSX_X64_URL, SHA256=$SHA256_OSX_X64 (placeholder)" +else + # Validate URLs are accessible before downloading (fast-fail) echo "Validating tarball URLs..." failed=false for url in "$OSX_ARM64_URL" "$OSX_X64_URL"; do @@ -153,15 +166,7 @@ if [[ "$VALIDATE_URLS" == true ]]; then 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 diff --git a/eng/pipelines/azure-pipelines-unofficial.yml b/eng/pipelines/azure-pipelines-unofficial.yml index 44a7668ba1e..b76b2ac764d 100644 --- a/eng/pipelines/azure-pipelines-unofficial.yml +++ b/eng/pipelines/azure-pipelines-unofficial.yml @@ -318,13 +318,11 @@ extends: artifactVersion: $(aspireArtifactVersion) channel: $(installerChannel) archiveRoot: $(Pipeline.Workspace)/native-archives - validateUrls: false - runInstallTest: false + skipUrlValidation: true - job: Homebrew displayName: Homebrew Cask timeoutInMinutes: 30 - condition: and(succeeded(), eq(variables['installerChannel'], 'stable')) pool: name: Azure Pipelines vmImage: macOS-latest-internal @@ -343,5 +341,4 @@ extends: version: $(aspireVersion) artifactVersion: $(aspireArtifactVersion) archiveRoot: $(Pipeline.Workspace)/native-archives - validateUrls: false - runInstallTest: false + skipUrlValidation: true diff --git a/eng/pipelines/azure-pipelines.yml b/eng/pipelines/azure-pipelines.yml index ca7f1de9845..60e96a5d9b0 100644 --- a/eng/pipelines/azure-pipelines.yml +++ b/eng/pipelines/azure-pipelines.yml @@ -383,12 +383,14 @@ extends: condition: | and( succeeded(), - ne(variables['Build.Reason'], 'PullRequest'), or( - ne(dependencies.build.outputs['Windows.computeChannel.installerChannel'], 'stable'), + eq(variables['Build.Reason'], 'PullRequest'), or( - eq(variables['Build.SourceBranch'], 'refs/heads/main'), - startsWith(variables['Build.SourceBranch'], 'refs/heads/release/') + ne(dependencies.build.outputs['Windows.computeChannel.installerChannel'], 'stable'), + or( + eq(variables['Build.SourceBranch'], 'refs/heads/main'), + startsWith(variables['Build.SourceBranch'], 'refs/heads/release/') + ) ) ) ) @@ -413,16 +415,33 @@ extends: steps: - checkout: self fetchDepth: 1 + - task: DownloadPipelineArtifact@2 + displayName: 🟣Download CLI archives (win-x64) + inputs: + artifact: native_archives_win_x64 + targetPath: '$(Pipeline.Workspace)/native-archives/native_archives_win_x64' + - task: DownloadPipelineArtifact@2 + displayName: 🟣Download CLI archives (win-arm64) + inputs: + artifact: native_archives_win_arm64 + targetPath: '$(Pipeline.Workspace)/native-archives/native_archives_win_arm64' - template: /eng/pipelines/templates/prepare-winget-manifest.yml@self parameters: version: $(aspireVersion) artifactVersion: $(aspireArtifactVersion) channel: $(installerChannel) + archiveRoot: $(Pipeline.Workspace)/native-archives + # Skip URL validation and install tests unless this is a build on a + # publishing branch (main, release/*, internal/release/*) where darc + # publishes CLI archives to ci.dot.net. Feature branches and PR builds + # don't have Maestro default channels so URLs won't be valid. + # NOTE: ${{ not() }} is NOT supported in AzDO template expressions, + # so we use De Morgan's law to express "not isPublishingBranch". + skipUrlValidation: ${{ or(eq(variables['_RunAsPublic'], 'true'), eq(variables['Build.Reason'], 'PullRequest'), and(ne(variables['Build.SourceBranch'], 'refs/heads/main'), eq(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), false), eq(startsWith(variables['Build.SourceBranch'], 'refs/heads/internal/release/'), false))) }} - job: Homebrew displayName: Homebrew Cask timeoutInMinutes: 30 - condition: and(succeeded(), eq(variables['installerChannel'], 'stable')) pool: name: Azure Pipelines vmImage: macOS-latest-internal @@ -430,7 +449,19 @@ extends: steps: - checkout: self fetchDepth: 1 + - task: DownloadPipelineArtifact@2 + displayName: 🟣Download CLI archives (osx-x64) + inputs: + artifact: native_archives_osx_x64 + targetPath: '$(Pipeline.Workspace)/native-archives/native_archives_osx_x64' + - task: DownloadPipelineArtifact@2 + displayName: 🟣Download CLI archives (osx-arm64) + inputs: + artifact: native_archives_osx_arm64 + targetPath: '$(Pipeline.Workspace)/native-archives/native_archives_osx_arm64' - template: /eng/pipelines/templates/prepare-homebrew-cask.yml@self parameters: version: $(aspireVersion) artifactVersion: $(aspireArtifactVersion) + archiveRoot: $(Pipeline.Workspace)/native-archives + skipUrlValidation: ${{ or(eq(variables['_RunAsPublic'], 'true'), eq(variables['Build.Reason'], 'PullRequest'), and(ne(variables['Build.SourceBranch'], 'refs/heads/main'), eq(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), false), eq(startsWith(variables['Build.SourceBranch'], 'refs/heads/internal/release/'), false))) }} diff --git a/eng/pipelines/release-publish-nuget.yml b/eng/pipelines/release-publish-nuget.yml index 9b0a8b3860b..01ed6141097 100644 --- a/eng/pipelines/release-publish-nuget.yml +++ b/eng/pipelines/release-publish-nuget.yml @@ -71,17 +71,11 @@ extends: name: NetCore1ESPool-Internal image: windows.vs2026preview.scout.amd64 os: windows - # Network isolation policy for ES4.2.4 CFS compliance. - # CFSClean is intentionally omitted here because it blocks all NuGet endpoints - # (api.nuget.org, www.nuget.org, etc.) via DNS sinkhole and this release pipeline - # must push packages to NuGet.org via the 1ES.PublishNuget@1 task. ES4.2.4 requires - # pipelines to *consume* packages from internal feeds (which we do — NuGet.config - # only uses pkgs.dev.azure.com/dnceng feeds), but does not prohibit *publishing* to - # public registries. CFSClean2 is retained to block Docker, PowerShell Gallery, and - # other endpoints not needed by this pipeline. - # See: https://eng.ms/docs/microsoft-ai/webxt/feed-verticals/start-experiences/fv-live-site/sfi-executor-centric-wiki/skills/security/mdp-cfs-security-change/skill + # Network isolation policy: match main build pipeline for ES4.2.4 CFS compliance. + # releaseJob templateContext provides approved network access for external publishing + # (NuGet.org, GitHub API for WinGet/Homebrew, Maestro). settings: - networkIsolationPolicy: Permissive,CFSClean2 + networkIsolationPolicy: Permissive,CFSClean,CFSClean2 stages: # ===== STAGE 1: PREPARE ARTIFACTS ===== diff --git a/eng/pipelines/templates/BuildAndTest.yml b/eng/pipelines/templates/BuildAndTest.yml index 5f3bf630398..16342ced2f0 100644 --- a/eng/pipelines/templates/BuildAndTest.yml +++ b/eng/pipelines/templates/BuildAndTest.yml @@ -206,6 +206,36 @@ steps: PathtoPublish: '${{ parameters.repoArtifactsPath }}/packages/Release/vscode' ArtifactName: aspire-vscode-extension + # Publish Windows CLI archives as separate artifacts so the Prepare Installers + # stage can download them by name (matching the native_archives_ pattern + # used by build_sign_native for macOS/Linux). + - ${{ if eq(parameters.isWindows, 'true') }}: + - ${{ each targetRid in parameters.targetRids }}: + - pwsh: | + $ErrorActionPreference = 'Stop' + $sourceDir = '${{ parameters.repoArtifactsPath }}/packages' + $pattern = 'aspire-cli-${{ targetRid }}-*' + $stagingDir = '$(Build.StagingDirectory)/native_archives_${{ replace(targetRid, '-', '_') }}' + + New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null + + $files = Get-ChildItem -Path $sourceDir -Recurse -Filter $pattern + if ($files.Count -eq 0) { + Write-Error "No CLI archives matching '$pattern' found under '$sourceDir'" + exit 1 + } + + foreach ($f in $files) { + Copy-Item $f.FullName $stagingDir + Write-Host "Staged: $($f.Name)" + } + displayName: 🟣Stage CLI archives (${{ targetRid }}) + - task: 1ES.PublishBuildArtifacts@1 + displayName: 🟣Publish native_archives_${{ replace(targetRid, '-', '_') }} + inputs: + PathtoPublish: $(Build.StagingDirectory)/native_archives_${{ replace(targetRid, '-', '_') }} + ArtifactName: native_archives_${{ replace(targetRid, '-', '_') }} + - script: ${{ parameters.dotnetScript }} build tests/workloads.proj @@ -229,6 +259,7 @@ steps: DOTNET_ROOT: $(Build.SourcesDirectory)\.dotnet TEST_LOG_PATH: $(Build.SourcesDirectory)\artifacts\log\$(_BuildConfig)\Aspire.Templates.Tests displayName: 🟣Run Template tests + continueOnError: true # Public pipeline - helix tests - ${{ if eq(parameters.runAsPublic, 'true') }}: diff --git a/eng/pipelines/templates/prepare-homebrew-cask.yml b/eng/pipelines/templates/prepare-homebrew-cask.yml index 8273527099a..20a4b0bfea1 100644 --- a/eng/pipelines/templates/prepare-homebrew-cask.yml +++ b/eng/pipelines/templates/prepare-homebrew-cask.yml @@ -8,12 +8,11 @@ parameters: - name: archiveRoot type: string default: '' - - name: validateUrls + - name: skipUrlValidation type: boolean - default: true - - name: runInstallTest - type: boolean - default: true + default: false + # runInstallTest is derived from skipUrlValidation (they are logical inverses). + # Callers should only set skipUrlValidation; install tests run when URLs are valid. steps: - bash: | @@ -53,9 +52,9 @@ steps: args+=(--archive-root "$ARCHIVE_ROOT") fi - validateUrlsNormalized="$(printf '%s' "${VALIDATE_URLS:-false}" | tr '[:upper:]' '[:lower:]')" - if [ "$validateUrlsNormalized" = "true" ]; then - args+=(--validate-urls) + skipUrlValidation="$(printf '%s' "${SKIP_URL_VALIDATION:-false}" | tr '[:upper:]' '[:lower:]')" + if [ "$skipUrlValidation" = "true" ]; then + args+=(--skip-url-validation) fi "$(Build.SourcesDirectory)/eng/homebrew/generate-cask.sh" "${args[@]}" @@ -66,7 +65,7 @@ steps: displayName: 🟣Generate Homebrew cask env: ARCHIVE_ROOT: '${{ parameters.archiveRoot }}' - VALIDATE_URLS: '${{ parameters.validateUrls }}' + SKIP_URL_VALIDATION: '${{ parameters.skipUrlValidation }}' - bash: | set -euo pipefail @@ -102,22 +101,36 @@ steps: echo "" echo "Auditing cask via local tap..." + skipUrlValidation="$(printf '%s' "${SKIP_URL_VALIDATION:-false}" | tr '[:upper:]' '[:lower:]')" + upstreamStatusCode="$(curl -sS -o /dev/null -w "%{http_code}" "https://api.github.com/repos/Homebrew/homebrew-cask/contents/$targetPath")" - auditArgs=(--cask --online "$tapName/$caskName") - auditCommand="brew audit --cask --online $tapName/$caskName" + auditArgs=(--cask) isNewCask=false if [ "$upstreamStatusCode" = "404" ]; then - echo "Detected new upstream cask; running new-cask audit." - auditArgs=(--cask --new --online "$tapName/$caskName") - auditCommand="brew audit --cask --new --online $tapName/$caskName" isNewCask=true + if [ "$skipUrlValidation" = "true" ]; then + echo "Detected new upstream cask but skipping --new audit (--new implies download checks and URLs are not expected to be valid for this build)." + else + echo "Detected new upstream cask; running new-cask audit." + auditArgs+=(--new) + fi elif [ "$upstreamStatusCode" = "200" ]; then - echo "Detected existing upstream cask; running standard online audit." + echo "Detected existing upstream cask; running standard audit." else echo "##[error]Could not determine whether $targetPath exists upstream (HTTP $upstreamStatusCode)" exit 1 fi + if [ "$skipUrlValidation" = "true" ]; then + echo "Skipping --online audit (URLs are not expected to be valid for this build)." + else + echo "Including --online audit (URLs are expected to be valid)." + auditArgs+=(--online) + fi + + auditArgs+=("$tapName/$caskName") + auditCommand="brew audit ${auditArgs[*]}" + auditCode=0 brew audit "${auditArgs[@]}" || auditCode=$? @@ -131,6 +144,8 @@ steps: trap - EXIT cleanup displayName: 🟣Validate cask syntax and audit + env: + SKIP_URL_VALIDATION: '${{ parameters.skipUrlValidation }}' - bash: | set -euo pipefail @@ -203,7 +218,7 @@ steps: echo " Confirmed: aspire is no longer in PATH" fi displayName: 🟣Test cask install/uninstall - condition: and(succeeded(), eq('${{ parameters.runInstallTest }}', 'true')) + condition: and(succeeded(), eq(${{ parameters.skipUrlValidation }}, false)) - bash: | set -euo pipefail @@ -242,7 +257,7 @@ steps: echo "Wrote Homebrew validation summary to $outputPath" cat "$outputPath" displayName: 🟣Write Homebrew validation summary - condition: and(succeeded(), eq('${{ parameters.runInstallTest }}', 'true')) + condition: and(succeeded(), eq(${{ parameters.skipUrlValidation }}, false)) - bash: | set -euo pipefail diff --git a/eng/pipelines/templates/prepare-winget-manifest.yml b/eng/pipelines/templates/prepare-winget-manifest.yml index 26231ea2da3..1bf6a6b818e 100644 --- a/eng/pipelines/templates/prepare-winget-manifest.yml +++ b/eng/pipelines/templates/prepare-winget-manifest.yml @@ -10,12 +10,11 @@ parameters: - name: archiveRoot type: string default: '' - - name: validateUrls + - name: skipUrlValidation type: boolean - default: true - - name: runInstallTest - type: boolean - default: true + default: false + # runInstallTest is derived from skipUrlValidation (they are logical inverses). + # Callers should only set skipUrlValidation; install tests run when URLs are valid. steps: - pwsh: | @@ -101,8 +100,8 @@ steps: $args.ArchiveRoot = '${{ parameters.archiveRoot }}' } - if ('${{ parameters.validateUrls }}' -eq 'true') { - $args.ValidateUrls = $true + if ('${{ parameters.skipUrlValidation }}' -eq 'true') { + $args.SkipUrlValidation = $true } & "$(Build.SourcesDirectory)/eng/winget/generate-manifests.ps1" @args @@ -226,7 +225,7 @@ steps: exit 1 } displayName: 🟣Test WinGet manifest install/uninstall - condition: and(succeeded(), eq('${{ parameters.runInstallTest }}', 'true')) + condition: and(succeeded(), eq(${{ parameters.skipUrlValidation }}, false)) - pwsh: | Copy-Item "$(Build.SourcesDirectory)/eng/winget/dogfood.ps1" "$(Build.StagingDirectory)/winget-manifests/dogfood.ps1" diff --git a/eng/winget/generate-manifests.ps1 b/eng/winget/generate-manifests.ps1 index c97ea2d5ae9..ca72d128f92 100644 --- a/eng/winget/generate-manifests.ps1 +++ b/eng/winget/generate-manifests.ps1 @@ -35,9 +35,10 @@ 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. +.PARAMETER SkipUrlValidation + When specified, skips all URL operations: both the HEAD-request validation and downloading + installer files to compute SHA256 hashes. Placeholder hashes are used instead. + This is useful for PR validation where the installer URLs have not been published yet. .EXAMPLE ./generate-manifests.ps1 -Version "13.3.0-preview.1.26111.5" ` @@ -47,7 +48,7 @@ ./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 + -Rids "win-x64,win-arm64" #> [CmdletBinding()] @@ -74,7 +75,7 @@ param( [string]$ReleaseNotesUrl, [Parameter(Mandatory = $false)] - [switch]$ValidateUrls + [switch]$SkipUrlValidation ) $ErrorActionPreference = 'Stop' @@ -264,8 +265,33 @@ foreach ($rid in $ridList) { $installerEntries += @{ Rid = $rid; Architecture = $arch; Url = $url } } -# Validate URLs are accessible before downloading (fast-fail) -if ($ValidateUrls) { +if ($ArchiveRoot) { + Write-Host "Computing SHA256 hashes from local archives in $ArchiveRoot..." + + $installersYaml = "Installers:" + foreach ($entry in $installerEntries) { + $archivePath = Get-LocalArchivePath -ArchiveRoot $ArchiveRoot -Rid $entry.Rid -Version $Version + $sha256 = Get-LocalFileSha256 -Path $archivePath -Description "$($entry.Rid) installer" + + $installersYaml += "`n- Architecture: $($entry.Architecture)" + $installersYaml += "`n InstallerUrl: $($entry.Url)" + $installersYaml += "`n InstallerSha256: $sha256" + } +} elseif ($SkipUrlValidation) { + Write-Host "SkipUrlValidation specified — using placeholder SHA256 hashes (installer URLs will not be validated or downloaded)" + Write-Host "" + + $installersYaml = "Installers:" + foreach ($entry in $installerEntries) { + $placeholderHash = "0" * 64 + Write-Host " $($entry.Rid): URL=$($entry.Url), SHA256=$placeholderHash (placeholder)" + + $installersYaml += "`n- Architecture: $($entry.Architecture)" + $installersYaml += "`n InstallerUrl: $($entry.Url)" + $installersYaml += "`n InstallerSha256: $placeholderHash" + } +} else { + # Validate URLs are accessible before downloading (fast-fail) Write-Host "Validating installer URLs..." $failed = $false foreach ($entry in $installerEntries) { @@ -285,23 +311,17 @@ if ($ValidateUrls) { exit 1 } Write-Host "" -} -Write-Host "Computing SHA256 hashes..." + Write-Host "Computing SHA256 hashes by downloading from URLs..." -$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 { + $installersYaml = "Installers:" + foreach ($entry in $installerEntries) { $sha256 = Get-RemoteFileSha256 -Url $entry.Url -Description "$($entry.Rid) installer" - } - $installersYaml += "`n- Architecture: $($entry.Architecture)" - $installersYaml += "`n InstallerUrl: $($entry.Url)" - $installersYaml += "`n InstallerSha256: $sha256" + $installersYaml += "`n- Architecture: $($entry.Architecture)" + $installersYaml += "`n InstallerUrl: $($entry.Url)" + $installersYaml += "`n InstallerSha256: $sha256" + } } # Define substitutions