Skip to content

Fix 13.4 staging CLI dropping nuget.config without Aspire package source mapping - #17528

Merged
joperezr merged 5 commits into
release/13.4from
mitchdenny/fix-init-nuget-staging-feed
May 27, 2026
Merged

Fix 13.4 staging CLI dropping nuget.config without Aspire package source mapping#17528
joperezr merged 5 commits into
release/13.4from
mitchdenny/fix-init-nuget-staging-feed

Conversation

@mitchdenny

@mitchdenny mitchdenny commented May 27, 2026

Copy link
Copy Markdown
Member

Fixes #17527.

The 13.4 staging CLI ships claiming channel: stable, which makes aspire init drop a nuget.config that points only at nuget.org — so aspire add yarp resolves the prior stable (13.3.5), or fails to find the stabilizing Aspire.AppHost.Sdk@13.4.0+0f5144527….

Three coordinated fixes

1. eng/pipelines/templates/build_sign_native.yml — reorder the channel-detect cascade

The auto-detect previously checked versionKind -eq 'release' before the release-branch regex. StabilizePackageVersion=true (every release-branch stabilizing build) sets DotNetFinalVersionKind=release, so the staging build was baked as AspireCliChannel=stable. Reordering so the release-branch regex runs first makes stabilizing release-branch builds resolve to staging correctly.

2. eng/pipelines/azure-pipelines.ymlaspireCliChannelOverride runtime parameter

Reordering (1) means release-branch builds no longer auto-resolve to stable. The actual GA ship build still needs to bake stable, so a runtime parameter (auto | stable | staging | daily, default auto) lets the operator pick the channel explicitly at queue time. auto falls through to the auto-detect cascade.

3. src/Aspire.Cli/Packaging/PackagingService.cs — version-shape-driven default quality

Even with fixes (1) + (2), the staging-channel synthesis was routing Aspire.* to the shared dnceng/dotnet9 daily feed instead of the SHA-derived darc-pub-microsoft-aspire-<hash> feed where stabilizing packages actually live. Root cause: stagingIdentityChannel defaulted quality to Both, making useSharedFeed=true. dotnet9 only carries 13.4.0-preview.1.*, not stable-shaped 13.4.0.

Fix: when the CLI's identity is staging, derive the default quality from the CLI build's version shape:

  • Stable-shaped (no semver prerelease tag) → Stable → SHA-derived darc feed (works for stabilizing builds).
  • Prerelease-shapedBoth → shared dotnet9 (historical behavior; SHA feeds only exist for stable builds).

The identity branch runs before the requested/configured branches in the if/else because init calls GetChannelsAsync(requestedChannelName: "staging") when identity is staging — short-circuiting there would re-introduce the bug. The version-shape predicate is injected via constructor for deterministic unit-test coverage.

Validation

End-to-end validated with a locally-built NAOT aspire binary:

./dotnet.sh publish src/Aspire.Cli/Aspire.Cli.csproj -c Release -r osx-arm64 /p:AspireCliChannel=staging
aspire config set -g overrideStagingFeed https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-0f514452/nuget/v3/index.json
aspire init --language csharp --non-interactive
aspire add yarp --non-interactive    # → Aspire.Hosting.Yarp::13.4.0 ✅

Resolved staging channel: feed=…darc-pub-microsoft-aspire-0f514452…, quality=Stable, pinnedVersion=(none).

PackagingServiceTests (50/50) pass.

Microsoft Reviewers: Open in CodeFlow

Copilot AI review requested due to automatic review settings May 27, 2026 06:12
@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17528

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17528"

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes channel computation in the AzDO native-sign build template so release-branch (“release/” and “internal/release/”) builds bake AspireCliChannel=staging even when StabilizePackageVersion=true causes DotNetFinalVersionKind=release. This aligns the baked CLI identity channel with how release-branch artifacts are published (staging feed first), preventing aspire init from generating a nuget.config that omits the staging-feed mapping for Aspire.*.

Changes:

  • Reorders the channel-selection conditionals so the release-branch regex check runs before the versionKind == 'release' check.
  • Adds an explanatory comment (with issue link) documenting why the ordering matters for stabilized release-branch builds.
Show a summary per file
File Description
eng/pipelines/templates/build_sign_native.yml Fixes AspireCliChannel computation ordering so release-branch builds correctly bake staging instead of stable.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@mitchdenny
mitchdenny changed the base branch from main to release/13.4 May 27, 2026 06:16
…bilizing

The channel-compute step in build_sign_native.yml was checking
$versionKind -eq 'release' BEFORE the release-branch regex check. A
13.4 staging build runs from a release/* branch with
StabilizePackageVersion=true, which sets DotNetFinalVersionKind=release,
so the wrong arm fired and baked AspireCliChannel=stable into the
binary.

Downstream, aspire init reads CliExecutionContext.IdentityChannel to
pick the channel mappings it writes into the workspace nuget.config.
With identity=stable there's no Aspire.* → staging-feed mapping, so
aspire add tries to resolve packages from nuget.org and either gets
13.3.5 or fails outright (the apphost.cs template pins
#:sdk Aspire.AppHost.Sdk@13.4.0+<sha>, which isn't on nuget.org).

Swap the conditions so the release-branch check runs first. Release-
branch builds are always staging artifacts; only release-shaped
non-release-branch builds (effectively none in practice) get stable.

Fixes #17527

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny
mitchdenny force-pushed the mitchdenny/fix-init-nuget-staging-feed branch from 30ca039 to b869bf3 Compare May 27, 2026 06:16
With release-branch builds now defaulting to 'staging' (so stabilizing
dogfood builds aren't mis-baked as 'stable'), there was no remaining
path to produce a real 'stable' GA ship binary — the same release/*
branch that produces the staging dogfood drops also produces the final
ship build, and the pipeline has no other signal to tell them apart.

Add a runtime pipeline parameter 'aspireCliChannelOverride' (auto |
stable | staging | daily, default auto) to azure-pipelines.yml and
thread it through every build_sign_native.yml invocation. When the
release manager kicks off the official GA ship build, they set this to
'stable' so the distributed binary bakes AspireCliChannel=stable and
aspire init writes the nuget.org-only nuget.config that matches the
promoted package set. Routine stabilizing builds leave it on 'auto'
and continue to bake 'staging'.

The override is validated against the same accepted-channel set that
IdentityChannelReader.IsValidChannel enforces at CLI startup so a typo
fails the pipeline step rather than producing a binary that refuses
to boot. pr-<N> is intentionally excluded from the override set since
PR builds always come from the PullRequest reason arm.

The unofficial pipeline doesn't get the parameter — its test builds
should always derive the channel from branch+reason, and the
template's default 'auto' achieves that.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@radical radical left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted 1 release correctness issue.

- name: aspireCliChannelOverride
displayName: 'Aspire CLI channel override (auto = derive from branch; set to stable for the GA ship build)'
type: string
default: 'auto'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because auto now bakes staging for every release/* build, the GA ship path depends on someone remembering to queue the source microsoft-aspire build with this override set to stable. The release docs currently only tell release managers to select a successful signed source build in release-publish-nuget, so following those instructions can publish GA aspire-cli-* assets whose binary still identifies as staging and writes staging-feed config from aspire init.

Please update the release/source-build docs to call out aspireCliChannelOverride=stable for the final GA source build (leaving auto for staging dogfood builds), and add a release-pipeline guard that verifies the selected source build's CLI archive/tool package has AspireCliChannel=stable before publishing it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 — I share the same concern. Needing to remember a queue-time parameter on the GA ship build is not where we want to be long-term; it's the kind of thing that gets missed exactly once and then we ship a binary that says staging to nuget.org users.

This is fine for 13.4 since we're days from ship and the override at least makes the choice explicit, but I've logged #17550 to decouple the channel identity from build-time baking entirely (sidecar manifest written by the acquisition script) so a validated staging artifact can be promoted to stable without a re-spin. Targeted at 13.5. Would love your input there.

The staging-channel synthesis in PackagingService defaulted to
PackageChannelQuality.Both for staging-identity CLIs. With Both,
useSharedFeed=true and Aspire.* gets routed to the shared dnceng/dotnet9
daily feed -- which only contains prerelease-tagged 13.4.0-preview.*
packages, not the stable-shaped 13.4.0 packages produced during release
stabilization (StabilizePackageVersion=true).

Net effect on the just-shipped staging build of 13.4: `aspire init`
drops a NuGet.config pointing Aspire.* at dotnet9, then `aspire add yarp`
fails to resolve Aspire.Hosting.Yarp 13.4.0 because dotnet9 doesn't
carry it. The packages actually live in the SHA-derived
darc-pub-microsoft-aspire-<hash> feed.

Fix: when the CLI's identity is staging, derive the synthesized
channel's default quality from the CLI build's version shape:
  - Stable-shaped (no semver prerelease tag) -> Stable, which makes
    useSharedFeed=false and routes Aspire.* to the SHA-derived darc
    feed where stabilizing packages actually live.
  - Prerelease-shaped -> Both (the historical default), since SHA-
    specific darc feeds are only created for stable release-branch
    builds and prerelease staging CLIs must use the shared feed.

The identity-staging branch runs before the requested/configured
branches in the if/else because `init` (and many other commands) calls
GetChannelsAsync(requestedChannelName: "staging") when the running
CLI's identity is staging -- short-circuiting on the requested branch
would re-introduce the bug.

The version-shape predicate is injected via constructor so unit tests
can deterministically exercise both paths regardless of the test-host
assembly's baked InformationalVersion.

Validated end-to-end with a locally-built NAOT `aspire` binary
(`/p:AspireCliChannel=staging`) + overrideStagingFeed pointing at
darc-pub-microsoft-aspire-0f514452: `aspire init` -> `aspire add yarp`
now resolves Aspire.Hosting.Yarp 13.4.0 instead of falling back to
13.3.5.

Refs #17527

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny changed the title Bake AspireCliChannel=staging for release-branch builds even when stabilizing Fix 13.4 staging CLI dropping nuget.config without Aspire package source mapping May 27, 2026
The stabilization-check CI job builds the test host with
StabilizePackageVersion=true, which bakes a stable-shaped (no '-')
informational version into Aspire.Cli. PackagingService's new
identity-staging branch then defaults quality to Stable and requires
a SHA suffix in the assembly's InformationalVersion to compute the
darc-pub feed URL. Stabilized test-host assemblies don't carry a
+sha suffix, so CreateStagingChannel returned null and the
UpdateCommand_WhenStagingIdentityRegistersChannel_UsesStagingForUnpinnedProject
test fell back to the default channel instead of staging.

Default CliTestHelper.PackagingServiceFactory to inject
isStableShapedCliVersion: () => false so command-level tests get
deterministic prerelease-shaped behavior (quality=Both → shared
dotnet9 feed) regardless of how the test host was built. Tests
that specifically exercise the stable-shape branch (in
PackagingServiceTests) construct PackagingService directly and
already pass an explicit predicate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

CLI E2E Tests unknown — 107 passed, 0 failed, 2 unknown (commit 850ca78)

View all recordings
Status Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View recording
AddPackageWhileAppHostRunningDetached ▶️ View recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View recording
AgentInitCommand_DefaultSelection_InstallsDefaultSkills ▶️ View recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View recording
AgentMcpListStructuredLogsReturnsLogsFromStarterApp ▶️ View recording
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_DevLocalhost ▶️ View recording
AgentMcpListStructuredLogsReturnsLogsFromStarterApp_Isolated ▶️ View recording
AllPublishMethodsBuildDockerImages ▶️ View recording
AspireAddAndStartWorkAgainstLegacyAppHostTs ▶️ View recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View recording
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost ▶️ View recording
AspireInitWithExistingAppHostDirRecreatesMissingNuGetConfigAndPreservesFiles ▶️ View recording
AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive ▶️ View recording
AspireStartUpdatesStaleTypeScriptAppHostPath ▶️ View recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View recording
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent ▶️ View recording
Banner_DisplayedOnFirstRun ▶️ View recording
Banner_DisplayedWithExplicitFlag ▶️ View recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View recording
CertificatesClean_RemovesCertificates ▶️ View recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View recording
CreateAndRunAspireStarterProject ▶️ View recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View recording
CreateAndRunEmptyAppHostProject ▶️ View recording
CreateAndRunJavaEmptyAppHostProject ▶️ View recording
CreateAndRunJsReactProject ▶️ View recording
CreateAndRunPythonReactProject ▶️ View recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View recording
CreateAndRunTypeScriptStarterProject ▶️ View recording
CreateJavaAppHostWithViteApp ▶️ View recording
CreateTypeScriptAppHostWithViteApp_AllowsGuestAppPackageManagerToDiffer ▶️ View recording
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain ▶️ View recording
DashboardRunWithAgentMcpListTracesReturnsNoTraces ▶️ View recording
DashboardRunWithAgentMcpListTracesReturnsNoTraces_DevLocalhost ▶️ View recording
DashboardRunWithOtelTracesReturnsNoTraces ▶️ View recording
DashboardRunWithOtelTracesReturnsNoTraces_DevLocalhost ▶️ View recording
DeployK8sBasicApiService ▶️ View recording
DeployK8sWithExternalHelmChart ▶️ View recording
DeployK8sWithGarnet ▶️ View recording
DeployK8sWithMongoDB ▶️ View recording
DeployK8sWithMySql ▶️ View recording
DeployK8sWithPostgres ▶️ View recording
DeployK8sWithRabbitMQ ▶️ View recording
DeployK8sWithRedis ▶️ View recording
DeployK8sWithSqlServer ▶️ View recording
DeployK8sWithValkey ▶️ View recording
DeployTypeScriptAppToKubernetes ▶️ View recording
DescribeCommandResolvesReplicaNames ▶️ View recording
DescribeCommandShowsRunningResources ▶️ View recording
DetachFormatJsonProducesValidJson ▶️ View recording
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance ▶️ View recording
DoListStepsShowsPipelineSteps ▶️ View recording
DocsCommand_RendersInteractiveMarkdownFromLocalSource ▶️ View recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View recording
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain ▶️ View recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View recording
GatewayWithoutExternalEndpoint_FailsPublishWithGuidance ▶️ View recording
GeneratedAspireDevScript_StartsWatchMode_WithConfiguredToolchain ▶️ View recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View recording
GlobalMigration_PreservesAllValueTypes ▶️ View recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View recording
IngressWithoutExternalEndpoint_FailsPublishWithGuidance ▶️ View recording
InitTypeScriptAppHost_AugmentsExistingViteRepoInWorkspaceSubdirectory ▶️ View recording
InteractiveCSharpInitCreatesExpectedFiles ▶️ View recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View recording
JavaScriptHostingApisRunFromTypeScriptAppHost ▶️ View recording
LatestCliCanStartStableChannelAppHost ▶️ View recording
LatestCliCanStartStableChannelTypeScriptAppHost ▶️ View recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View recording
LogsCommandShowsResourceLogs ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterApp ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterAppIsolated ▶️ View recording
PsCommandListsRunningAppHost ▶️ View recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View recording
PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifacts ▶️ View recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ View recording
ResourceCommand_FailedExecution_DisplaysAppHostLogPathAndLogContainsEntries ▶️ View recording
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput ▶️ View recording
RestoreGeneratesSdkFiles ▶️ View recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View recording
RunReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
RunReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
SecretCrudOnDotNetAppHost ▶️ View recording
SecretCrudOnTypeScriptAppHost ▶️ View recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ View recording
StartReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
StartReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
StopAllAppHostsFromAppHostDirectory ▶️ View recording
StopJavaPolyglotAppHostUsingApphostDirectory ▶️ View recording
StopNonInteractiveSingleAppHost ▶️ View recording
StopTypeScriptPolyglotAppHostUsingApphostDirectory ▶️ View recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View recording
UnAwaitedChainsCompileWithAutoResolvePromises ▶️ View recording
UpdateProjectChannelToStable_CSharpEmptyAppHost_PreservesAspireConfigChannel ▶️ View recording
UpdateProjectChannelToStable_CSharpSingleFileInit_PreservesAspireConfigChannel ▶️ View recording
UpdateProjectChannelToStable_TypeScriptSingleFileInit_PreservesAspireConfigChannel ▶️ View recording
UpdateProjectChannelToStable_TypeScript_PreviewsStablePackagesAndPreservesChannel ▶️ View recording

📹 Recordings uploaded automatically from CI run #26506998459

@radical radical left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two test-coverage gaps. The fixes themselves look correct — cascade reorder, override parameter, and version-shape predicate all verified end-to-end. These comments flag gaps in the test surface only, not bugs in the changes.

.Build();
var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger<PackagingService>.Instance, isStableShapedCliVersion: () => true);

var channels = await packagingService.GetChannelsAsync().DefaultTimeout();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR's core change is putting stagingIdentityChannel first in the if/else in PackagingService.GetChannelsAsync so the stagingChannelRequested branch doesn't short-circuit on a staging-baked CLI. This test sets identityChannel: Staging but calls GetChannelsAsync() with no requestedChannelName, so it doesn't exercise the combination that actually arises in real flows. A future refactor that reordered the if/else (stagingChannelRequested first → Both) would still pass this test, and the #17527 misroute would silently regress.

Concretely, both flags are true simultaneously in:

  • InitCommand.cs:671GetChannelsAsync(ct, identityChannel) on a staging CLI running aspire init
  • IntegrationPackageSearchService.cs:26 (used by aspire add) and DotNetBasedAppHostServerProject.cs:334, when the project has channel: staging

Suggest adding two variants that pass requestedChannelName: PackageChannelNames.Staging:

  • identity=staging + requested=staging + stable-shaped → Stable (locks in the conditional ordering)
  • identity=staging + requested=staging + prerelease-shaped → Both (inverse; asserts the predicate is consulted on the identity path)

A _AndConfigurationChannelIsAlsoStaging variant (configuration["channel"] = "staging") would similarly lock the stagingChannelConfigured arm.

$channel = "pr-$prNumber"
} elseif ($versionKind -eq 'release') {
$channel = 'stable'
} elseif ($sourceBranch -match '^refs/heads/(release|internal/release)/') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cascade reorder is the headline pipeline fix and currently has no test coverage. The repo already has the infrastructure: tests/Infrastructure.Tests/PowerShellScripts/PowerShellCommand.cs invokes standalone .ps1 files via pwsh (used by StageNativeCliToolPackagesTests, BuildTestMatrixTests, etc.).

To make this cascade testable, extract this inline pwsh into eng/scripts/compute-cli-channel.ps1:

param(
  [Parameter(Mandatory=$true)][string]$Reason,
  [Parameter(Mandatory=$true)][string]$SourceBranch,
  [string]$PrNumber = '',
  [string]$Override = 'auto',
  [Parameter(Mandatory=$true)][string]$VersionKind
)
# ...cascade...
Write-Output $channel

The YAML step becomes a single pwsh -File call. A [Theory]-based ComputeCliChannelTests then covers cases with zero coverage today:

  • release-branch + versionKind=release → staging (the bug fix)
  • release-branch + versionKind != release → staging (early stabilization phase)
  • main + versionKind=release → stable (unchanged)
  • main + versionKind != release → daily (unchanged)
  • override=stable on a release branch → stable (the ship-build path)
  • override=auto → falls through
  • PR build with non-digit $prNumber → throw (defense-in-depth that's untested today)
  • override=garbage → throw (the inner -notin validation; only fires if a non-pipeline caller bypasses the queue-time values: constraint)

Comment thread eng/pipelines/templates/build_sign_native.yml Outdated
@joperezr

Copy link
Copy Markdown
Member

I'm going to merge this in to unblock dogfooding of staging. ANy other feedback can be addressed as a follow up.

@joperezr
joperezr merged commit 6a82355 into release/13.4 May 27, 2026
210 checks passed
@joperezr
joperezr deleted the mitchdenny/fix-init-nuget-staging-feed branch May 27, 2026 18:19
@microsoft-github-policy-service microsoft-github-policy-service Bot added this to the 13.4 milestone May 27, 2026
@IEvangelist

Copy link
Copy Markdown
Member

PR Testing Report

PR Information

CLI Version Verification

  • Expected Commit: cae475a
  • Installed Version: 13.4.0-pr.17528.gcae475a0
  • Status: [PASS] Verified

Changes Analyzed

Files Changed

  • eng/pipelines/azure-pipelines.yml
  • eng/pipelines/templates/build_sign_native.yml
  • src/Aspire.Cli/Packaging/PackagingService.cs
  • tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs
  • tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs

Change Categories

  • CLI changes detected
  • Pipeline/build changes detected
  • Hosting integration changes
  • Dashboard changes
  • Client/Component changes
  • Template changes
  • Test changes

Scope Note

The exact stable-shaped identityChannel=staging path is baked into native release/staging binaries by the build pipeline. The PR dogfood CLI self-identifies as pr-17528, so runtime testing focused on the available package-source mapping and AppHost workflows from the PR CLI and documented that build-time channel-baking limitation.

Test Scenarios Executed

Scenario 1: Dogfood PR template creation

Objective: Create a fresh Aspire app from the PR package hive and verify generated NuGet package-source mapping.
Coverage Type: Happy path
Status: [PASS] Passed

Steps:

  1. Created a fresh output directory.
  2. Ran aspire new aspire-empty with --source set to the PR hive and --version set to the installed PR CLI version.
  3. Inspected the generated NuGet config for packageSourceMapping and Aspire* mapping to the PR source.

Evidence:

  • C:\Users\dapine.copilot\session-state\6e7d7a61-a273-4681-b0ec-e40937c13811\files\pr-17528-testing\scenarios\scenario-1-empty-template\aspire-new-empty.log
  • C:\Users\dapine.copilot\session-state\6e7d7a61-a273-4681-b0ec-e40937c13811\files\pr-17528-testing\scenarios\scenario-1-empty-template\nuget-config-check.txt

Observations:
Generated project completed successfully and NuGet config includes Aspire package-source mapping for the PR source.


Scenario 2: AppHost startup smoke test

Objective: Verify a freshly generated starter AppHost can start under the PR CLI and report resources.
Coverage Type: Happy path
Status: [PASS] Passed

Steps:

  1. Created a fresh aspire-starter app with Redis disabled and all prompts suppressed.
  2. Started the generated AppHost with an explicit --apphost path.
  3. Waited for webfrontend and apiservice to reach up status.
  4. Captured describe output and stopped the AppHost.

Evidence:

  • C:\Users\dapine.copilot\session-state\6e7d7a61-a273-4681-b0ec-e40937c13811\files\pr-17528-testing\scenarios\scenario-2-starter-startup\aspire-new-starter.log
  • C:\Users\dapine.copilot\session-state\6e7d7a61-a273-4681-b0ec-e40937c13811\files\pr-17528-testing\scenarios\scenario-2-starter-startup\aspire-start.log
  • C:\Users\dapine.copilot\session-state\6e7d7a61-a273-4681-b0ec-e40937c13811\files\pr-17528-testing\scenarios\scenario-2-starter-startup\aspire-wait-webfrontend.log
  • C:\Users\dapine.copilot\session-state\6e7d7a61-a273-4681-b0ec-e40937c13811\files\pr-17528-testing\scenarios\scenario-2-starter-startup\aspire-wait-apiservice.log
  • C:\Users\dapine.copilot\session-state\6e7d7a61-a273-4681-b0ec-e40937c13811\files\pr-17528-testing\scenarios\scenario-2-starter-startup\aspire-describe.log
  • C:\Users\dapine.copilot\session-state\6e7d7a61-a273-4681-b0ec-e40937c13811\files\pr-17528-testing\scenarios\scenario-2-starter-startup\aspire-stop.log

Observations:
Starter app was created, started, both generated resources reached up status, and describe listed webfrontend/apiservice.


Scenario 3: Existing output directory boundary

Objective: Verify aspire new does not overwrite an existing generated project directory.
Coverage Type: Boundary / unhappy path
Status: [PASS] Passed

Steps:

  1. Re-ran aspire new aspire-empty against the Scenario 1 output directory.
  2. Captured the non-zero command result and validation message.

Evidence:

  • C:\Users\dapine.copilot\session-state\6e7d7a61-a273-4681-b0ec-e40937c13811\files\pr-17528-testing\scenarios\scenario-1-empty-template\aspire-new-existing-output.log

Observations:
Command failed safely with a non-zero exit code and an existing-directory validation message.

Expected Unhappy-Path Outcome: Non-zero exit code with a clear validation error; existing files remain untouched.


Scenario 4: Invalid AppHost path boundary

Objective: Verify follow-up AppHost commands fail safely for a missing --apphost path.
Coverage Type: Boundary / unhappy path
Status: [PASS] Passed

Steps:

  1. Ran aspire describe with an explicit path to a non-existent AppHost project.
  2. Captured the non-zero command result and error text.

Evidence:

  • C:\Users\dapine.copilot\session-state\6e7d7a61-a273-4681-b0ec-e40937c13811\files\pr-17528-testing\scenarios\scenario-4-invalid-apphost\aspire-describe-invalid-apphost.log

Observations:
Command failed safely with a non-zero exit code and a missing-path error.

Expected Unhappy-Path Outcome: Non-zero exit code with a clear missing AppHost path error.


Summary

Scenario Status Notes
Dogfood PR template creation [PASS] Passed Generated project completed successfully and NuGet config includes Aspire package-source mapping for the PR source.
AppHost startup smoke test [PASS] Passed Starter app was created, started, both generated resources reached up status, and describe listed webfrontend/apiservice.
Existing output directory boundary [PASS] Passed Command failed safely with a non-zero exit code and an existing-directory validation message.
Invalid AppHost path boundary [PASS] Passed Command failed safely with a non-zero exit code and a missing-path error.

Overall Result

[PASS] PR VERIFIED

Recommendations

  • No runtime issues found in the dogfood CLI scenarios. The build-time stable-shaped staging-channel path remains covered by the PR unit test because it cannot be directly produced from PR dogfood artifacts.

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

✅ No documentation update needed.

docs_required → already documented by name

Triggered signals (1): pr_body_has_cli_flag_mention

Evidence: The PR body's validation section contains aspire init --language csharp --non-interactive and aspire add yarp --non-interactive. These flags are pre-existing and already documented:

  • --language: documented in src/frontend/src/content/docs/reference/cli/commands/aspire-init.mdx as "--language — The AppHost language to scaffold (csharp, typescript)."
  • --non-interactive: documented in src/frontend/src/content/docs/reference/cli/commands/aspire-new.mdx as "The --non-interactive flag is not fully supported."

The PR is a bug fix that corrects internal staging channel routing in PackagingService.cs and reorders the AspireCliChannel detection cascade in CI pipeline templates (eng/pipelines/templates/build_sign_native.yml, eng/pipelines/azure-pipelines.yml). No new user-facing CLI flags, commands, options, or public API surface was introduced. The aspire init and aspire add behaviors mentioned in the PR body are existing behaviors that the fix restores to work correctly in staging builds.

mitchdenny added a commit that referenced this pull request May 28, 2026
…rce mapping (#17528)

* Bake AspireCliChannel=staging for release-branch builds even when stabilizing

The channel-compute step in build_sign_native.yml was checking
$versionKind -eq 'release' BEFORE the release-branch regex check. A
13.4 staging build runs from a release/* branch with
StabilizePackageVersion=true, which sets DotNetFinalVersionKind=release,
so the wrong arm fired and baked AspireCliChannel=stable into the
binary.

Downstream, aspire init reads CliExecutionContext.IdentityChannel to
pick the channel mappings it writes into the workspace nuget.config.
With identity=stable there's no Aspire.* → staging-feed mapping, so
aspire add tries to resolve packages from nuget.org and either gets
13.3.5 or fails outright (the apphost.cs template pins
#:sdk Aspire.AppHost.Sdk@13.4.0+<sha>, which isn't on nuget.org).

Swap the conditions so the release-branch check runs first. Release-
branch builds are always staging artifacts; only release-shaped
non-release-branch builds (effectively none in practice) get stable.

Fixes #17527

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add aspireCliChannelOverride pipeline parameter for GA ship builds

With release-branch builds now defaulting to 'staging' (so stabilizing
dogfood builds aren't mis-baked as 'stable'), there was no remaining
path to produce a real 'stable' GA ship binary — the same release/*
branch that produces the staging dogfood drops also produces the final
ship build, and the pipeline has no other signal to tell them apart.

Add a runtime pipeline parameter 'aspireCliChannelOverride' (auto |
stable | staging | daily, default auto) to azure-pipelines.yml and
thread it through every build_sign_native.yml invocation. When the
release manager kicks off the official GA ship build, they set this to
'stable' so the distributed binary bakes AspireCliChannel=stable and
aspire init writes the nuget.org-only nuget.config that matches the
promoted package set. Routine stabilizing builds leave it on 'auto'
and continue to bake 'staging'.

The override is validated against the same accepted-channel set that
IdentityChannelReader.IsValidChannel enforces at CLI startup so a typo
fails the pipeline step rather than producing a binary that refuses
to boot. pr-<N> is intentionally excluded from the override set since
PR builds always come from the PullRequest reason arm.

The unofficial pipeline doesn't get the parameter — its test builds
should always derive the channel from branch+reason, and the
template's default 'auto' achieves that.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Route stabilizing staging CLI to SHA-derived darc feed

The staging-channel synthesis in PackagingService defaulted to
PackageChannelQuality.Both for staging-identity CLIs. With Both,
useSharedFeed=true and Aspire.* gets routed to the shared dnceng/dotnet9
daily feed -- which only contains prerelease-tagged 13.4.0-preview.*
packages, not the stable-shaped 13.4.0 packages produced during release
stabilization (StabilizePackageVersion=true).

Net effect on the just-shipped staging build of 13.4: `aspire init`
drops a NuGet.config pointing Aspire.* at dotnet9, then `aspire add yarp`
fails to resolve Aspire.Hosting.Yarp 13.4.0 because dotnet9 doesn't
carry it. The packages actually live in the SHA-derived
darc-pub-microsoft-aspire-<hash> feed.

Fix: when the CLI's identity is staging, derive the synthesized
channel's default quality from the CLI build's version shape:
  - Stable-shaped (no semver prerelease tag) -> Stable, which makes
    useSharedFeed=false and routes Aspire.* to the SHA-derived darc
    feed where stabilizing packages actually live.
  - Prerelease-shaped -> Both (the historical default), since SHA-
    specific darc feeds are only created for stable release-branch
    builds and prerelease staging CLIs must use the shared feed.

The identity-staging branch runs before the requested/configured
branches in the if/else because `init` (and many other commands) calls
GetChannelsAsync(requestedChannelName: "staging") when the running
CLI's identity is staging -- short-circuiting on the requested branch
would re-introduce the bug.

The version-shape predicate is injected via constructor so unit tests
can deterministically exercise both paths regardless of the test-host
assembly's baked InformationalVersion.

Validated end-to-end with a locally-built NAOT `aspire` binary
(`/p:AspireCliChannel=staging`) + overrideStagingFeed pointing at
darc-pub-microsoft-aspire-0f514452: `aspire init` -> `aspire add yarp`
now resolves Aspire.Hosting.Yarp 13.4.0 instead of falling back to
13.3.5.

Refs #17527

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Pin PackagingService stable-shape predicate to false in CliTestHelper

The stabilization-check CI job builds the test host with
StabilizePackageVersion=true, which bakes a stable-shaped (no '-')
informational version into Aspire.Cli. PackagingService's new
identity-staging branch then defaults quality to Stable and requires
a SHA suffix in the assembly's InformationalVersion to compute the
darc-pub feed URL. Stabilized test-host assemblies don't carry a
+sha suffix, so CreateStagingChannel returned null and the
UpdateCommand_WhenStagingIdentityRegistersChannel_UsesStagingForUnpinnedProject
test fell back to the default channel instead of staging.

Default CliTestHelper.PackagingServiceFactory to inject
isStableShapedCliVersion: () => false so command-level tests get
deterministic prerelease-shaped behavior (quality=Both → shared
dotnet9 feed) regardless of how the test host was built. Tests
that specifically exercise the stable-shape branch (in
PackagingServiceTests) construct PackagingService directly and
already pass an explicit predicate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update eng/pipelines/templates/build_sign_native.yml

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jose Perez Rodriguez <joperezr@microsoft.com>
mitchdenny added a commit that referenced this pull request May 29, 2026
…7646)

The signed-build verifier ran 'aspire new aspire-starter' after extracting
the CLI archive, claiming to exercise 'template engine + bundle
self-extraction without requiring a NuGet restore'. That claim was wrong:
'aspire new' resolves template versions via
TemplateNuGetConfigService.ResolveTemplatePackageAsync, which always
queries a NuGet feed.

The step only ever succeeded on release branches because builds were
mis-baked with AspireCliChannel=stable, which routed the implicit
identity-channel lookup to nuget.org and found a previously-shipped
Aspire.ProjectTemplates version. Once #17528 corrected the release-branch
builds to bake AspireCliChannel=staging, the identity channel switched to
a staging feed that is not reachable from the 1ES signed-build agent, and
the step started failing with 'No template versions were found' in the
internal build pipeline.

This is the same egress reason the TypeScript starter check was already
removed for (#17274, tracked in #17345). Re-adding meaningful starter
coverage in the signed-build verifier requires either an internal NuGet
mirror or a no-network template path.

Drop the step (and the corresponding test assertion); the verifier still
covers archive layout, sidecar contract, and 'aspire --version'
execution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Staging build of CLI bakes channel as stable, causing aspire init to drop nuget.config without staging feed (resolves wrong Aspire package versions)

6 participants