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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 24 additions & 19 deletions eng/homebrew/generate-cask.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 2 additions & 5 deletions eng/pipelines/azure-pipelines-unofficial.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -343,5 +341,4 @@ extends:
version: $(aspireVersion)
artifactVersion: $(aspireArtifactVersion)
archiveRoot: $(Pipeline.Workspace)/native-archives
validateUrls: false
runInstallTest: false
skipUrlValidation: true
41 changes: 36 additions & 5 deletions eng/pipelines/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/')
)
)
)
)
Expand All @@ -413,24 +415,53 @@ 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
os: macOS
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))) }}
14 changes: 4 additions & 10 deletions eng/pipelines/release-publish-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 =====
Expand Down
31 changes: 31 additions & 0 deletions eng/pipelines/templates/BuildAndTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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_<rid> 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
Expand All @@ -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') }}:
Expand Down
49 changes: 32 additions & 17 deletions eng/pipelines/templates/prepare-homebrew-cask.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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[@]}"
Expand All @@ -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
Expand Down Expand Up @@ -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=$?

Expand All @@ -131,6 +144,8 @@ steps:
trap - EXIT
cleanup
displayName: 🟣Validate cask syntax and audit
env:
SKIP_URL_VALIDATION: '${{ parameters.skipUrlValidation }}'

- bash: |
set -euo pipefail
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 7 additions & 8 deletions eng/pipelines/templates/prepare-winget-manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading
Loading