Skip to content
Closed
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
12 changes: 6 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,8 @@ jobs:
- name: Validate lockfile registries
# Allowlist scoped to lines starting with "resolved" (the only lines in
# yarn.lock that carry a tarball URL — `npm:` aliases don't).
# CONTRIBUTING.MD asks contributors to regenerate yarn.lock through the
# internal dotnet-public-npm feed; an allowlist catches drift to any
# extension/CONTRIBUTING.md documents the internal dotnet-public-npm feed
# requirement; an allowlist catches drift to any
# other public mirror (npmmirror.com, jsr.io, github.com tarballs, ...)
# rather than only npmjs.org and yarnpkg.com.
run: |
Expand All @@ -398,8 +398,8 @@ jobs:
# through node.exe, POSIX tar) are only exercised on contributor machines
# and never on a fresh CI image. extension_tests_win covers the Windows
# paths; this job covers Linux/macOS. macOS is omitted because the only
# platform-specific branch beyond Linux is the cache path, which is
# explicitly overridden via COREPACK_HOME in the build entrypoints.
# platform-specific branch beyond Linux is the cache path, which this job
# explicitly overrides via COREPACK_HOME.
runs-on: ubuntu-latest
env:
NPM_REGISTRY: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/
Expand Down Expand Up @@ -434,8 +434,8 @@ jobs:
corepack enable
- name: Seed Corepack Yarn cache via prepareCorepackYarn.mjs
# Exercises the script's non-Windows branches against a clean cache
# directory. Failing here means a contributor on Linux/macOS following
# CONTRIBUTING.MD would also be broken.
# directory. Failing here means the CI/AzDO Corepack bootstrap path would
# also be broken.
run: node ./scripts/prepareCorepackYarn.mjs
- name: Validate lockfile registries
# Mirror of the same allowlist guard in extension_tests_win so this
Expand Down
148 changes: 148 additions & 0 deletions docs/cli-staging-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Validating staging feed routing with a local CLI build

This document describes how to make a locally built Aspire CLI resolve `Aspire.*`
packages exactly the way an official **staging** (or **stable**) build would, so the
staging feed-routing behavior can be validated end-to-end without an official build.

## Background

A staging-identity CLI is an official release-branch build whose own commit always has
a SHA-specific `darc-pub-microsoft-aspire-<commit>` feed carrying its matching packages
(prerelease-shaped `13.4.0-preview.*` and stable-shaped `13.4.0` alike). Feed
**provenance** is decided by the CLI's baked build **identity** (`AspireCliChannel`),
while version **filtering** (the channel quality) is decided by the CLI's **version
shape**. See `PackagingService.ShouldUseSharedStagingFeed`.

A locally built CLI bakes a `local` identity and an unstamped informational version, so
it never synthesizes a staging channel and never derives a darc feed. The two diagnostic
overrides below let you simulate the staging path locally.

## The two diagnostic overrides

Both are read by `PackagingService` only (their blast radius is limited to staging
feed-routing decisions — they do **not** change the global identity used for hive or
package-directory lookups):

| Config key | Purpose |
| --- | --- |
| `overrideCliIdentityChannel` | Forces the identity used for staging-feed routing decisions. Must be a valid channel (`stable`, `staging`, `daily`, `local`, or `pr-<N>`); invalid values are ignored and the real identity is used. |
| `overrideCliInformationalVersion` | Forces the informational version that both the SHA-derivation provider and the version-shape (quality) predicate read. The part after `+` (truncated to 8 chars) builds the darc URL; the version part determines stable-vs-prerelease shape. |

**Both overrides are required** to reach the darc path from a local build:

- Identity override alone → the SHA is still unstamped, so the darc URL can't be derived.
- Version override alone → the identity stays `local`, so routing never selects the darc feed.

When either override is set, the CLI emits a one-time warning so an overridden
identity/feed can't silently resolve packages on a normal invocation.

## Recipe

1. Build the CLI locally:

```bash
./build.sh --build /p:SkipNativeBuild=true
```

2. In the apphost directory, set `channel: staging` in `aspire.config.json` (this is what
`aspire add` filters the synthesized channels to):

```json
{
"channel": "staging"
}
```

3. Set the two overrides (environment variables are the simplest; they are read
case-insensitively with no prefix):

```bash
export overrideCliIdentityChannel=staging
export overrideCliInformationalVersion=13.4.0-preview.1.26280.6+<full-commit-hash>
```

Use a real release-branch build commit hash so the derived feed actually exists if you
intend to restore; any 8+ char hex suffix works for inspecting the resolved feed URL.

4. Run `aspire add` with debug logging and confirm the resolved darc feed:

```bash
aspire add foundry --debug
```

The logs should show the staging channel resolving `Aspire*` to
`.../darc-pub-microsoft-aspire-<first8-of-commit>/...` rather than the shared
`dnceng/.../dotnet9` daily feed.

To simulate a **stable**-shaped staging build, use a stable-shaped version override
(e.g. `13.4.0+<full-commit-hash>`); the channel quality becomes `Stable` while the feed
stays the darc feed.

## Helper scripts

`eng/scripts/debug-staging.{sh,ps1}` and `eng/scripts/debug-stable.{sh,ps1}` wrap the
recipe above. Both target identity `staging` and expect the **same** darc feed; they
differ only in version shape/quality:

| Script | Version shape | Expected quality | Scenario |
| --- | --- | --- | --- |
| `debug-staging` | prerelease (`13.4.0-preview.*`) | `Both` | [#17744](https://github.com/microsoft/aspire/issues/17744) — the bug this PR fixes |
| `debug-stable` | stable (`13.4.0`) | `Stable` | [#17527](https://github.com/microsoft/aspire/issues/17527) — stable-shaped release build |

Each script computes the expected `darc-pub-microsoft-aspire-<sha8>` feed and supports
three modes:

- **Validate (default):** runs `aspire add <pkg> --debug` in a throwaway directory and
asserts the darc feed appears in the resolution log. Exits non-zero if it doesn't.
- **`--print-env` / `-PrintEnv`:** emits `export`/`$env:` lines you apply to your current
shell. Every subsequent `aspire` command then behaves like the simulated build.
- **`--shell` / `-Shell`:** opens an interactive subshell with the overrides applied and
the target CLI first on `PATH`. It also points `NUGET_PACKAGES` at an isolated, per-sha
cache so restores from the simulated staging feed never contaminate your real global
package cache. Exiting the subshell restores normal behavior.

Common flags: `--sha <commit>` (required, 8–40 hex), `--cli <path>` (CLI to drive),
`--pr <N>` (install that PR's full-bundle build first, then target it), `--version <ver>`.

### Interactive validation against an installed PR build

You don't need a local source build — the easiest carrier is an installed **PR build**,
which is a real full-bundle `~/.aspire` install. Install it, then make it behave like a
staging build for a full `aspire new` / `aspire add` / run flow:

```bash
# 1. Install the PR's full-bundle build.
./eng/scripts/get-aspire-cli-pr.sh 17743

# 2a. Apply staging overrides to the CURRENT shell (every aspire command is staging-flavored):
eval "$(./eng/scripts/debug-stable.sh --sha <commit> --print-env)"
aspire new # behaves like the simulated staging build
aspire add foundry
# revert when done:
unset channel overrideCliIdentityChannel overrideCliInformationalVersion

# 2b. ...or get a throwaway subshell instead (overrides vanish on 'exit'):
./eng/scripts/debug-stable.sh --pr 17743 --sha <commit> --shell
```

PowerShell is identical with the `.ps1` siblings:

```powershell
./eng/scripts/get-aspire-cli-pr.ps1 17743
./eng/scripts/debug-stable.ps1 -Sha <commit> -PrintEnv | Invoke-Expression
# ...or:
./eng/scripts/debug-stable.ps1 -Pr 17743 -Sha <commit> -Shell
```

The overrides are scoped to `PackagingService` feed routing and only ever live in the
shell/subshell environment, so nothing is written to global or per-project config.

## Validation matrix

| Identity | Version shape | Expected feed | Expected quality |
| --- | --- | --- | --- |
| `staging` | prerelease | `darc-pub-microsoft-aspire-<sha8>` | `Both` |
| `staging` | stable | `darc-pub-microsoft-aspire-<sha8>` | `Stable` |
| `daily` | any | shared `dnceng/.../dotnet9` daily feed | `Both` |
| `local` / `pr-<N>` | any | local/PR hive + implicit (no staging synthesis) | n/a |
| `stable` | stable | nuget.org | `Stable` |
2 changes: 2 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ dotnet test --no-launch-profile -- \

To test changes from a specific pull request locally, see [dogfooding-pull-requests.md](/docs/dogfooding-pull-requests.md) for instructions on installing Aspire CLI and NuGet packages built by that PR's CI run.

To validate how the CLI resolves `Aspire.*` packages for **staging** and **stable** release-branch builds (including making an installed PR build behave like a staging build), see [cli-staging-validation.md](/docs/cli-staging-validation.md).

## Coding Agents

Aspire uses GitHub Copilot automatic code review on pull requests. We expect Copilot review comments to be reviewed and addressed before merging, either by making the requested change or by explaining why a suggested change is not needed.
Expand Down
21 changes: 21 additions & 0 deletions eng/pipelines/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@ parameters:
displayName: 'Package VS Code Extension as Pre-Release'
type: boolean
default: false
# Operator-controlled override for the AspireCliChannel baked into native CLI
# binaries by build_sign_native.yml. The default 'auto' lets the pipeline pick
# stable / staging / daily / pr-<N> from Build.Reason and Build.SourceBranch
# (where release/* and internal/release/* branches always resolve to 'staging'
# so stabilizing dogfood builds aren't mis-baked as 'stable' — see
# https://github.com/microsoft/aspire/issues/17527). Set this to 'stable' when
# kicking off the official GA ship build from a release/* branch so the
# distributed binary identifies as stable and `aspire init` writes a
# nuget.org-only nuget.config matching the promoted package set.
- name: aspireCliChannelOverride
displayName: 'Aspire CLI channel override (auto = derive from branch; set to stable for the GA ship build)'
type: string
default: 'auto'
values:
- auto
- stable
- staging
- daily

trigger:
batch: true
Expand Down Expand Up @@ -158,6 +176,7 @@ extends:
- osx-x64
codeSign: true
teamName: $(_TeamName)
aspireCliChannelOverride: ${{ parameters.aspireCliChannelOverride }}
extraBuildArgs: >-
/p:Configuration=$(_BuildConfig)
$(_SignArgs)
Expand All @@ -173,6 +192,7 @@ extends:
# no need to sign ELF binaries on linux
codeSign: false
teamName: $(_TeamName)
aspireCliChannelOverride: ${{ parameters.aspireCliChannelOverride }}
extraBuildArgs: >-
/p:Configuration=$(_BuildConfig)
$(_OfficialBuildIdArgs)
Expand All @@ -185,6 +205,7 @@ extends:
- win-arm64
codeSign: true
teamName: $(_TeamName)
aspireCliChannelOverride: ${{ parameters.aspireCliChannelOverride }}
extraBuildArgs: >-
/p:Configuration=$(_BuildConfig)
$(_SignArgs)
Expand Down
47 changes: 44 additions & 3 deletions eng/pipelines/templates/build_sign_native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ parameters:
extraBuildArgs: ''
codeSign: false
teamName: ''
# Optional override for AspireCliChannel. Accepted values: 'auto' (default;
# let computeCliChannel below pick stable/staging/daily/pr-<N> from Build.Reason
# and Build.SourceBranch), 'stable', 'staging', 'daily'. Set this to 'stable'
# when running the official ship pipeline so the GA CLI binary is baked with
# AspireCliChannel=stable (and aspire init then writes a nuget.org-only
# nuget.config, which is correct because the packages have been promoted to
# nuget.org). For routine stabilizing builds from a release/* branch — which
# also set DotNetFinalVersionKind=release — leave this on 'auto' so the
# channel falls through to 'staging' and aspire init writes a nuget.config
# that maps Aspire.* to the staging feed. See
# https://github.com/microsoft/aspire/issues/17527 for the bug that made this
# override necessary.
aspireCliChannelOverride: 'auto'

jobs:

Expand Down Expand Up @@ -84,15 +97,32 @@ jobs:
$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 ($reason -eq 'PullRequest') {
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
Expand All @@ -105,10 +135,21 @@ jobs:
# 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 ($versionKind -eq 'release') {
$channel = 'stable'
} 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'
Expand Down
7 changes: 3 additions & 4 deletions eng/pipelines/templates/install-corepack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ steps:
targetType: 'inline'
script: |
# Pinned Corepack shim version, sourced from
# extension/scripts/corepack-version.txt so this script,
# extension/build.sh, extension/build.ps1, the GitHub Actions
# workflow, and every AzDO pipeline that references this template
# stay in sync. The Yarn release itself is pinned in
# extension/scripts/corepack-version.txt so this template, the GitHub
# Actions workflow, and every AzDO pipeline that references this
# template stay in sync. The Yarn release itself is pinned in
# extension/package.json via the `packageManager` field.
$CorepackVersion = (Get-Content -Raw -Path '$(Build.SourcesDirectory)/extension/scripts/corepack-version.txt').Trim()
$corepackHome = Join-Path '$(Agent.TempDirectory)' 'corepack'
Expand Down
Loading
Loading