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
18 changes: 18 additions & 0 deletions docs/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ Before starting a release:
1. **Signed Build**: Have a successful signed build from the official [`microsoft-aspire`](https://dev.azure.com/dnceng/internal/_build?definitionId=1602) pipeline
- The build will be selected from a dropdown when running the release pipeline
- The build should have a `BAR ID - NNNNNN` tag (auto-extracted by the pipeline)
- **For GA ships**: queue the source build with `aspireCliChannelOverride: stable`.
The default `auto` resolution bakes `AspireCliChannel=staging` for every
`release/*` source build (correct for dogfood/staging), so a stable ship
requires the explicit override. The release pipeline enforces this with a
`Validate Source Build CLI Channel` guard that fetches the source build's
tags and fails if `aspire-cli-channel - stable` is not present.
See [microsoft/aspire#17527](https://github.com/microsoft/aspire/issues/17527)
for the bug this guard prevents, and the `AllowNonStableCliChannel`
parameter below for the documented escape hatch.

2. **Release Branch**: Ensure the release branch exists (e.g., `release/9.2`)

Expand Down Expand Up @@ -97,6 +106,7 @@ Before starting a release:
| `SkipGitHubTasks` | Set `true` to skip dispatching the GH workflow | `false` |
| `SkipReleaseAssets` | Set `true` to skip uploading aspire-cli-* assets to the GitHub release | `false` |
| `SkipHomebrewValidation` | Set `true` if re-running after a successful Homebrew cask validation (validates against the live GH release) | `false` |
| `AllowNonStableCliChannel` | **Escape hatch**: bypass the `Validate Source Build CLI Channel` guard. Leave `false` for any GA ship — the guard catches a source build queued without `aspireCliChannelOverride=stable`. Setting this to `true` lets a source build with `AspireCliChannel=staging` (or any other value) publish to nuget.org. There is currently no known use case; the parameter exists so a real emergency override is documented rather than ad-hoc. | `false` |
| `GitHubTasksWorkflowRef` | Ref to load `release-github-tasks.yml` from when dispatching. Only affects the workflow source — the release branch/commit are passed via inputs. Override only when testing pipeline changes on a topic branch. | `main` |

4. Select the **Resources** button in the bottom right, then select the source build from the `aspire-build` dropdown
Expand All @@ -109,6 +119,14 @@ Before starting a release:
source build (after the tag-emitting change in `azure-pipelines.yml`
is on that release branch) or pass an explicit `ReleaseVersion`
override below.
- **For GA ships**: also verify the source build has the
`aspire-cli-channel - stable` tag. If it shows `aspire-cli-channel - staging`
(or any other value), the build was queued without
`aspireCliChannelOverride=stable` — pick a different source build, or
queue a new one. The `Validate Source Build CLI Channel` step will fail
the pipeline if you proceed without a `stable`-tagged build (unless
`AllowNonStableCliChannel` is set, which you almost certainly do not
want for a GA ship).
5. Click "Run" and monitor the pipeline. The final stage (`GitHubTasks`)
dispatches `release-github-tasks.yml`, waits for it to complete, and
then uploads the `aspire-cli-*` archives from the source build's
Expand Down
118 changes: 118 additions & 0 deletions eng/pipelines/release-publish-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ parameters:
type: boolean
default: false

# Source builds from `release/*` branches bake `AspireCliChannel=staging` by
# default (see eng/pipelines/templates/build_sign_native.yml). A GA ship build
# must explicitly queue `microsoft-aspire` with `aspireCliChannelOverride=stable`
# so the CLI binary identifies as `stable` and `aspire init` drops the
# nuget.org-only nuget.config that GA users expect. To enforce this, the
# `Validate Source Build CLI Channel` step below fetches the source build's
# tags and fails the pipeline unless the `aspire-cli-channel - stable` tag is
# present. Set this parameter to `true` only in the rare case of intentionally
# publishing a non-stable-channel build to nuget.org (currently no known
# use case — exists as a documented escape hatch).
# See: https://github.com/microsoft/aspire/pull/17528 (#17527) and
# https://github.com/microsoft/aspire/issues/17550 (longer-term 13.5+ work
# to decouple channel identity from build-time baking).
- name: AllowNonStableCliChannel
displayName: '[Advanced] Allow source build with AspireCliChannel != stable (escape hatch; leave false for GA ships)'
type: boolean
default: false

# Ref used when invoking `workflow_dispatch` against release-github-tasks.yml.
# GitHub loads the workflow file *from this ref*, so it controls which version
# of the workflow runs. The branch/commit being released is passed separately
Expand Down Expand Up @@ -427,6 +445,106 @@ extends:
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

# ===== VALIDATE SOURCE BUILD CLI CHANNEL =====
# Source builds from `release/*` branches bake `AspireCliChannel=staging`
# by default (see eng/pipelines/templates/build_sign_native.yml and
# eng/scripts/compute-cli-channel.ps1). To ship a build as GA, the
# source build must have been queued with `aspireCliChannelOverride=stable`,
# which causes compute-cli-channel.ps1 to also tag the source build
# with `aspire-cli-channel - stable`.
#
# Without this guard, forgetting the queue-time override on the GA
# ship build silently publishes a `staging`-baked CLI binary to
# nuget.org; `aspire init` then drops a nuget.config that maps
# Aspire.* to the staging feed, which 404s for GA users once the
# darc-pub feed for that SHA expires. See
# https://github.com/microsoft/aspire/issues/17527 for the bug this
# guard is the safety net against.
#
# Builds from before the channel-tag emission landed will not have
# this tag; those cannot be shipped as GA without queueing a fresh
# source build. The `AllowNonStableCliChannel` parameter is the
# documented escape hatch.
- ${{ if and(eq(parameters.DryRun, false), eq(parameters.AllowNonStableCliChannel, false)) }}:
- powershell: |
$buildId = "$(resources.pipeline.aspire-build.runID)"
$org = "$(System.CollectionUri)"
$project = "internal"

Write-Host "Fetching build tags for build: $buildId"

$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 ', ')"

# Tags look like 'aspire-cli-channel - stable'. The ' - ' separator
# matches the existing 'BAR ID - <id>' and 'release-version - X.Y.Z'
# tag styles emitted elsewhere in the source build.
$channelTags = @($response.value | Where-Object { $_ -match '^aspire-cli-channel - (.+)$' })

if ($channelTags.Count -eq 0) {
Write-Error @"
Source build $buildId has no 'aspire-cli-channel - *' tag.

This means either:
(a) The build predates the channel-tag emission in
eng/scripts/compute-cli-channel.ps1 (anything before that
landed on the source branch). Queue a fresh source build
from the current release branch and re-run this pipeline
against it.
(b) The compute-cli-channel.ps1 step was modified to drop
the `##vso[build.addbuildtag]` emission. Restore it.

To override this guard for an intentional non-stable ship,
re-run with -AllowNonStableCliChannel `$true.
"@
exit 1
}

if ($channelTags.Count -gt 1) {
Write-Error "Source build $buildId has multiple 'aspire-cli-channel - *' tags ($($channelTags -join ', ')). Refusing to guess."
exit 1
}

$resolvedChannel = ($channelTags[0] -replace '^aspire-cli-channel - ', '').Trim()
Write-Host "Source build CLI channel: '$resolvedChannel'"

if ($resolvedChannel -ne 'stable') {
Write-Error @"
Source build $buildId has aspire-cli-channel='$resolvedChannel', not 'stable'.

The selected microsoft-aspire build was queued with the default
AspireCliChannel resolution, which bakes 'staging' for release/*
branches. Publishing this build to nuget.org would ship a CLI
binary that identifies as '$resolvedChannel' and writes the
corresponding nuget.config from ``aspire init`` for end users —
see https://github.com/microsoft/aspire/issues/17527.

To produce a GA ship build, re-queue microsoft-aspire
(definition 1602) with -aspireCliChannelOverride stable, then
re-run this pipeline against the new source build.

To override this guard for an intentional non-stable ship,
re-run with -AllowNonStableCliChannel `$true.
"@
exit 1
}

Write-Host "✓ Source build $buildId has AspireCliChannel=stable; GA ship may proceed."
} catch {
Write-Error "Failed to fetch build tags: $_"
exit 1
}
displayName: 'Validate Source Build CLI Channel'
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

# ===== DOWNLOAD PACKAGES =====
# Artifacts are downloaded automatically via templateContext.inputs above

Expand Down
75 changes: 14 additions & 61 deletions eng/pipelines/templates/build_sign_native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,71 +92,24 @@ jobs:
# be visible here, and there is no other AzDO consumer of this value.
# The variable is job-scoped (no isOutput=true) since it is consumed by
# a later step in the same job.
#
# The cascade itself lives in eng/scripts/compute-cli-channel.ps1 so it
# can be unit-tested by ComputeCliChannelTests; this step only resolves
# DotNetFinalVersionKind (which needs the in-tree dotnet + Versions.props)
# and forwards AzDO build context to the script.
- pwsh: |
$ErrorActionPreference = 'Stop'
$reason = '$(Build.Reason)'
$sourceBranch = '$(Build.SourceBranch)'
$prNumber = '$(System.PullRequest.PullRequestNumber)'
# Template-time substitution: the value is the resolved
# aspireCliChannelOverride parameter literal, never a runtime
# variable. Quoting protects an empty/default value.
$override = '${{ parameters.aspireCliChannelOverride }}'
Write-Host "Build.Reason: '$reason'"
Write-Host "Build.SourceBranch: '$sourceBranch'"
Write-Host "System.PullRequest.PullRequestNumber: '$prNumber'"
Write-Host "aspireCliChannelOverride: '$override'"

$versionKind = & "$(Build.SourcesDirectory)/$(dotnetScript)" msbuild "$(Build.SourcesDirectory)/eng/Versions.props" -getProperty:DotNetFinalVersionKind
$versionKind = $versionKind.Trim()
Write-Host "DotNetFinalVersionKind: '$versionKind'"

if ($override -and $override -ne 'auto') {
# Operator override path. Validate against the same accepted set
# that IdentityChannelReader.IsValidChannel enforces at CLI startup
# so a typo here fails the pipeline step rather than producing a
# binary that refuses to boot. pr-<N> is intentionally excluded
# from the override set — PR builds always come from the
# PullRequest reason arm below.
if ($override -notin @('stable', 'staging', 'daily')) {
throw "aspireCliChannelOverride='$override' is not one of: auto, stable, staging, daily."
}
$channel = $override.ToLowerInvariant()
}
elseif ($reason -eq 'PullRequest') {
# Defense in depth: validate digit-only PR number rather than just
# non-emptiness. If the agent ever returns the literal macro string
# (e.g. '$(System.PullRequest.PullRequestNumber)' unresolved) this
# catches it at compute time rather than letting an invalid
# AspireCliChannel value reach the build and be rejected later by
# IdentityChannelReader.IsValidChannel — clearer failure attribution.
if ($prNumber -notmatch '^\d+$') {
throw "Build.Reason is 'PullRequest' but System.PullRequest.PullRequestNumber was not a numeric PR number: '$prNumber'."
}
# Bake the resolved hive label directly into AspireCliChannel. The CLI
# consumes this verbatim and avoids the legacy "pr" + parsed-PrNumber join.
$channel = "pr-$prNumber"
} elseif ($sourceBranch -match '^refs/heads/(release|internal/release)/') {
# Release/internal-release branches always produce staging artifacts —
# they are published to the staging feed for dogfooding and only later
# promoted to nuget.org. This must be checked BEFORE the
# `versionKind == release` arm, because a release-branch build also sets
# StabilizePackageVersion=true (→ DotNetFinalVersionKind=release) once
# we are stabilizing for ship. Without this ordering, the stabilized
# staging build would bake AspireCliChannel=stable and `aspire init`
# would drop a nuget.config with no staging feed mapping, causing
# `aspire add` to resolve Aspire.* packages from nuget.org (older
# versions) or fail to resolve the +sha-pinned Aspire.AppHost.Sdk.
# See https://github.com/microsoft/aspire/issues/17527.
$channel = 'staging'
} elseif ($versionKind -eq 'release') {
$channel = 'stable'
} else {
# main and any other branch fall through to daily
$channel = 'daily'
}

Write-Host "Aspire CLI channel: $channel"
Write-Host "##vso[task.setvariable variable=aspireCliChannel]$channel"
# Template-time substitution for $Override: the value is the resolved
# aspireCliChannelOverride parameter literal, never a runtime variable.
# Quoting protects an empty/default value.
& "$(Build.SourcesDirectory)/eng/scripts/compute-cli-channel.ps1" `
-Reason '$(Build.Reason)' `
-SourceBranch '$(Build.SourceBranch)' `
-PrNumber '$(System.PullRequest.PullRequestNumber)' `
-Override '${{ parameters.aspireCliChannelOverride }}' `
-VersionKind $versionKind
name: computeCliChannel
displayName: 🟣Determine Aspire CLI channel

Expand Down
Loading
Loading