CLI: bake channel into assembly metadata; drop global-channel writes#16820
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16820Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16820" |
d043a14 to
ed36ba2
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
|
b7d7417 to
3837f90
Compare
0e44dae to
178f5ec
Compare
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
|
Add an AspireCliChannel MSBuild property whose value (stable | staging | daily |
pr | local) is emitted as `[AssemblyMetadata("AspireCliChannel", ...)]` on the
CLI assembly. Locally-built CLIs default to "local" so they don't impersonate
the real "daily" channel — this matters for downstream PSM guards keyed on
identity == channel.
CI overrides the default via /p:AspireCliChannel=$(aspireCliChannel):
- eng/clipack/Common.projitems propagates the value through the RID-specific
pack pipeline.
- eng/pipelines/templates/build_sign_native.yml threads it through the AzDO
internal native-build template.
- .github/workflows/build-cli-native-archives.yml does the same for GitHub
Actions native archive builds.
PackageChannelNames.Local ("local") is added as a constant so the rest of the
CLI doesn't have to deal in raw strings.
Tests:
- AssemblyMetadataChannelTests asserts the running CLI assembly carries the
AssemblyMetadata attribute and that its value is one of the known channels.
- CliMetadataPackagingTests verifies the metadata survives `dotnet pack`.
- ClipackPropagationTests pins that clipack RID-specific packs honor the
AspireCliChannel override.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wire the bootstrap so the running CLI's channel is sourced from the binary's
own [AssemblyMetadata("AspireCliChannel", ...)] value rather than from any
ambient configuration. This is the runtime half of the channel-baking story:
the build pins the channel into the assembly, and the CLI reads its own
identity at startup.
- Acquisition/IdentityChannelReader.cs is the new component. It reads the
AspireCliChannel metadata key from a caller-supplied Assembly. The ctor
requires an explicit Assembly (no Assembly? = null default) so misuse is
caught at construction time rather than via a cryptic
"metadata missing on '?'" later. Production callers pass
typeof(Aspire.Cli.Program).Assembly; tests pass a fake AssemblyBuilder
assembly.
- Program.BuildApplicationAsync registers IIdentityChannelReader in DI and
threads its value into the new CliExecutionContext.Channel /
.IdentityChannel / .PrNumber surface.
- CliExecutionContext exposes:
* Channel — the resolved hive label. For non-PR builds, identical to
the identity channel verbatim. For PR builds (identity == "pr" with
non-null PrNumber), resolves to "pr-<N>" — the directory name the
packaging service uses, and the value reseed call sites write into a
project's aspire.config.json#channel.
* IdentityChannel — the raw build-time identity ("local"|"stable"|
"staging"|"daily"|"pr"). Distinct from Channel because consumers that
need the build-time taxonomy (PSM guards, version checks) should not
see the per-PR refinement.
* PrNumber — exposed verbatim. The constructor refuses
channel="pr"+prNumber=null so a malformed PR build cannot construct.
Tests:
- IdentityChannelReaderTests covers the metadata-key contract and the real
GitHub Actions InformationalVersion shapes (-pr.<N>.g<SHA>+<sha>) the
parser must accept.
- CliBootstrapTests asserts the production wiring resolves a known channel
value when invoked against the real Aspire.Cli assembly.
- CliExecutionContextTests exercises the Channel/IdentityChannel/PrNumber
surface, including the malformed-PR-build constructor guard.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The original name suggested the count was specific to PR build hives, but the implementation has always returned the count of every subdirectory in the hives root — including the local hive and any other channel-specific hives a developer may have. The new name reflects what the method actually does. This is a mechanical rename of the method on CliExecutionContext plus its five call sites (AddCommand, IntegrationPackageSearchService, NewCommand, UpdateCommand, TemplateNuGetConfigService). Comments and the doc string are updated to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ries
The app-host project plumbing was reading the channel through
IConfigurationService.GetConfigurationAsync("channel", ...) — a global lookup
that returned whatever happened to be in the user's ambient aspire config.
After the IdentityChannelReader work, the channel is available verbatim from
CliExecutionContext.Channel, so the global read is redundant and removes a
silent source of channel drift between sibling processes.
Changes:
- AppHostServerProjectFactory: drop the IConfigurationService dependency.
The factory now takes CliExecutionContext directly and forwards it.
- DotNetBasedAppHostServerProject: drop the unused
_configurationService field and its constructor parameter.
- GuestAppHostProject: add the CliExecutionContext dependency so the
generated guest-app-host can pass the channel through into its own
DI graph (channel-aware paths that previously asked the global config
now ask the execution context).
Tests:
- AppHostServerProjectTests: drop the configurationService argument from
the test factory; nothing else changes.
- GuestAppHostProjectTests: pass a test CliExecutionContext through the
constructor for parity with the production wiring.
- CliTestHelper: factory now wires CliExecutionContext into the new
constructor signatures.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t aspire.config.json
When a prebuilt app-host runs against an existing project, the channel it
should use is the one the project was scaffolded against — stored in the
project's aspire.config.json#channel. The previous implementation consulted
global identity-channel state, which could disagree with the per-project
value when a developer ran multiple CLIs against the same project.
Changes:
- PrebuiltAppHostServer.ResolveChannelName: read only the per-project
aspire.config.json. Return null when the file is absent or sets no
channel; let downstream consumers decide what to do with that.
- Add a PSM (per-source mapping) guard: when the running CLI's identity
channel is "local" and the requested channel is also "local", suppress
the temporary NuGet config generation so the project picks up the user's
ambient NuGet sources instead of being pinned to the local hive's PSM.
For every other identity channel (stable, staging, daily, pr) the guard
does not fire and a per-channel NuGet config is generated as before.
Tests:
- PrebuiltAppHostServerChannelResolutionTests pins the per-project
aspire.config.json#channel contract: null when absent, value when
present.
- PrebuiltAppHostServerTests adds the PSM-guard cross-product (identity
channel × requested channel) so a regression that broadens the guard
is caught.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ck; "local" override → implicit channel
TemplateNuGetConfigService had three channel-resolving entry points
(PromptToCreateOrUpdateNuGetConfigAsync, CreateOrUpdateNuGetConfigWithoutPromptAsync,
ResolveTemplatePackageAsync) and each one — when the caller did not supply an
explicit channel — fell back to reading ~/.aspire/aspire.config.json#channel
through IConfigurationService. That made template resolution silently depend
on whatever channel happened to be active for OTHER projects on the machine.
The fix is to drop the IConfigurationService dependency entirely: the only
channel inputs are now (1) the caller-supplied argument or (2) the implicit
channel. The three entry points short-circuit on null/whitespace input
instead of consulting a global source.
Side effect — local-channel override:
A locally-built CLI bakes channel="local" into its assembly metadata. On a
clean machine without ~/.aspire/hives/local, PackagingService produces no
"local" channel, and InitCommand forwards CliExecutionContext.Channel
("local") as the explicit ChannelOverride. Without resolver-level support
this throws ChannelNotFoundException and `aspire init` is unusable on a
clean machine. New behavior: a request for "local" with no matching channel
resolves to the implicit channel — semantically a CLI with no local hive
is just a CLI using the ambient NuGet configuration. The fallback is
narrowly scoped to "local"; any other unrecognized channel name still fails
loudly (typos like "stalbe" still surface as ChannelNotFoundException).
Tests:
- TemplateNuGetConfigServiceTests covers null/whitespace short-circuits
for the two prompt-style entry points, the local→implicit fallback,
and the negative case that other unrecognized channels still throw.
- DotNetTemplateFactoryTests drops the now-removed IConfigurationService
argument from CreateTemplateFactory.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
….json
When `aspire init` or `aspire new` scaffolds a new project, write the
CLI's resolved CliExecutionContext.Channel value into the project's
aspire.config.json#channel field. This pins the per-project channel to
the CLI that scaffolded it, so subsequent runs against the project
resolve packages from the same hive — independent of any global channel
setting that may have drifted.
Changes:
- ScaffoldingService gains channel-reseed entry points used by all
starter-template factories.
- CliTemplateFactory.{Go,Python,TypeScript}StarterTemplate forward
CliExecutionContext.Channel into the reseed step after files are
copied.
- InitCommand and NewCommand pass CliExecutionContext.Channel into
the scaffolding pipeline; they no longer derive the channel from
global config.
Tests:
- ChannelReseedTests pins the per-channel reseed contract: each
identity channel (local | stable | staging | daily | pr-N) ends up
written verbatim into aspire.config.json#channel, including the
pr-N resolution for PR-channel CLIs.
- InitCommandTests adds the reseed coverage and updates the existing
init flow expectations to match the new wiring (drops the now-
removed FakeConfigurationServiceWithChannel helper and the two
tests that exercised the deleted global-channel-fallback path).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… recognizes "local"
Two related additions that complete the local-channel runtime story.
PackageChannel — flat-folder enumeration:
For local hive channels the Aspire* source is a flat folder of .nupkg
files (the layout created by ./build.sh --pack). `dotnet package search`
does not support local folder sources and returns no results, so
GetIntegrationPackagesAsync would yield an empty set for the local hive
even when packages are present. New behavior: when PinnedVersion != null
and the Aspire-filtered mapping points at an existing local directory,
enumerate *.nupkg files directly and project them to NuGetPackage
identities. The HTTP/search path is unchanged for non-local channels.
VersionHelper.IsLocalBuildChannel — accept "local":
The helper previously recognized only `pr-*` (PR hives) and `run-*`
(workflow-run hives) as locally-built channels. With the baked
AspireCliChannel value of "local" for ./build.sh builds, the helper now
also returns true for the literal "local" channel, so version-check
paths classify local-dev builds correctly alongside PR builds.
Tests:
- PackagingServiceTests adds direct coverage for the flat-folder
enumeration path, including the corrupted-state shape (hive dir
exists but `packages/` is empty) where PinnedVersion is null and
enumeration must safely yield nothing.
- VersionHelperTests pins the new "local" → true case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…l-dir installs
The PR-install scripts (get-aspire-cli-pr.{sh,ps1}) installed CLI artifacts
into a hive label that did not match what the running CLI's
CliExecutionContext.Channel resolved to. The mismatch surfaced when the
script ran against a local `--local-dir` source: artifacts landed under
~/.aspire/hives/<wrong-label>/, but the CLI looked under
~/.aspire/hives/pr-<N>/ (PR builds) or ~/.aspire/hives/local/ (local
dev builds), so the freshly-installed packages were invisible.
Resolution rule:
- If the script's source nupkgs carry a PR-suffixed version
(matching `pr.<N>.g<sha>`), the hive label is `pr-<N>`.
- Otherwise the hive label is `local`.
- Default falls back to `local` so `--local-dir` installs from local-dev
builds (no PR suffix) work end-to-end without manual labeling.
The release-side scripts (get-aspire-cli.{sh,ps1}) had divergent hive-
labeling logic that the PR-script fix obsoletes; the release scripts now
share the same three-branch resolution, removing ~340 lines of duplicated
branch-handling.
The polyglot-validation setup-local-cli.sh helper learns the same rule so
CI's local-CLI smoke tests install into the same hive label the CLI will
read from.
Tests:
- PRScriptShellTests / PRScriptPowerShellTests cover the three-branch
resolution (pr-N from suffix, local fallback, explicit override).
- ReleaseScriptShellTests / ReleaseScriptPowerShellTests mirror the
pinning on the release-script side after the dedup.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Self-update previously saved the chosen channel (or cleared it for "stable")
into ~/.aspire/aspire.config.json#channel so subsequent `aspire new` and
`aspire init` runs would pick it up via global config. With the per-project
channel reseed landed earlier in this branch, the global setting is no
longer a source of truth for anything — every scaffolded project carries
its own channel — so the global write is dead code and a source of drift
between projects on the same machine.
Changes:
- UpdateCommand.ExecuteSelfUpdateAsync no longer calls
SetConfigurationAsync / DeleteConfigurationAsync for "channel".
- The accompanying comment is rewritten to describe the current contract:
the channel choice applies to this self-update only; subsequent
scaffolding resolves channel per-project.
- The legacy → new config migration drops the channel key during
migration (the migrated global config never carries channel), matching
the new "channel is not stored globally" invariant. The legacy file
is still preserved verbatim for backward compatibility with older CLIs.
Tests:
- UpdateCommandTests asserts no global channel writes occur during
self-update across all selectable channels.
- ConfigMigrationTests (E2E) updates the migration assertion to expect
the channel key absent in the migrated aspire.config.json and present
in the preserved legacy file.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pin the `aspire add` hive-resolution behavior across the new local channel
to match the existing PR-hive coverage:
- AddCommand_WithLocalHive_PrefersCurrentCliVersion: with a populated
~/.aspire/hives/local/packages dir whose Aspire.Hosting pkg matches
the running CLI's version, `aspire add` should pick that version
without prompting (the local-hive equivalent of the existing
WithPrHive coverage).
This complements the PackageChannel flat-folder enumeration and the
ChannelReseed coverage added earlier on this branch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e+join
Previously the CLI carried two pieces of channel state:
1. [AssemblyMetadata("AspireCliChannel", "pr")] — the 5-value
taxonomy (stable | staging | daily | pr | local).
2. AssemblyInformationalVersionAttribute (0.0.0-pr.<N>.g<sha>) — the
version stamp, which was *separately* parsed at runtime by
IdentityChannelReader.ParsePrNumber to extract <N>.
CliExecutionContext then joined the two as `pr-<N>` for the hive label
exposed via .Channel. The split existed only inside the CLI itself —
every non-`pr` channel passed through verbatim, no external consumer
relied on the `pr` literal, and the only other src/ reader of the
raw taxonomy (PrebuiltAppHostServer.cs:423) only checked `== "local"`
which is unchanged in either scheme.
Collapse the two pieces. CI now bakes the resolved hive label
directly:
* GH Actions (.github/workflows/build-cli-native-archives.yml):
pull_request -> pr-<github.event.pull_request.number>
* AzDO (eng/pipelines/templates/build_sign_native.yml):
Build.Reason == PullRequest -> pr-<System.PullRequest.PullRequestNumber>,
with a digit-only regex pre-check that throws if the agent ever
hands us an unresolved macro or non-numeric value (clearer failure
attribution than waiting for the runtime IsValidChannel check).
Runtime simplifications:
* IdentityChannelReader.ParsePrNumber: deleted (~40 lines).
* IdentityChannelReader.ResolveChannel: now validates shape via the
new IsValidChannel helper. Accepts the four fixed strings
(stable | staging | daily | local) or `pr-<digits>`. Rejects the
legacy literal `pr` (no suffix) so a CI misconfiguration cannot
silently mis-route packages.
* CliExecutionContext: `prNumber` ctor parameter dropped;
PrNumber and IdentityChannel properties dropped; Channel is now a
plain auto-property. The `channel=='pr' && prNumber is null` throw
guard goes away — the invariant is now structurally enforced by
the channel string itself.
* Program.cs: the `if (channel == "pr") { ParsePrNumber(...) }`
block collapses to one line.
PrebuiltAppHostServer.cs: the one IdentityChannel reference becomes
Channel; behavior identical because the only check is `== "local"`
and `local` is unchanged.
Tests:
* AssemblyMetadataChannelTests: smoke-tests the baked value against
IsValidChannel (was: set-membership against the legacy 5 values).
* IdentityChannelReaderTests: drops the ParsePrNumber theory entirely,
adds the IsValidChannel truth table, and adds a Throws-based set
covering invalid shapes (including the legacy `pr` literal as a
regression guard for the CI change).
* CliExecutionContextTests: rewritten as a thin getter test (the
type is now a holder, not a join).
* CliBootstrapTests: drops the IdentityChannel branch on the PrNumber
assertion.
* InitCommandTests, PrebuiltAppHostServerTests, ChannelReseedTests:
builders drop the `prNumber:` argument; callers that previously
passed `channel: "pr", prNumber: 12345` now pass
`channel: "pr-12345"`.
All 2881 Aspire.Cli.Tests pass; src/Aspire.Cli and tests build clean
on net10.0/arm64.
Out of scope: the shell-script PR-suffix detection in
setup-local-cli.sh (commit 160801c) parses nupkg filenames stamped by
VersionSuffix, not assembly metadata, so it is not affected by this
change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds direct tests for behaviors the branch already implements but that were
only covered transitively, so a regression here would surface in unrelated
suites rather than naming the cause.
- CliExecutionContextTests: 4 GetHiveCount() tests (missing dir → 0, empty
dir → 0, populated count, ignores stray files). GetHiveCount() gates the
channel picker in update, the channel sub-menu in add, and explicit-channel
inclusion in IntegrationPackageSearchService — so a bug here previously
manifested as cross-command flake.
- UpdateCommandTests: three picker-contract tests.
* WithHivesAndConfiguredChannel and WithHivesAndLocallyConfiguredChannel
close the (hive-present × configured-channel) intersection — existing
tests cover one axis at a time but not the intersection that PR-build
users actually hit.
* WithHives_PromptOffersChannelsInPackagingServiceOrder is the
prompt-content contract: every existing prompt callback either flag-sets
or .First()s the choices; nothing verified the names, order, or
'Name (SourceDetails)' label format. Reorderings or label drops would
otherwise be silently green.
- PrebuiltAppHostServerChannelResolutionTests: two migration-safety tests for
the new aspire.config.json ?? legacy AspireJsonConfiguration fallback.
Covers the legacy-only path (no new file present) and the precedence when
both files exist with different channels — guards against an accidental
?? operand swap.
- TemplateNuGetConfigServiceTests: three IncludePrHives matrix tests. The
existing three resolver tests all pass IncludePrHives: false; the true row
(the aspire new path) and the (true × no-hives-on-disk) AND-gate were
uncovered. Adds: IncludePrHivesTrue_WithHivesPresent_AllChannelsParticipate,
IncludePrHivesTrue_NoHivesOnDisk_RestrictsToImplicit, and
IncludePrHivesFalse_IgnoresHivesEvenWhenPresent (the aspire init policy).
Verified locally: 72/72 of the affected test classes pass; 56/56 unaffected
NewCommandTests still pass.
Three gaps from the audit are intentionally deferred:
- NewCommand local-build channel bias and global-config-fallback removal
(the audit's T1/G8) target ResolveCliTemplateVersionAsync, which only
fires for TemplateRuntime.Cli templates. Reaching it via aspire-starter
is impossible — it's TemplateRuntime.DotNet and takes a different
resolution path. The clean fix is to extract the channel-selection block
to an internal helper that takes (channels, executionContextChannel,
configuredChannelName) and tests it as a pure function across all rows;
that refactor is out of scope for a pure test-add commit.
- A scaffold-then-resolve end-to-end round-trip (G5) is now reachable in
principle via the new G6 read-side tests plus existing ChannelReseedTests
on the write side, so the marginal value of a joint test is small.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…reflection
Eleven targeted test simplifications across the new channel-resolution suite.
Net: 15 files touched, -284 lines, no coverage dropped (verified by
case-by-case audit: every assertion in every removed/folded test still has at
least one equivalent assertion remaining).
1. Delete CliMetadataPackagingTests.cs — its sole "metadata exists +
non-empty" assertion is a strict subset of
AssemblyMetadataChannelTests.AspireCliChannel_AssemblyMetadata_HasValidShape
(IsValidChannel already rejects null/empty).
2. CliBootstrapTests: BuildApplication_LocallyBuiltCli_… and
BuildApplication_CliExecutionContextChannel_… asserted the same thing.
Keep one; extract GetBakedEntryAssemblyChannel() so the
GetCustomAttributes<AssemblyMetadataAttribute>().Single(...) lookup isn't
open-coded.
3. UpdateCommandTests: drop SelfUpdate_StableChannel_DoesNotDeleteGlobalChannel
— the SelfUpdate_DoesNotWriteChannelToGlobalConfiguration theory's
"update --self --channel stable" row already asserts no-delete (and no-set,
so it's a strict superset).
4. AssemblyMetadataChannelTests: replace fragile
CliAssembly_BakesChannel_AsLocal_MatchingCsprojDefault (would fail under
/p:AspireCliChannel=stable CI builds) with
Csproj_DeclaresAspireCliChannelDefault_AsLocal — inspects the csproj XML
for the `<AspireCliChannel Condition="'$(AspireCliChannel)' == ''">local`
element. Guards the same property (the declared default) without coupling
to the build configuration of the test host.
5. AddCommandTests: extract RunAddRedisWithHiveScenarioAsync to share the
workspace + prompter + project locator + AddPackage-capture scaffolding
across the three hive tests (WithPrHive / WithLocalHive / WithLocalAndPrHives).
Each test now declares only the hive layout and the SearchPackagesAsync mock.
6. ChannelReseedTests: NoExplicitChannel (4 InlineData rows) + ExplicitChannel
(Fact) → one [Theory] with five rows over (contextChannel, explicitChannel,
expected). Drop the CreateExecutionContext→BuildContext one-line wrapper
and switch from Directory.CreateTempSubdirectory + try/finally to
TemporaryWorkspace (project convention).
7. IdentityChannelReaderTests: the
IsValidChannel_MatchesExpectedTruthTable theory exhaustively asserts the
shape contract; the assembly-roundtrip tests only need to verify the wiring
from ReadChannel through IsValidChannel. Reduce
ReadChannel_AssemblyHasMetadataForValidChannel_ReturnsValue (6 rows) and
ReadChannel_InvalidChannelValue_Throws (10 rows) to one representative
[Fact] each; drop ReadChannel_AssemblyHasEmptyChannelMetadata_Throws
(covered by the empty-string row in the truth table).
8. PrebuiltAppHostServer.ResolveChannelName: private → internal (the assembly
already has [InternalsVisibleTo("Aspire.Cli.Tests")]). Drop reflection
in PrebuiltAppHostServerChannelResolutionTests (4 sites) and
PrebuiltAppHostServerTests (1 site).
9. CliExecutionContextTests: Channel_DefaultsToLocal_WhenNotSpecified now
uses CreateContext(channel: null) instead of inlining the 5-line ctor
that the helper already encapsulates.
10. Move Ctor_NullAssembly_ThrowsArgumentNullException from CliBootstrapTests
to IdentityChannelReaderTests — it's about reader ctor validation, not
bootstrap-wiring DI.
11. TemplateNuGetConfigServiceTests: fold three IncludePrHives Facts
(TrueWithHives / TrueNoHives / FalseWithHives) into one [Theory] with
three rows over (includePrHives, createHiveOnDisk, expectedVersion,
expectImplicitChannel, expectedChannelName).
Verified: build clean; 180 tests across the affected classes pass locally.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…write removal The self-update process description listed a 7-step flow ending with "Saves selected channel to global settings". That step was removed across the v3 acquisition work — the CLI no longer writes the channel globally (acquisition scripts dropped save_global_settings; aspire update --self stopped persisting --channel). Identity is now baked into the binary via AspireCliChannel assembly metadata; per-project overrides live in aspire.config.json#channel. Drop step 7 and add a short note explaining the new model so docs match reality. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d precedence
Identity-channel is baked into the CLI via AspireCliChannel assembly
metadata; the acquisition scripts and aspire update --self no longer
seed a global "channel" field. The step-2 fallback in
ConfigurationService.GetConfigurationFromDirectoryAsync still reads
the global file, however, so aspire update's documented step-3
precedence ("global config 'channel'") is reachable.
That's intentional for now — users who deliberately ran
\`aspire config set -g channel <x>\` on the new CLI keep their
preference honored by aspire update — but it's worth flagging at both
the call site (UpdateCommand precedence comment) and the impl
(ConfigurationService) so a future reader doesn't assume the global
read is also dead. Add TODO markers calling out that the fallback can
be removed once telemetry confirms negligible global-channel usage.
No behavior change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The staging-channel gate in KnownFeatures.IsStagingChannelEnabled reads configuration["channel"] (env vars + command-line + per-project aspire.config.json#channel + global config), NOT CliExecutionContext.IdentityChannel. A CLI baked with AspireCliChannel=staging does NOT auto-enable staging in the packaging service unless the user has also set the configuration value explicitly (e.g. via `aspire config set channel staging` or `--channel staging`). This is by design: identity selects which hive directory the CLI ships in; feature gating goes through user-visible configuration. Audit flagged the asymmetry as undocumented; adding a <remarks> block so future maintainers do not read the gate as identity-driven. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GuestAppHostProject.RunAsync and ScaffoldingService.ScaffoldGuestLanguageAsync
each silently wrote config.Channel back to aspire.config.json after build /
scaffold prepare. Both writes reduced to one of two cases:
- The project already had a channel pinned -> buildResult.ChannelName /
prepareResult.ChannelName matched it (both come from
AspireConfigFile.Load(...)?.Channel) -> no-op file write.
- The project had no channel pinned -> buildResult.ChannelName was null,
falling through to `?? _executionContext.IdentityChannel` -> silent
identity pin on every `aspire run`.
Neither case is useful work. The C# apphost path (DotNetBasedAppHostServerProject)
never writes config.Channel and has worked fine in this regard, so the
asymmetry was branch-introduced (PR microsoft#16820's channel refactor), not
load-bearing.
After this commit:
- `aspire run` is pure-read for config.Channel (guest + dotnet symmetric).
- Scaffolding writes config.Channel exactly once, at seed time
(ScaffoldingService:75) via the existing config.Save(...) at line 82.
- `aspire update` writes config.Channel only when the user resolved an
explicit channel (already pinned by the previous commit).
Three writes total across the CLI, each tied to a concrete user-driven
action. No silent pinning anywhere.
New regression test RunAsync_DoesNotMutateConfigChannel (Theory x2:
seededChannel="stable" and null) was verified red before applying the fix
(both cases produced Actual: "pr-99999"; after fix both stay at the seeded
value).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses PR microsoft#16820 reviewer feedback about inconsistent channel-name case sensitivity. Uses StringComparison.Ordinal for CLI channel names, including the PSM local-channel guard, because these channel values are baked/configured as well-known lowercase identifiers rather than free-form case-insensitive labels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses PR microsoft#16820 reviewer feedback asking whether the pinned local-hive package scan should use StartsWith instead of Contains. The filter is intended to keep Aspire.Hosting packages, so matching the package-id prefix avoids accidental substring matches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses PR microsoft#16820 reviewer feedback to use PackageChannelNames constants for baked identity-channel validation. Keeps the fixed channel set centralized while preserving the existing pr-<N> prefix validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…o local hive extract_version_suffix_from_packages (bash) / Get-VersionSuffixFromPackages (pwsh) short-circuit in --dry-run / -WhatIf and previously returned the literal 'pr.1234.a1b2c3d4'. The new --local-dir auto-detect logic at the call site feeds that value through the regex ^pr\.([0-9]+)\.[0-9a-g]+$ to derive a hive label, so the mock unconditionally forced hive_label='pr-1234' regardless of what was actually in --local-dir, making the intended 'local' fallback unreachable in dry-run. Return the non-PR-shaped sentinel 'local' instead, which the caller's regex intentionally does not match, so the dry-run path now behaves the same as the real-world 'no PR suffix detected' path: hive_label='local'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…taChannelTests The deleted tests asserted XML shape on eng/clipack/Common.projitems (MSBuild idiom: <MSBuild Targets="Publish" Properties="…@(AdditionalProperties)" />). That couples the test to a particular MSBuild construct: a behavior-preserving refactor (e.g. forwarding through a helper target or hand-built Properties= string) would fail the test, and an XML-valid but behaviorally-wrong refactor would still pass it. The downstream contract — "the baked CLI binary has AspireCliChannel in its AssemblyMetadata" — is covered by Aspire.Cli.Tests.AssemblyMetadataChannelTests, which reads the actual baked metadata and validates it against IdentityChannelReader.IsValidChannel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…omments
In ReleaseScript{Shell,PowerShell}Tests:
1. Drop Assert.DoesNotContain("save_global_settings" / "Save-GlobalSettings", …).
Those are internal helper-function names; renaming the helper would break
the test even though the contract still holds. The artifact-level check
(File.Exists(globalConfig) == false) and the path-name check on dry-run
output (DoesNotContain "aspire.config.json") already cover the real
contract.
2. Replace "PR1 invariant" comments with statements of the invariant itself:
install scripts must not write a global aspire.config.json — the channel
is baked into the CLI binary at build time and read via
IdentityChannelReader, so a global channel field would shadow the baked
value. The string "PR1" encodes development-time PR sequencing and rots
post-merge.
43/43 ReleaseScript{Shell,PowerShell} tests pass after the change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…gn intent
CLI E2E tests intentionally run against multiple CliInstallStrategy modes
selected via CliInstallStrategy.Detect: LocalHive (local-built archive),
InstallScript (downloaded by get-aspire-cli{,-pr}.{sh,ps1}), and pre-existing
CLI on PATH. Only LocalHive returns a non-null channel from
PrepareLocalChannel; the other strategies rely on the CLI's baked channel
plus ambient NuGet feeds.
The old comment ("For LocalHive runs, point the freshly-created project at
the local channel…") read like a runtime env-sniff. Replace with a comment
that names the strategy explicitly and contrasts LocalHive against the
null-returning strategies, so readers see the if-branch as design intent
rather than runtime hedging.
Touched sites (no behavior change):
- ChannelUpdateWorkflowTests.cs (1 site, replaced existing comment)
- TypeScriptCodegenValidationTests.cs (1 site, added comment)
- TypeScriptPolyglotTests.cs (4 sites: two channelArgument ternaries and
two WriteLocalChannelSettings if-blocks, comment adapted per site)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In LocalDir_DryRun_WithGitHubRunIdEnvSet_UsesLocalHiveLabel and its PowerShell
sibling LocalDir_WhatIf_WithGitHubRunIdEnvSet_UsesLocalHiveLabel, the existing
assertion was too weak:
Assert.Contains("local", result.Output, StringComparison.OrdinalIgnoreCase);
The substring "local" appears in the fixture path (local-artifacts) and in
neighboring log lines ("from local packages"), so the assertion passes even
when the actual hive label is wrong (e.g. pr-<N> from a dry-run mock that
collides with the auto-detect regex).
Tighten to the exact phrase the script emits at install_aspire_cli /
Resolve-HiveLabel:
Assert.Contains("Using hive label: local", result.Output, StringComparison.Ordinal);
The existing Assert.DoesNotContain("run-99999", …) guard is preserved — it
protects a different regression (GITHUB_RUN_ID leaking into the hive label
when --local-dir / -LocalDir is used).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ance Without --pr-number / --hive-label, the script previously dropped packages into hives/run-<workflowRunId>/packages. With the v3 design, the installed CLI's CliExecutionContext.Channel is baked at build time from AspireCliChannel and is one of pr-<N>/staging/daily/local — never run-<id>. Packages installed into a run-<id> hive are therefore unreachable from the CLI. Reject early with an actionable error directing the user to pass --pr-number (preferred) or --hive-label matching the baked AspireCliChannel. This is the Option B path from the v3-pr1 build-infra review; Option A (querying the binary's baked channel) would require new CLI surface area. Tests: - PRScriptShellTests / PRScriptPowerShellTests: add regression guards asserting the rejection, plus tighten the --run-id-as-first-arg parsing tests to pair --run-id with --hive-label so they still verify positional parsing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two coupled changes that close a silent-failure path on push events: 1. .github/workflows/tests.yml: gate the polyglot_validation job on github.event_name == 'pull_request'. Mirrors cli_starter_validation_windows. ci.yml triggers tests.yml on push to main/release/** as well as PRs, but the polyglot validation flow has no way to compute a hive label that the non-PR CLI archive (channel=daily/staging) will read from. Also exempt polyglot_validation.result == 'skipped' from the non-PR failure gate in test_summary. 2. .github/workflows/polyglot-validation/setup-local-cli.sh: remove the silent fall-back to HIVE_LABEL=local when no PR suffix is detected on the built nupkgs and fail loud instead. This is now a guard rather than a real code path — the workflow guard above ensures the script only runs on PR events where the suffix is always present — but the loud error is important defense in depth if the guard ever regresses. Option A (deriving the hive label from the CLI binary's baked AspireCliChannel) would require new CLI surface area and is not pursued here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erProject + NewCommand
Mirrors PrebuiltAppHostServerChannelResolutionTests to cover the remaining
two readers from which PR1 removed the global-channel read fallback
(IConfigurationService.GetConfigurationAsync("channel", ...)):
- DotNetBasedAppHostServerProject — direct-instantiation behavioral guards:
per-project aspire.config.json is honored, legacy AspireJsonConfiguration
is honored when the new file is absent, the new format wins when both
exist, and a null channel is returned when no per-project state is set.
- NewCommand — tripwire IConfigurationService that throws on any
GetConfigurationAsync/GetConfigurationFromDirectoryAsync call for the
'channel' key (mirrors InitCommand_DoesNotConsultGlobalConfigurationService\
ForChannelKey).
Addresses the gap noted in PR1 review feedback that 'coverage relies on the
existing tests still passing after the fallback delete' for these two readers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR1 added several channel-name comparisons using StringComparison.Ordinal,
which made user-supplied channel values (config, --channel, baked metadata
read paths) reject mixed-case inputs. Three CI tests caught it:
- KnownFeaturesTests.IsStagingChannelEnabled_IsCaseInsensitive_ForChannelValue
- KnownFeaturesTests.IsStagingChannelEnabled_IsCaseInsensitive_ForUppercaseChannelValue
- VersionHelperTests.IsLocalBuildChannel_RecognizesAllLocalChannelForms("LOCAL")
Switch all channel-name matching to OrdinalIgnoreCase across the call sites
that compare values against PackageChannelNames.* literals or PackageChannel
records. IdentityChannelReader keeps Ordinal intentionally — it strictly
validates the lowercase value baked into our own assembly metadata and uses
case-sensitivity as a misconfiguration tripwire.
Also addresses PR microsoft#16820 review comment r3228883227.
bff53cc to
4ecc899
Compare
- Merged decision inbox (livingston-runbook-e2e-feasibility + livingston-pr1-test-followups) into decisions.md - Created orchestration logs for livingston (runbook analysis + test implementation) and basher (push + CI watch) - Created session log summarizing PR microsoft#16820 final state: tests passing, ready for review - Appended team updates to livingston and basher history files - Summarized livingston history.md (overflow >15KB) into history-archive.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a Channel Identity subsection in docs/specs/bundle.md covering the AspireCliChannel → [AssemblyMetadata] → IdentityChannelReader → CliExecutionContext.IdentityChannel pipeline, plus tables for the supported channel names and the hive types on disk. Adds a user-facing channel-names callout to docs/dogfooding-pull-requests.md so users know the valid values for aspire.config.json#channel and `aspire update --channel`, including the `local` hive used by developer builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Agent: livingston Files created: - .squad/decisions.md (merged inbox) - .squad/orchestration-log/2026-05-12T22-39-42Z-livingston.md - .squad/log/2026-05-12T22-39-42Z-channel-docs.md Decision inbox (4 items) merged and cleared. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…icrosoft#16820) * feat(cli): bake AspireCliChannel into assembly metadata Add an AspireCliChannel MSBuild property whose value (stable | staging | daily | pr | local) is emitted as `[AssemblyMetadata("AspireCliChannel", ...)]` on the CLI assembly. Locally-built CLIs default to "local" so they don't impersonate the real "daily" channel — this matters for downstream PSM guards keyed on identity == channel. CI overrides the default via /p:AspireCliChannel=$(aspireCliChannel): - eng/clipack/Common.projitems propagates the value through the RID-specific pack pipeline. - eng/pipelines/templates/build_sign_native.yml threads it through the AzDO internal native-build template. - .github/workflows/build-cli-native-archives.yml does the same for GitHub Actions native archive builds. PackageChannelNames.Local ("local") is added as a constant so the rest of the CLI doesn't have to deal in raw strings. Tests: - AssemblyMetadataChannelTests asserts the running CLI assembly carries the AssemblyMetadata attribute and that its value is one of the known channels. - CliMetadataPackagingTests verifies the metadata survives `dotnet pack`. - ClipackPropagationTests pins that clipack RID-specific packs honor the AspireCliChannel override. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(cli): IdentityChannelReader sources channel from assembly metadata Wire the bootstrap so the running CLI's channel is sourced from the binary's own [AssemblyMetadata("AspireCliChannel", ...)] value rather than from any ambient configuration. This is the runtime half of the channel-baking story: the build pins the channel into the assembly, and the CLI reads its own identity at startup. - Acquisition/IdentityChannelReader.cs is the new component. It reads the AspireCliChannel metadata key from a caller-supplied Assembly. The ctor requires an explicit Assembly (no Assembly? = null default) so misuse is caught at construction time rather than via a cryptic "metadata missing on '?'" later. Production callers pass typeof(Aspire.Cli.Program).Assembly; tests pass a fake AssemblyBuilder assembly. - Program.BuildApplicationAsync registers IIdentityChannelReader in DI and threads its value into the new CliExecutionContext.Channel / .IdentityChannel / .PrNumber surface. - CliExecutionContext exposes: * Channel — the resolved hive label. For non-PR builds, identical to the identity channel verbatim. For PR builds (identity == "pr" with non-null PrNumber), resolves to "pr-<N>" — the directory name the packaging service uses, and the value reseed call sites write into a project's aspire.config.json#channel. * IdentityChannel — the raw build-time identity ("local"|"stable"| "staging"|"daily"|"pr"). Distinct from Channel because consumers that need the build-time taxonomy (PSM guards, version checks) should not see the per-PR refinement. * PrNumber — exposed verbatim. The constructor refuses channel="pr"+prNumber=null so a malformed PR build cannot construct. Tests: - IdentityChannelReaderTests covers the metadata-key contract and the real GitHub Actions InformationalVersion shapes (-pr.<N>.g<SHA>+<sha>) the parser must accept. - CliBootstrapTests asserts the production wiring resolves a known channel value when invoked against the real Aspire.Cli assembly. - CliExecutionContextTests exercises the Channel/IdentityChannel/PrNumber surface, including the malformed-PR-build constructor guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): rename GetPrHiveCount to GetHiveCount The original name suggested the count was specific to PR build hives, but the implementation has always returned the count of every subdirectory in the hives root — including the local hive and any other channel-specific hives a developer may have. The new name reflects what the method actually does. This is a mechanical rename of the method on CliExecutionContext plus its five call sites (AddCommand, IntegrationPackageSearchService, NewCommand, UpdateCommand, TemplateNuGetConfigService). Comments and the doc string are updated to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): drop IConfigurationService from app-host project factories The app-host project plumbing was reading the channel through IConfigurationService.GetConfigurationAsync("channel", ...) — a global lookup that returned whatever happened to be in the user's ambient aspire config. After the IdentityChannelReader work, the channel is available verbatim from CliExecutionContext.Channel, so the global read is redundant and removes a silent source of channel drift between sibling processes. Changes: - AppHostServerProjectFactory: drop the IConfigurationService dependency. The factory now takes CliExecutionContext directly and forwards it. - DotNetBasedAppHostServerProject: drop the unused _configurationService field and its constructor parameter. - GuestAppHostProject: add the CliExecutionContext dependency so the generated guest-app-host can pass the channel through into its own DI graph (channel-aware paths that previously asked the global config now ask the execution context). Tests: - AppHostServerProjectTests: drop the configurationService argument from the test factory; nothing else changes. - GuestAppHostProjectTests: pass a test CliExecutionContext through the constructor for parity with the production wiring. - CliTestHelper: factory now wires CliExecutionContext into the new constructor signatures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): PrebuiltAppHostServer resolves channel from per-project aspire.config.json When a prebuilt app-host runs against an existing project, the channel it should use is the one the project was scaffolded against — stored in the project's aspire.config.json#channel. The previous implementation consulted global identity-channel state, which could disagree with the per-project value when a developer ran multiple CLIs against the same project. Changes: - PrebuiltAppHostServer.ResolveChannelName: read only the per-project aspire.config.json. Return null when the file is absent or sets no channel; let downstream consumers decide what to do with that. - Add a PSM (per-source mapping) guard: when the running CLI's identity channel is "local" and the requested channel is also "local", suppress the temporary NuGet config generation so the project picks up the user's ambient NuGet sources instead of being pinned to the local hive's PSM. For every other identity channel (stable, staging, daily, pr) the guard does not fire and a per-channel NuGet config is generated as before. Tests: - PrebuiltAppHostServerChannelResolutionTests pins the per-project aspire.config.json#channel contract: null when absent, value when present. - PrebuiltAppHostServerTests adds the PSM-guard cross-product (identity channel × requested channel) so a regression that broadens the guard is caught. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): TemplateNuGetConfigService drops global-channel fallback; "local" override → implicit channel TemplateNuGetConfigService had three channel-resolving entry points (PromptToCreateOrUpdateNuGetConfigAsync, CreateOrUpdateNuGetConfigWithoutPromptAsync, ResolveTemplatePackageAsync) and each one — when the caller did not supply an explicit channel — fell back to reading ~/.aspire/aspire.config.json#channel through IConfigurationService. That made template resolution silently depend on whatever channel happened to be active for OTHER projects on the machine. The fix is to drop the IConfigurationService dependency entirely: the only channel inputs are now (1) the caller-supplied argument or (2) the implicit channel. The three entry points short-circuit on null/whitespace input instead of consulting a global source. Side effect — local-channel override: A locally-built CLI bakes channel="local" into its assembly metadata. On a clean machine without ~/.aspire/hives/local, PackagingService produces no "local" channel, and InitCommand forwards CliExecutionContext.Channel ("local") as the explicit ChannelOverride. Without resolver-level support this throws ChannelNotFoundException and `aspire init` is unusable on a clean machine. New behavior: a request for "local" with no matching channel resolves to the implicit channel — semantically a CLI with no local hive is just a CLI using the ambient NuGet configuration. The fallback is narrowly scoped to "local"; any other unrecognized channel name still fails loudly (typos like "stalbe" still surface as ChannelNotFoundException). Tests: - TemplateNuGetConfigServiceTests covers null/whitespace short-circuits for the two prompt-style entry points, the local→implicit fallback, and the negative case that other unrecognized channels still throw. - DotNetTemplateFactoryTests drops the now-removed IConfigurationService argument from CreateTemplateFactory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(cli): scaffolding reseeds channel into per-project aspire.config.json When `aspire init` or `aspire new` scaffolds a new project, write the CLI's resolved CliExecutionContext.Channel value into the project's aspire.config.json#channel field. This pins the per-project channel to the CLI that scaffolded it, so subsequent runs against the project resolve packages from the same hive — independent of any global channel setting that may have drifted. Changes: - ScaffoldingService gains channel-reseed entry points used by all starter-template factories. - CliTemplateFactory.{Go,Python,TypeScript}StarterTemplate forward CliExecutionContext.Channel into the reseed step after files are copied. - InitCommand and NewCommand pass CliExecutionContext.Channel into the scaffolding pipeline; they no longer derive the channel from global config. Tests: - ChannelReseedTests pins the per-channel reseed contract: each identity channel (local | stable | staging | daily | pr-N) ends up written verbatim into aspire.config.json#channel, including the pr-N resolution for PR-channel CLIs. - InitCommandTests adds the reseed coverage and updates the existing init flow expectations to match the new wiring (drops the now- removed FakeConfigurationServiceWithChannel helper and the two tests that exercised the deleted global-channel-fallback path). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(cli): PackageChannel enumerates flat-folder hives; VersionHelper recognizes "local" Two related additions that complete the local-channel runtime story. PackageChannel — flat-folder enumeration: For local hive channels the Aspire* source is a flat folder of .nupkg files (the layout created by ./build.sh --pack). `dotnet package search` does not support local folder sources and returns no results, so GetIntegrationPackagesAsync would yield an empty set for the local hive even when packages are present. New behavior: when PinnedVersion != null and the Aspire-filtered mapping points at an existing local directory, enumerate *.nupkg files directly and project them to NuGetPackage identities. The HTTP/search path is unchanged for non-local channels. VersionHelper.IsLocalBuildChannel — accept "local": The helper previously recognized only `pr-*` (PR hives) and `run-*` (workflow-run hives) as locally-built channels. With the baked AspireCliChannel value of "local" for ./build.sh builds, the helper now also returns true for the literal "local" channel, so version-check paths classify local-dev builds correctly alongside PR builds. Tests: - PackagingServiceTests adds direct coverage for the flat-folder enumeration path, including the corrupted-state shape (hive dir exists but `packages/` is empty) where PinnedVersion is null and enumeration must safely yield nothing. - VersionHelperTests pins the new "local" → true case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): get-aspire-cli-pr picks the correct hive label for local-dir installs The PR-install scripts (get-aspire-cli-pr.{sh,ps1}) installed CLI artifacts into a hive label that did not match what the running CLI's CliExecutionContext.Channel resolved to. The mismatch surfaced when the script ran against a local `--local-dir` source: artifacts landed under ~/.aspire/hives/<wrong-label>/, but the CLI looked under ~/.aspire/hives/pr-<N>/ (PR builds) or ~/.aspire/hives/local/ (local dev builds), so the freshly-installed packages were invisible. Resolution rule: - If the script's source nupkgs carry a PR-suffixed version (matching `pr.<N>.g<sha>`), the hive label is `pr-<N>`. - Otherwise the hive label is `local`. - Default falls back to `local` so `--local-dir` installs from local-dev builds (no PR suffix) work end-to-end without manual labeling. The release-side scripts (get-aspire-cli.{sh,ps1}) had divergent hive- labeling logic that the PR-script fix obsoletes; the release scripts now share the same three-branch resolution, removing ~340 lines of duplicated branch-handling. The polyglot-validation setup-local-cli.sh helper learns the same rule so CI's local-CLI smoke tests install into the same hive label the CLI will read from. Tests: - PRScriptShellTests / PRScriptPowerShellTests cover the three-branch resolution (pr-N from suffix, local fallback, explicit override). - ReleaseScriptShellTests / ReleaseScriptPowerShellTests mirror the pinning on the release-script side after the dedup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): aspire update stops writing global channel setting Self-update previously saved the chosen channel (or cleared it for "stable") into ~/.aspire/aspire.config.json#channel so subsequent `aspire new` and `aspire init` runs would pick it up via global config. With the per-project channel reseed landed earlier in this branch, the global setting is no longer a source of truth for anything — every scaffolded project carries its own channel — so the global write is dead code and a source of drift between projects on the same machine. Changes: - UpdateCommand.ExecuteSelfUpdateAsync no longer calls SetConfigurationAsync / DeleteConfigurationAsync for "channel". - The accompanying comment is rewritten to describe the current contract: the channel choice applies to this self-update only; subsequent scaffolding resolves channel per-project. - The legacy → new config migration drops the channel key during migration (the migrated global config never carries channel), matching the new "channel is not stored globally" invariant. The legacy file is still preserved verbatim for backward compatibility with older CLIs. Tests: - UpdateCommandTests asserts no global channel writes occur during self-update across all selectable channels. - ConfigMigrationTests (E2E) updates the migration assertion to expect the channel key absent in the migrated aspire.config.json and present in the preserved legacy file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli): cross-channel coverage for aspire add (local hive) Pin the `aspire add` hive-resolution behavior across the new local channel to match the existing PR-hive coverage: - AddCommand_WithLocalHive_PrefersCurrentCliVersion: with a populated ~/.aspire/hives/local/packages dir whose Aspire.Hosting pkg matches the running CLI's version, `aspire add` should pick that version without prompting (the local-hive equivalent of the existing WithPrHive coverage). This complements the PackageChannel flat-folder enumeration and the ChannelReseed coverage added earlier on this branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): bake `pr-<N>` into AspireCliChannel; drop runtime parse+join Previously the CLI carried two pieces of channel state: 1. [AssemblyMetadata("AspireCliChannel", "pr")] — the 5-value taxonomy (stable | staging | daily | pr | local). 2. AssemblyInformationalVersionAttribute (0.0.0-pr.<N>.g<sha>) — the version stamp, which was *separately* parsed at runtime by IdentityChannelReader.ParsePrNumber to extract <N>. CliExecutionContext then joined the two as `pr-<N>` for the hive label exposed via .Channel. The split existed only inside the CLI itself — every non-`pr` channel passed through verbatim, no external consumer relied on the `pr` literal, and the only other src/ reader of the raw taxonomy (PrebuiltAppHostServer.cs:423) only checked `== "local"` which is unchanged in either scheme. Collapse the two pieces. CI now bakes the resolved hive label directly: * GH Actions (.github/workflows/build-cli-native-archives.yml): pull_request -> pr-<github.event.pull_request.number> * AzDO (eng/pipelines/templates/build_sign_native.yml): Build.Reason == PullRequest -> pr-<System.PullRequest.PullRequestNumber>, with a digit-only regex pre-check that throws if the agent ever hands us an unresolved macro or non-numeric value (clearer failure attribution than waiting for the runtime IsValidChannel check). Runtime simplifications: * IdentityChannelReader.ParsePrNumber: deleted (~40 lines). * IdentityChannelReader.ResolveChannel: now validates shape via the new IsValidChannel helper. Accepts the four fixed strings (stable | staging | daily | local) or `pr-<digits>`. Rejects the legacy literal `pr` (no suffix) so a CI misconfiguration cannot silently mis-route packages. * CliExecutionContext: `prNumber` ctor parameter dropped; PrNumber and IdentityChannel properties dropped; Channel is now a plain auto-property. The `channel=='pr' && prNumber is null` throw guard goes away — the invariant is now structurally enforced by the channel string itself. * Program.cs: the `if (channel == "pr") { ParsePrNumber(...) }` block collapses to one line. PrebuiltAppHostServer.cs: the one IdentityChannel reference becomes Channel; behavior identical because the only check is `== "local"` and `local` is unchanged. Tests: * AssemblyMetadataChannelTests: smoke-tests the baked value against IsValidChannel (was: set-membership against the legacy 5 values). * IdentityChannelReaderTests: drops the ParsePrNumber theory entirely, adds the IsValidChannel truth table, and adds a Throws-based set covering invalid shapes (including the legacy `pr` literal as a regression guard for the CI change). * CliExecutionContextTests: rewritten as a thin getter test (the type is now a holder, not a join). * CliBootstrapTests: drops the IdentityChannel branch on the PrNumber assertion. * InitCommandTests, PrebuiltAppHostServerTests, ChannelReseedTests: builders drop the `prNumber:` argument; callers that previously passed `channel: "pr", prNumber: 12345` now pass `channel: "pr-12345"`. All 2881 Aspire.Cli.Tests pass; src/Aspire.Cli and tests build clean on net10.0/arm64. Out of scope: the shell-script PR-suffix detection in setup-local-cli.sh (commit 160801c) parses nupkg filenames stamped by VersionSuffix, not assembly metadata, so it is not affected by this change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli): cover picker & channel-resolution gaps on the channel branch Adds direct tests for behaviors the branch already implements but that were only covered transitively, so a regression here would surface in unrelated suites rather than naming the cause. - CliExecutionContextTests: 4 GetHiveCount() tests (missing dir → 0, empty dir → 0, populated count, ignores stray files). GetHiveCount() gates the channel picker in update, the channel sub-menu in add, and explicit-channel inclusion in IntegrationPackageSearchService — so a bug here previously manifested as cross-command flake. - UpdateCommandTests: three picker-contract tests. * WithHivesAndConfiguredChannel and WithHivesAndLocallyConfiguredChannel close the (hive-present × configured-channel) intersection — existing tests cover one axis at a time but not the intersection that PR-build users actually hit. * WithHives_PromptOffersChannelsInPackagingServiceOrder is the prompt-content contract: every existing prompt callback either flag-sets or .First()s the choices; nothing verified the names, order, or 'Name (SourceDetails)' label format. Reorderings or label drops would otherwise be silently green. - PrebuiltAppHostServerChannelResolutionTests: two migration-safety tests for the new aspire.config.json ?? legacy AspireJsonConfiguration fallback. Covers the legacy-only path (no new file present) and the precedence when both files exist with different channels — guards against an accidental ?? operand swap. - TemplateNuGetConfigServiceTests: three IncludePrHives matrix tests. The existing three resolver tests all pass IncludePrHives: false; the true row (the aspire new path) and the (true × no-hives-on-disk) AND-gate were uncovered. Adds: IncludePrHivesTrue_WithHivesPresent_AllChannelsParticipate, IncludePrHivesTrue_NoHivesOnDisk_RestrictsToImplicit, and IncludePrHivesFalse_IgnoresHivesEvenWhenPresent (the aspire init policy). Verified locally: 72/72 of the affected test classes pass; 56/56 unaffected NewCommandTests still pass. Three gaps from the audit are intentionally deferred: - NewCommand local-build channel bias and global-config-fallback removal (the audit's T1/G8) target ResolveCliTemplateVersionAsync, which only fires for TemplateRuntime.Cli templates. Reaching it via aspire-starter is impossible — it's TemplateRuntime.DotNet and takes a different resolution path. The clean fix is to extract the channel-selection block to an internal helper that takes (channels, executionContextChannel, configuredChannelName) and tests it as a pure function across all rows; that refactor is out of scope for a pure test-add commit. - A scaffold-then-resolve end-to-end round-trip (G5) is now reachable in principle via the new G6 read-side tests plus existing ChannelReseedTests on the write side, so the marginal value of a joint test is small. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli): simplify channel-test suite — dedupe, fold theories, drop reflection Eleven targeted test simplifications across the new channel-resolution suite. Net: 15 files touched, -284 lines, no coverage dropped (verified by case-by-case audit: every assertion in every removed/folded test still has at least one equivalent assertion remaining). 1. Delete CliMetadataPackagingTests.cs — its sole "metadata exists + non-empty" assertion is a strict subset of AssemblyMetadataChannelTests.AspireCliChannel_AssemblyMetadata_HasValidShape (IsValidChannel already rejects null/empty). 2. CliBootstrapTests: BuildApplication_LocallyBuiltCli_… and BuildApplication_CliExecutionContextChannel_… asserted the same thing. Keep one; extract GetBakedEntryAssemblyChannel() so the GetCustomAttributes<AssemblyMetadataAttribute>().Single(...) lookup isn't open-coded. 3. UpdateCommandTests: drop SelfUpdate_StableChannel_DoesNotDeleteGlobalChannel — the SelfUpdate_DoesNotWriteChannelToGlobalConfiguration theory's "update --self --channel stable" row already asserts no-delete (and no-set, so it's a strict superset). 4. AssemblyMetadataChannelTests: replace fragile CliAssembly_BakesChannel_AsLocal_MatchingCsprojDefault (would fail under /p:AspireCliChannel=stable CI builds) with Csproj_DeclaresAspireCliChannelDefault_AsLocal — inspects the csproj XML for the `<AspireCliChannel Condition="'$(AspireCliChannel)' == ''">local` element. Guards the same property (the declared default) without coupling to the build configuration of the test host. 5. AddCommandTests: extract RunAddRedisWithHiveScenarioAsync to share the workspace + prompter + project locator + AddPackage-capture scaffolding across the three hive tests (WithPrHive / WithLocalHive / WithLocalAndPrHives). Each test now declares only the hive layout and the SearchPackagesAsync mock. 6. ChannelReseedTests: NoExplicitChannel (4 InlineData rows) + ExplicitChannel (Fact) → one [Theory] with five rows over (contextChannel, explicitChannel, expected). Drop the CreateExecutionContext→BuildContext one-line wrapper and switch from Directory.CreateTempSubdirectory + try/finally to TemporaryWorkspace (project convention). 7. IdentityChannelReaderTests: the IsValidChannel_MatchesExpectedTruthTable theory exhaustively asserts the shape contract; the assembly-roundtrip tests only need to verify the wiring from ReadChannel through IsValidChannel. Reduce ReadChannel_AssemblyHasMetadataForValidChannel_ReturnsValue (6 rows) and ReadChannel_InvalidChannelValue_Throws (10 rows) to one representative [Fact] each; drop ReadChannel_AssemblyHasEmptyChannelMetadata_Throws (covered by the empty-string row in the truth table). 8. PrebuiltAppHostServer.ResolveChannelName: private → internal (the assembly already has [InternalsVisibleTo("Aspire.Cli.Tests")]). Drop reflection in PrebuiltAppHostServerChannelResolutionTests (4 sites) and PrebuiltAppHostServerTests (1 site). 9. CliExecutionContextTests: Channel_DefaultsToLocal_WhenNotSpecified now uses CreateContext(channel: null) instead of inlining the 5-line ctor that the helper already encapsulates. 10. Move Ctor_NullAssembly_ThrowsArgumentNullException from CliBootstrapTests to IdentityChannelReaderTests — it's about reader ctor validation, not bootstrap-wiring DI. 11. TemplateNuGetConfigServiceTests: fold three IncludePrHives Facts (TrueWithHives / TrueNoHives / FalseWithHives) into one [Theory] with three rows over (includePrHives, createHiveOnDisk, expectedVersion, expectImplicitChannel, expectedChannelName). Verified: build clean; 180 tests across the affected classes pass locally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(specs): correct bundle.md self-update flow after global-channel write removal The self-update process description listed a 7-step flow ending with "Saves selected channel to global settings". That step was removed across the v3 acquisition work — the CLI no longer writes the channel globally (acquisition scripts dropped save_global_settings; aspire update --self stopped persisting --channel). Identity is now baked into the binary via AspireCliChannel assembly metadata; per-project overrides live in aspire.config.json#channel. Drop step 7 and add a short note explaining the new model so docs match reality. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(cli): mark global-channel fallback transitional in UpdateCommand precedence Identity-channel is baked into the CLI via AspireCliChannel assembly metadata; the acquisition scripts and aspire update --self no longer seed a global "channel" field. The step-2 fallback in ConfigurationService.GetConfigurationFromDirectoryAsync still reads the global file, however, so aspire update's documented step-3 precedence ("global config 'channel'") is reachable. That's intentional for now — users who deliberately ran \`aspire config set -g channel <x>\` on the new CLI keep their preference honored by aspire update — but it's worth flagging at both the call site (UpdateCommand precedence comment) and the impl (ConfigurationService) so a future reader doesn't assume the global read is also dead. Add TODO markers calling out that the fallback can be removed once telemetry confirms negligible global-channel usage. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(acquisition): auto-detect raw-build vs tarball in --local-dir flow Cherry-picks the helper bodies from 721811c but reshapes the user surface: instead of a separate --local-binary flag, install_from_local_dir auto-detects from directory contents — tarball glob match → archive flow; recursive aspire-exe match → raw-build flow. Drops the LOCAL_BINARY=false variable, --local-binary arg parser, and requires-LocalDir guard from the original commit (no longer needed with auto-detect). Preserves bash/pwsh parity. Existing LocalDir_* tests pass unmodified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(acquisition): release scripts must not write global channel field PR1 invariant: install.sh / install.ps1 no longer write `~/.aspire/aspire.config.json#channel`. Adds Install_DryRun_DoesNotWriteGlobalChannelField to the bash and pwsh test classes, asserting both that stdout under --dry-run / -WhatIf contains no channel-write log line and that the global config file isn't created under MockHome. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): rename CLI identity & project-requested channel symbols Disambiguate the two distinct channel concepts that PrebuiltAppHostServer threads through restore: - CliExecutionContext.Channel -> IdentityChannel The hive label baked into the CLI binary via [AssemblyMetadata("AspireCliChannel", ...)]. Identity of the running CLI, not what any project asks for. - PrebuiltAppHostServer.ResolveChannelName() -> ResolveRequestedChannel() Reads aspire.config.json#channel (with legacy fallback). The project's request to restore — independent of which CLI is running. - Parameter / local `channelName` -> `requestedChannel` Across RestoreNuGetPackagesAsync, BuildIntegrationProjectAsync, TryCreateTemporaryNuGetConfigAsync, GetNuGetSourcesAsync. XML doc on IdentityChannel makes the identity-vs-request distinction explicit. No behaviour change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): PSM guard keys on resolved channel, not CLI identity PrebuiltAppHostServer.TryCreateTemporaryNuGetConfigAsync used to short-circuit package source mapping (PSM) emission when the running CLI's IdentityChannel was "local". That dropped PSM for legitimate scenarios: - locally-built CLI (IdentityChannel == "local") running an apphost whose aspire.config.json#channel asks for "pr-12345" / "daily" / "staging" — restore silently used the ambient NuGet config (PSM dropped), instead of the channel's package source mappings. The guard now keys on the resolved channel.Name. The local hive has no real mappings, so emitting PSM for it would constrain restore to nothing; for every other channel (stable, staging, daily, pr-*) PSM must emit so restore honours the channel's package source mappings — regardless of which CLI identity is running. Keying on channel.Name (rather than the input requestedChannel) is robust to alias/normalization in the channel lookup above the guard. Test matrix now covers the full identity x requested cross-product: - LocalIdentity_LocalRequested_ReturnsNull - LocalIdentity_PrRequested_EmitsConfig (regression case) - StableIdentity_StableRequested_EmitsConfig - StableIdentity_LocalRequested_ReturnsNull - DailyIdentity_DailyRequested_EmitsConfig - PrIdentity_DifferentPrRequested_EmitsConfig - LocalIdentity_StagingRequested_EmitsConfigWithGlobalPackagesFolder (also pins that ConfigureGlobalPackagesFolder propagates to the emitted nuget.config; staging is the only channel today with that setting) - LocalRequested_ReturnsNull_RegardlessOfIdentity [Theory x4] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): split TemplatePackageQuery, move init's local fallback to caller TemplatePackageQuery.ChannelOverride carried two concepts: - aspire init forwarded _executionContext.IdentityChannel (CLI identity). - aspire new forwarded inputs.Channel (--channel / user request). The resolver compensated with a "channelName == 'local' and not found -> fall back to implicit" branch. That rule is only semantically correct for the identity caller: an explicit `aspire new --channel local` on a machine without a local hive should surface a ChannelNotFoundException, not silently switch to ambient NuGet. Rename the field to RequestedChannel and move the fallback contract into InitCommand (the only caller that wants it). The resolver becomes a pure single-concept lookup — drops the special-case branch entirely. User-visible effects: - `aspire init` on a local-CLI without ~/.aspire/hives/local: still falls back to implicit (init catches ChannelNotFound and retries with RequestedChannel: null). - `aspire new --channel local` without the hive: now throws ChannelNotFoundException with the standard "Valid options are: ..." message instead of silently using ambient NuGet. Test coverage: - ResolveTemplatePackage_RequestedChannel_NotFound_Throws - ResolveTemplatePackage_RequestedChannel_Matches_ReturnsThatChannel - existing InitCommand_OnLocalChannelCli_WithNoLocalHive_FallsBackToImplicitChannel still passes (now exercises the fallback in InitCommand, not the resolver). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(cli): document dual-source contract on Template*NuGetConfigAsync The string? channelName parameter on PromptToCreateOrUpdateNuGetConfigAsync and CreateOrUpdateNuGetConfigWithoutPromptAsync is passed by: - EmptyTemplate / aspire new : inputs.Channel (project request) - InitCommand : CliExecutionContext.IdentityChannel (running CLI identity) The lookup against IPackagingService.GetChannelsAsync is name-equivalent for both, so the existing single-parameter shape stays. Update the XML docs to document the dual-source contract explicitly so future readers do not read the parameter name as request-only (the audit flagged this as a maintainability trap). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): `aspire update` no longer pins identity when channel is implicit GuestAppHostProject.UpdatePackagesAsync used to write `config.Channel = _executionContext.IdentityChannel` whenever the update's resolved PackageChannel was implicit. That silently propagated dev/PR-build CLI identities into projects: a developer running `aspire update` with a locally-built CLI would pin `channel: "local"` into the project's aspire.config.json, surfacing identity into a file the user expected to remain untouched. `aspire update` is a no-pin path: the user is updating packages, not initialising. Only write config.Channel when the update actually resolved an explicit channel (--channel, per-project pin, prompt selection). The scaffolding / build-time paths intentionally do auto-pin identity; those are first-write contracts and unaffected. The pre-existing UpdateCommand_WithoutHives_UsesImplicitChannelWithoutPrompting test used a mock TestProjectUpdater that bypassed GuestAppHostProject entirely, so this bug was unit-test-invisible. New regression test UpdatePackagesAsync_ImplicitChannel_DoesNotPinIdentityIntoConfig exercises the real path and was verified red before applying the fix (Actual: "pr-99999" before fix, null after). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(cli): clarify `IsStagingChannelEnabled` reads layered configuration The staging-channel gate in KnownFeatures.IsStagingChannelEnabled reads configuration["channel"] (env vars + command-line + per-project aspire.config.json#channel + global config), NOT CliExecutionContext.IdentityChannel. A CLI baked with AspireCliChannel=staging does NOT auto-enable staging in the packaging service unless the user has also set the configuration value explicitly (e.g. via `aspire config set channel staging` or `--channel staging`). This is by design: identity selects which hive directory the CLI ships in; feature gating goes through user-visible configuration. Audit flagged the asymmetry as undocumented; adding a <remarks> block so future maintainers do not read the gate as identity-driven. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): `aspire run` no longer mutates `config.Channel` GuestAppHostProject.RunAsync and ScaffoldingService.ScaffoldGuestLanguageAsync each silently wrote config.Channel back to aspire.config.json after build / scaffold prepare. Both writes reduced to one of two cases: - The project already had a channel pinned -> buildResult.ChannelName / prepareResult.ChannelName matched it (both come from AspireConfigFile.Load(...)?.Channel) -> no-op file write. - The project had no channel pinned -> buildResult.ChannelName was null, falling through to `?? _executionContext.IdentityChannel` -> silent identity pin on every `aspire run`. Neither case is useful work. The C# apphost path (DotNetBasedAppHostServerProject) never writes config.Channel and has worked fine in this regard, so the asymmetry was branch-introduced (PR microsoft#16820's channel refactor), not load-bearing. After this commit: - `aspire run` is pure-read for config.Channel (guest + dotnet symmetric). - Scaffolding writes config.Channel exactly once, at seed time (ScaffoldingService:75) via the existing config.Save(...) at line 82. - `aspire update` writes config.Channel only when the user resolved an explicit channel (already pinned by the previous commit). Three writes total across the CLI, each tied to a concrete user-driven action. No silent pinning anywhere. New regression test RunAsync_DoesNotMutateConfigChannel (Theory x2: seededChannel="stable" and null) was verified red before applying the fix (both cases produced Actual: "pr-99999"; after fix both stay at the seeded value). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * style(cli): standardize channel-name comparisons Addresses PR microsoft#16820 reviewer feedback about inconsistent channel-name case sensitivity. Uses StringComparison.Ordinal for CLI channel names, including the PSM local-channel guard, because these channel values are baked/configured as well-known lowercase identifiers rather than free-form case-insensitive labels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): use StartsWith for hosting package match Addresses PR microsoft#16820 reviewer feedback asking whether the pinned local-hive package scan should use StartsWith instead of Contains. The filter is intended to keep Aspire.Hosting packages, so matching the package-id prefix avoids accidental substring matches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * style(cli): use channel constants in identity reader Addresses PR microsoft#16820 reviewer feedback to use PackageChannelNames constants for baked identity-channel validation. Keeps the fixed channel set centralized while preserving the existing pr-<N> prefix validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(acquisition): dry-run helpers return non-PR sentinel; fall back to local hive extract_version_suffix_from_packages (bash) / Get-VersionSuffixFromPackages (pwsh) short-circuit in --dry-run / -WhatIf and previously returned the literal 'pr.1234.a1b2c3d4'. The new --local-dir auto-detect logic at the call site feeds that value through the regex ^pr\.([0-9]+)\.[0-9a-g]+$ to derive a hive label, so the mock unconditionally forced hive_label='pr-1234' regardless of what was actually in --local-dir, making the intended 'local' fallback unreachable in dry-run. Return the non-PR-shaped sentinel 'local' instead, which the caller's regex intentionally does not match, so the dry-run path now behaves the same as the real-world 'no PR suffix detected' path: hive_label='local'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli): drop ClipackPropagationTests — coverage via AssemblyMetadataChannelTests The deleted tests asserted XML shape on eng/clipack/Common.projitems (MSBuild idiom: <MSBuild Targets="Publish" Properties="…@(AdditionalProperties)" />). That couples the test to a particular MSBuild construct: a behavior-preserving refactor (e.g. forwarding through a helper target or hand-built Properties= string) would fail the test, and an XML-valid but behaviorally-wrong refactor would still pass it. The downstream contract — "the baked CLI binary has AspireCliChannel in its AssemblyMetadata" — is covered by Aspire.Cli.Tests.AssemblyMetadataChannelTests, which reads the actual baked metadata and validates it against IdentityChannelReader.IsValidChannel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(acquisition): drop helper-name asserts and scrub PR1 narrative comments In ReleaseScript{Shell,PowerShell}Tests: 1. Drop Assert.DoesNotContain("save_global_settings" / "Save-GlobalSettings", …). Those are internal helper-function names; renaming the helper would break the test even though the contract still holds. The artifact-level check (File.Exists(globalConfig) == false) and the path-name check on dry-run output (DoesNotContain "aspire.config.json") already cover the real contract. 2. Replace "PR1 invariant" comments with statements of the invariant itself: install scripts must not write a global aspire.config.json — the channel is baked into the CLI binary at build time and read via IdentityChannelReader, so a global channel field would shadow the baked value. The string "PR1" encodes development-time PR sequencing and rots post-merge. 43/43 ReleaseScript{Shell,PowerShell} tests pass after the change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli/e2e): sharpen LocalHive comments as strategy-parametric design intent CLI E2E tests intentionally run against multiple CliInstallStrategy modes selected via CliInstallStrategy.Detect: LocalHive (local-built archive), InstallScript (downloaded by get-aspire-cli{,-pr}.{sh,ps1}), and pre-existing CLI on PATH. Only LocalHive returns a non-null channel from PrepareLocalChannel; the other strategies rely on the CLI's baked channel plus ambient NuGet feeds. The old comment ("For LocalHive runs, point the freshly-created project at the local channel…") read like a runtime env-sniff. Replace with a comment that names the strategy explicitly and contrasts LocalHive against the null-returning strategies, so readers see the if-branch as design intent rather than runtime hedging. Touched sites (no behavior change): - ChannelUpdateWorkflowTests.cs (1 site, replaced existing comment) - TypeScriptCodegenValidationTests.cs (1 site, added comment) - TypeScriptPolyglotTests.cs (4 sites: two channelArgument ternaries and two WriteLocalChannelSettings if-blocks, comment adapted per site) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(acquisition): tighten dry-run hive-label assertion to exact phrase In LocalDir_DryRun_WithGitHubRunIdEnvSet_UsesLocalHiveLabel and its PowerShell sibling LocalDir_WhatIf_WithGitHubRunIdEnvSet_UsesLocalHiveLabel, the existing assertion was too weak: Assert.Contains("local", result.Output, StringComparison.OrdinalIgnoreCase); The substring "local" appears in the fixture path (local-artifacts) and in neighboring log lines ("from local packages"), so the assertion passes even when the actual hive label is wrong (e.g. pr-<N> from a dry-run mock that collides with the auto-detect regex). Tighten to the exact phrase the script emits at install_aspire_cli / Resolve-HiveLabel: Assert.Contains("Using hive label: local", result.Output, StringComparison.Ordinal); The existing Assert.DoesNotContain("run-99999", …) guard is preserved — it protects a different regression (GITHUB_RUN_ID leaking into the hive label when --local-dir / -LocalDir is used). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(acquisition): --run-id-only flow is rejected with actionable guidance Without --pr-number / --hive-label, the script previously dropped packages into hives/run-<workflowRunId>/packages. With the v3 design, the installed CLI's CliExecutionContext.Channel is baked at build time from AspireCliChannel and is one of pr-<N>/staging/daily/local — never run-<id>. Packages installed into a run-<id> hive are therefore unreachable from the CLI. Reject early with an actionable error directing the user to pass --pr-number (preferred) or --hive-label matching the baked AspireCliChannel. This is the Option B path from the v3-pr1 build-infra review; Option A (querying the binary's baked channel) would require new CLI surface area. Tests: - PRScriptShellTests / PRScriptPowerShellTests: add regression guards asserting the rejection, plus tighten the --run-id-as-first-arg parsing tests to pair --run-id with --hive-label so they still verify positional parsing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(acquisition): polyglot-validation is PR-only and fails loud off-PR Two coupled changes that close a silent-failure path on push events: 1. .github/workflows/tests.yml: gate the polyglot_validation job on github.event_name == 'pull_request'. Mirrors cli_starter_validation_windows. ci.yml triggers tests.yml on push to main/release/** as well as PRs, but the polyglot validation flow has no way to compute a hive label that the non-PR CLI archive (channel=daily/staging) will read from. Also exempt polyglot_validation.result == 'skipped' from the non-PR failure gate in test_summary. 2. .github/workflows/polyglot-validation/setup-local-cli.sh: remove the silent fall-back to HIVE_LABEL=local when no PR suffix is detected on the built nupkgs and fail loud instead. This is now a guard rather than a real code path — the workflow guard above ensures the script only runs on PR events where the suffix is always present — but the loud error is important defense in depth if the guard ever regresses. Option A (deriving the hive label from the CLI binary's baked AspireCliChannel) would require new CLI surface area and is not pursued here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli): add fallback-removal regression for DotNetBasedAppHostServerProject + NewCommand Mirrors PrebuiltAppHostServerChannelResolutionTests to cover the remaining two readers from which PR1 removed the global-channel read fallback (IConfigurationService.GetConfigurationAsync("channel", ...)): - DotNetBasedAppHostServerProject — direct-instantiation behavioral guards: per-project aspire.config.json is honored, legacy AspireJsonConfiguration is honored when the new file is absent, the new format wins when both exist, and a null channel is returned when no per-project state is set. - NewCommand — tripwire IConfigurationService that throws on any GetConfigurationAsync/GetConfigurationFromDirectoryAsync call for the 'channel' key (mirrors InitCommand_DoesNotConsultGlobalConfigurationService\ ForChannelKey). Addresses the gap noted in PR1 review feedback that 'coverage relies on the existing tests still passing after the fallback delete' for these two readers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): channel-name comparisons are case-insensitive PR1 added several channel-name comparisons using StringComparison.Ordinal, which made user-supplied channel values (config, --channel, baked metadata read paths) reject mixed-case inputs. Three CI tests caught it: - KnownFeaturesTests.IsStagingChannelEnabled_IsCaseInsensitive_ForChannelValue - KnownFeaturesTests.IsStagingChannelEnabled_IsCaseInsensitive_ForUppercaseChannelValue - VersionHelperTests.IsLocalBuildChannel_RecognizesAllLocalChannelForms("LOCAL") Switch all channel-name matching to OrdinalIgnoreCase across the call sites that compare values against PackageChannelNames.* literals or PackageChannel records. IdentityChannelReader keeps Ordinal intentionally — it strictly validates the lowercase value baked into our own assembly metadata and uses case-sensitivity as a misconfiguration tripwire. Also addresses PR microsoft#16820 review comment r3228883227. * docs: document channel identity, supported names, and hives Adds a Channel Identity subsection in docs/specs/bundle.md covering the AspireCliChannel → [AssemblyMetadata] → IdentityChannelReader → CliExecutionContext.IdentityChannel pipeline, plus tables for the supported channel names and the hive types on disk. Adds a user-facing channel-names callout to docs/dogfooding-pull-requests.md so users know the valid values for aspire.config.json#channel and `aspire update --channel`, including the `local` hive used by developer builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(squad): log channel-docs session (livingston) Agent: livingston Files created: - .squad/decisions.md (merged inbox) - .squad/orchestration-log/2026-05-12T22-39-42Z-livingston.md - .squad/log/2026-05-12T22-39-42Z-channel-docs.md Decision inbox (4 items) merged and cleared. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Update no longer crashes when the AppHost's pinned Aspire.AppHost.Sdk
version cannot be resolved (e.g. moving between PR builds after the
hive was refreshed). UpdateCommand now calls the project locator in
TrustConfiguredPath mode, which skips MSBuild validation of the
configured AppHost path so ProjectUpdater can rewrite the SDK pin
via its existing fallback parser.
* Update no longer defaults to the Implicit ("daily") channel when a
PR-built CLI is run against an AppHost that has no per-project or
global channel pin. UpdateCommand now consults
ExecutionContext.IdentityChannel as a default before the prompt
fallback, restoring the pre-#16820 behavior without re-introducing
any global channel writes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix two regressions in `aspire update`
* Update no longer crashes when the AppHost's pinned Aspire.AppHost.Sdk
version cannot be resolved (e.g. moving between PR builds after the
hive was refreshed). UpdateCommand now calls the project locator in
TrustConfiguredPath mode, which skips MSBuild validation of the
configured AppHost path so ProjectUpdater can rewrite the SDK pin
via its existing fallback parser.
* Update no longer defaults to the Implicit ("daily") channel when a
PR-built CLI is run against an AppHost that has no per-project or
global channel pin. UpdateCommand now consults
ExecutionContext.IdentityChannel as a default before the prompt
fallback, restoring the pre-#16820 behavior without re-introducing
any global channel writes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Strip orphaned PR-hive sources from <packageSources> on update
When 'aspire update' merges a project's NuGet.config to a new channel, any
~/.aspire/hives/pr-<N>/packages entry that lives only in <packageSources>
(no corresponding <packageSourceMapping> element) used to survive the merge.
RemoveEmptyPackageSourceElements only cleans up <packageSource> mapping
entries that become empty during the merge, so a source that was never
mapped — or whose mapping was rewritten by an earlier merge — would linger
forever. As soon as the hive directory is replaced on disk (which now
happens routinely for refreshed PR builds), 'dotnet restore' fails with
NU1301: The local source '...' doesn't exist.
Add a final pass over <packageSources> that removes any safe-to-remove
source (the existing IsSourceSafeToRemove heuristic — paths under
.aspire/hives) that is not in sourcesInUse and not in the new channel's
RequiredSources.
Add two regression tests covering both shapes: original config with a
PR-hive source mapped (already cleaned by the empty-element pass; locked
down) and original config with the same PR-hive source listed but with no
mapping element (the actual failure mode).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix PR install PATH dedup and refuse PR-channel --self update
Two additional regressions that compounded the aspire update problems:
(D) get-aspire-cli-pr.{sh,ps1} appended a fresh PATH entry on every PR
install because the install path embeds the PR number
(/Users/midenn/.aspire/dogfood/pr-<N>/bin), so the literal-line dedup in
add_to_path never matched any prior PR install. Detect any existing
dogfood/pr-*/bin line and replace it in place when adding a new one;
do the same for the Windows user PATH registry entry.
(E) aspire update --self prompts {Stable, Daily, Staging} and routes
through _cliDownloader.DownloadLatestCliAsync. PR channels have no
cliDownloadBaseUrl, so a PR-built CLI silently moved the user off
the PR build whenever they ran --self. Refuse with a clear message
pointing at the acquisition script (the supported refresh path for
PR installs) when the running CLI's IdentityChannel starts with
'pr-'. An explicit --channel still opts out.
Tests:
- AddToPath_PrInstall_ReplacesExistingDogfoodPrLine
- AddToPath_PrInstall_WithNoPriorDogfoodLine_AppendsAsBefore
- AddToPath_NonPrInstall_AppendsAndDoesNotMatchDogfoodHeuristic
- UpdateCommand_SelfUpdate_WhenIdentityChannelIsPr_RefusesWithAcquisitionScriptHint
- UpdateCommand_SelfUpdate_WhenIdentityChannelIsPrAndExplicitChannelGiven_AllowsDownload
- UpdateCommand_SelfUpdate_WhenIdentityChannelIsDaily_AllowsDownload
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Revert PR-channel refusal on aspire update --self
Per user feedback: 'aspire update --self' should remain a valid
ejection mechanism for PR builds. The hard-coded Stable/Daily/Staging
prompt already lets a PR-built CLI move to a real channel; refusing
with an acquisition-script hint was unhelpful gatekeeping.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Persist resolved channel from `aspire new` (.NET scaffold)
The .NET starter and apphost templates went through DotNetTemplateFactory
without ever writing aspire.config.json#channel. The TypeScript starter
(CliTemplateFactory.TypeScriptStarterTemplate) and the empty-template /
init paths (ScaffoldingService) already mirrored the resolved channel
into the per-project config. The .NET path was an asymmetric gap.
Without a pin in the scaffolded project, `aspire update` skips its
local-config precedence step and falls through to either an interactive
prompt (when hives exist) or the Implicit/nuget.org channel — silently
moving a project scaffolded by a PR-built or daily CLI onto stable.
Mirror the TS pattern: when the resolved channel is Explicit (pr-<N>,
daily, staging, local), call AspireConfigFile.LoadOrCreate + Save with
the channel name. Implicit channels (stable/nuget.org) intentionally
stay unpinned so the user's ambient NuGet config governs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Extend channel pin to Python and Go starters
aspire-py-starter and aspire-go-starter previously declined to persist
the resolved channel into aspire.config.json, on the rationale that
PrebuiltAppHostServer aggregates package sources from every registered
channel when no pin exists. That rationale only held for a daily CLI —
on a PR-built CLI the next 'aspire update' falls through to a daily
prompt or the Implicit/nuget.org channel because the local-config step
in the channel-resolution precedence finds nothing.
Mirror the TypeScript starter / .NET starter pattern: when NewCommand
resolves an Explicit channel, write it to aspire.config.json#channel
before the SDK generation step. Implicit channels (stable/nuget.org)
remain unpinned.
Extend NewCommandTemplateConfigPersistenceTests to cover GoStarter and
PythonStarter across all three pin scenarios (identity unregistered,
identity matches, --channel overrides), and drop the now-obsolete
NonChannelPinningStarter_NeverPersistsChannel tripwire.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Skip persistent profile/PATH writes for PR installs
PR installs land under <prefix>/dogfood/pr-<N>/bin, a per-PR path used
for session-scoped dogfooding. Writing it into ~/.zshrc / ~/.bashrc (Unix)
or HKCU\Environment (Windows) silently demoted a developer's daily/stable
install on every new shell until they hunted down the stale entry.
Treat PR installs as session-only: update the current session PATH and
print the activation hint so the user can opt into persistence manually,
but leave shell profiles and the Windows user PATH untouched. Non-PR
routes through these same scripts (release channels, --install-prefix
overrides) keep the previous persistent-PATH behavior.
The earlier dogfood/pr-* dedup pass in add_to_path / Update-PathEnvironment
is no longer needed and has been removed; the obsolete add_to_path PR
dedup tests are replaced with a single test pinning the simpler append
contract.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Drop PR-N / date narrative from committed comments
Repo convention is that committed comments describe the current contract
as seen against origin/main; they should not reference internal PR-N
identifiers or branch-evolution narrative. Rewrite the affected block
headers in UpdateCommandTests.cs (and any matching narrative in
UpdateCommand.cs / ProjectLocator.cs found by audit) to describe what
the code / test group guards, in terms of contract rather than history.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Mitch Denny <midenn@orangecake.localdomain>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: mitchdenny <mitchdenny@users.noreply.github.com>
Co-authored-by: Mitch Denny <midenn@Mac.localdomain>
…icrosoft#16820) * feat(cli): bake AspireCliChannel into assembly metadata Add an AspireCliChannel MSBuild property whose value (stable | staging | daily | pr | local) is emitted as `[AssemblyMetadata("AspireCliChannel", ...)]` on the CLI assembly. Locally-built CLIs default to "local" so they don't impersonate the real "daily" channel — this matters for downstream PSM guards keyed on identity == channel. CI overrides the default via /p:AspireCliChannel=$(aspireCliChannel): - eng/clipack/Common.projitems propagates the value through the RID-specific pack pipeline. - eng/pipelines/templates/build_sign_native.yml threads it through the AzDO internal native-build template. - .github/workflows/build-cli-native-archives.yml does the same for GitHub Actions native archive builds. PackageChannelNames.Local ("local") is added as a constant so the rest of the CLI doesn't have to deal in raw strings. Tests: - AssemblyMetadataChannelTests asserts the running CLI assembly carries the AssemblyMetadata attribute and that its value is one of the known channels. - CliMetadataPackagingTests verifies the metadata survives `dotnet pack`. - ClipackPropagationTests pins that clipack RID-specific packs honor the AspireCliChannel override. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(cli): IdentityChannelReader sources channel from assembly metadata Wire the bootstrap so the running CLI's channel is sourced from the binary's own [AssemblyMetadata("AspireCliChannel", ...)] value rather than from any ambient configuration. This is the runtime half of the channel-baking story: the build pins the channel into the assembly, and the CLI reads its own identity at startup. - Acquisition/IdentityChannelReader.cs is the new component. It reads the AspireCliChannel metadata key from a caller-supplied Assembly. The ctor requires an explicit Assembly (no Assembly? = null default) so misuse is caught at construction time rather than via a cryptic "metadata missing on '?'" later. Production callers pass typeof(Aspire.Cli.Program).Assembly; tests pass a fake AssemblyBuilder assembly. - Program.BuildApplicationAsync registers IIdentityChannelReader in DI and threads its value into the new CliExecutionContext.Channel / .IdentityChannel / .PrNumber surface. - CliExecutionContext exposes: * Channel — the resolved hive label. For non-PR builds, identical to the identity channel verbatim. For PR builds (identity == "pr" with non-null PrNumber), resolves to "pr-<N>" — the directory name the packaging service uses, and the value reseed call sites write into a project's aspire.config.json#channel. * IdentityChannel — the raw build-time identity ("local"|"stable"| "staging"|"daily"|"pr"). Distinct from Channel because consumers that need the build-time taxonomy (PSM guards, version checks) should not see the per-PR refinement. * PrNumber — exposed verbatim. The constructor refuses channel="pr"+prNumber=null so a malformed PR build cannot construct. Tests: - IdentityChannelReaderTests covers the metadata-key contract and the real GitHub Actions InformationalVersion shapes (-pr.<N>.g<SHA>+<sha>) the parser must accept. - CliBootstrapTests asserts the production wiring resolves a known channel value when invoked against the real Aspire.Cli assembly. - CliExecutionContextTests exercises the Channel/IdentityChannel/PrNumber surface, including the malformed-PR-build constructor guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): rename GetPrHiveCount to GetHiveCount The original name suggested the count was specific to PR build hives, but the implementation has always returned the count of every subdirectory in the hives root — including the local hive and any other channel-specific hives a developer may have. The new name reflects what the method actually does. This is a mechanical rename of the method on CliExecutionContext plus its five call sites (AddCommand, IntegrationPackageSearchService, NewCommand, UpdateCommand, TemplateNuGetConfigService). Comments and the doc string are updated to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): drop IConfigurationService from app-host project factories The app-host project plumbing was reading the channel through IConfigurationService.GetConfigurationAsync("channel", ...) — a global lookup that returned whatever happened to be in the user's ambient aspire config. After the IdentityChannelReader work, the channel is available verbatim from CliExecutionContext.Channel, so the global read is redundant and removes a silent source of channel drift between sibling processes. Changes: - AppHostServerProjectFactory: drop the IConfigurationService dependency. The factory now takes CliExecutionContext directly and forwards it. - DotNetBasedAppHostServerProject: drop the unused _configurationService field and its constructor parameter. - GuestAppHostProject: add the CliExecutionContext dependency so the generated guest-app-host can pass the channel through into its own DI graph (channel-aware paths that previously asked the global config now ask the execution context). Tests: - AppHostServerProjectTests: drop the configurationService argument from the test factory; nothing else changes. - GuestAppHostProjectTests: pass a test CliExecutionContext through the constructor for parity with the production wiring. - CliTestHelper: factory now wires CliExecutionContext into the new constructor signatures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): PrebuiltAppHostServer resolves channel from per-project aspire.config.json When a prebuilt app-host runs against an existing project, the channel it should use is the one the project was scaffolded against — stored in the project's aspire.config.json#channel. The previous implementation consulted global identity-channel state, which could disagree with the per-project value when a developer ran multiple CLIs against the same project. Changes: - PrebuiltAppHostServer.ResolveChannelName: read only the per-project aspire.config.json. Return null when the file is absent or sets no channel; let downstream consumers decide what to do with that. - Add a PSM (per-source mapping) guard: when the running CLI's identity channel is "local" and the requested channel is also "local", suppress the temporary NuGet config generation so the project picks up the user's ambient NuGet sources instead of being pinned to the local hive's PSM. For every other identity channel (stable, staging, daily, pr) the guard does not fire and a per-channel NuGet config is generated as before. Tests: - PrebuiltAppHostServerChannelResolutionTests pins the per-project aspire.config.json#channel contract: null when absent, value when present. - PrebuiltAppHostServerTests adds the PSM-guard cross-product (identity channel × requested channel) so a regression that broadens the guard is caught. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): TemplateNuGetConfigService drops global-channel fallback; "local" override → implicit channel TemplateNuGetConfigService had three channel-resolving entry points (PromptToCreateOrUpdateNuGetConfigAsync, CreateOrUpdateNuGetConfigWithoutPromptAsync, ResolveTemplatePackageAsync) and each one — when the caller did not supply an explicit channel — fell back to reading ~/.aspire/aspire.config.json#channel through IConfigurationService. That made template resolution silently depend on whatever channel happened to be active for OTHER projects on the machine. The fix is to drop the IConfigurationService dependency entirely: the only channel inputs are now (1) the caller-supplied argument or (2) the implicit channel. The three entry points short-circuit on null/whitespace input instead of consulting a global source. Side effect — local-channel override: A locally-built CLI bakes channel="local" into its assembly metadata. On a clean machine without ~/.aspire/hives/local, PackagingService produces no "local" channel, and InitCommand forwards CliExecutionContext.Channel ("local") as the explicit ChannelOverride. Without resolver-level support this throws ChannelNotFoundException and `aspire init` is unusable on a clean machine. New behavior: a request for "local" with no matching channel resolves to the implicit channel — semantically a CLI with no local hive is just a CLI using the ambient NuGet configuration. The fallback is narrowly scoped to "local"; any other unrecognized channel name still fails loudly (typos like "stalbe" still surface as ChannelNotFoundException). Tests: - TemplateNuGetConfigServiceTests covers null/whitespace short-circuits for the two prompt-style entry points, the local→implicit fallback, and the negative case that other unrecognized channels still throw. - DotNetTemplateFactoryTests drops the now-removed IConfigurationService argument from CreateTemplateFactory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(cli): scaffolding reseeds channel into per-project aspire.config.json When `aspire init` or `aspire new` scaffolds a new project, write the CLI's resolved CliExecutionContext.Channel value into the project's aspire.config.json#channel field. This pins the per-project channel to the CLI that scaffolded it, so subsequent runs against the project resolve packages from the same hive — independent of any global channel setting that may have drifted. Changes: - ScaffoldingService gains channel-reseed entry points used by all starter-template factories. - CliTemplateFactory.{Go,Python,TypeScript}StarterTemplate forward CliExecutionContext.Channel into the reseed step after files are copied. - InitCommand and NewCommand pass CliExecutionContext.Channel into the scaffolding pipeline; they no longer derive the channel from global config. Tests: - ChannelReseedTests pins the per-channel reseed contract: each identity channel (local | stable | staging | daily | pr-N) ends up written verbatim into aspire.config.json#channel, including the pr-N resolution for PR-channel CLIs. - InitCommandTests adds the reseed coverage and updates the existing init flow expectations to match the new wiring (drops the now- removed FakeConfigurationServiceWithChannel helper and the two tests that exercised the deleted global-channel-fallback path). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(cli): PackageChannel enumerates flat-folder hives; VersionHelper recognizes "local" Two related additions that complete the local-channel runtime story. PackageChannel — flat-folder enumeration: For local hive channels the Aspire* source is a flat folder of .nupkg files (the layout created by ./build.sh --pack). `dotnet package search` does not support local folder sources and returns no results, so GetIntegrationPackagesAsync would yield an empty set for the local hive even when packages are present. New behavior: when PinnedVersion != null and the Aspire-filtered mapping points at an existing local directory, enumerate *.nupkg files directly and project them to NuGetPackage identities. The HTTP/search path is unchanged for non-local channels. VersionHelper.IsLocalBuildChannel — accept "local": The helper previously recognized only `pr-*` (PR hives) and `run-*` (workflow-run hives) as locally-built channels. With the baked AspireCliChannel value of "local" for ./build.sh builds, the helper now also returns true for the literal "local" channel, so version-check paths classify local-dev builds correctly alongside PR builds. Tests: - PackagingServiceTests adds direct coverage for the flat-folder enumeration path, including the corrupted-state shape (hive dir exists but `packages/` is empty) where PinnedVersion is null and enumeration must safely yield nothing. - VersionHelperTests pins the new "local" → true case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): get-aspire-cli-pr picks the correct hive label for local-dir installs The PR-install scripts (get-aspire-cli-pr.{sh,ps1}) installed CLI artifacts into a hive label that did not match what the running CLI's CliExecutionContext.Channel resolved to. The mismatch surfaced when the script ran against a local `--local-dir` source: artifacts landed under ~/.aspire/hives/<wrong-label>/, but the CLI looked under ~/.aspire/hives/pr-<N>/ (PR builds) or ~/.aspire/hives/local/ (local dev builds), so the freshly-installed packages were invisible. Resolution rule: - If the script's source nupkgs carry a PR-suffixed version (matching `pr.<N>.g<sha>`), the hive label is `pr-<N>`. - Otherwise the hive label is `local`. - Default falls back to `local` so `--local-dir` installs from local-dev builds (no PR suffix) work end-to-end without manual labeling. The release-side scripts (get-aspire-cli.{sh,ps1}) had divergent hive- labeling logic that the PR-script fix obsoletes; the release scripts now share the same three-branch resolution, removing ~340 lines of duplicated branch-handling. The polyglot-validation setup-local-cli.sh helper learns the same rule so CI's local-CLI smoke tests install into the same hive label the CLI will read from. Tests: - PRScriptShellTests / PRScriptPowerShellTests cover the three-branch resolution (pr-N from suffix, local fallback, explicit override). - ReleaseScriptShellTests / ReleaseScriptPowerShellTests mirror the pinning on the release-script side after the dedup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): aspire update stops writing global channel setting Self-update previously saved the chosen channel (or cleared it for "stable") into ~/.aspire/aspire.config.json#channel so subsequent `aspire new` and `aspire init` runs would pick it up via global config. With the per-project channel reseed landed earlier in this branch, the global setting is no longer a source of truth for anything — every scaffolded project carries its own channel — so the global write is dead code and a source of drift between projects on the same machine. Changes: - UpdateCommand.ExecuteSelfUpdateAsync no longer calls SetConfigurationAsync / DeleteConfigurationAsync for "channel". - The accompanying comment is rewritten to describe the current contract: the channel choice applies to this self-update only; subsequent scaffolding resolves channel per-project. - The legacy → new config migration drops the channel key during migration (the migrated global config never carries channel), matching the new "channel is not stored globally" invariant. The legacy file is still preserved verbatim for backward compatibility with older CLIs. Tests: - UpdateCommandTests asserts no global channel writes occur during self-update across all selectable channels. - ConfigMigrationTests (E2E) updates the migration assertion to expect the channel key absent in the migrated aspire.config.json and present in the preserved legacy file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli): cross-channel coverage for aspire add (local hive) Pin the `aspire add` hive-resolution behavior across the new local channel to match the existing PR-hive coverage: - AddCommand_WithLocalHive_PrefersCurrentCliVersion: with a populated ~/.aspire/hives/local/packages dir whose Aspire.Hosting pkg matches the running CLI's version, `aspire add` should pick that version without prompting (the local-hive equivalent of the existing WithPrHive coverage). This complements the PackageChannel flat-folder enumeration and the ChannelReseed coverage added earlier on this branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): bake `pr-<N>` into AspireCliChannel; drop runtime parse+join Previously the CLI carried two pieces of channel state: 1. [AssemblyMetadata("AspireCliChannel", "pr")] — the 5-value taxonomy (stable | staging | daily | pr | local). 2. AssemblyInformationalVersionAttribute (0.0.0-pr.<N>.g<sha>) — the version stamp, which was *separately* parsed at runtime by IdentityChannelReader.ParsePrNumber to extract <N>. CliExecutionContext then joined the two as `pr-<N>` for the hive label exposed via .Channel. The split existed only inside the CLI itself — every non-`pr` channel passed through verbatim, no external consumer relied on the `pr` literal, and the only other src/ reader of the raw taxonomy (PrebuiltAppHostServer.cs:423) only checked `== "local"` which is unchanged in either scheme. Collapse the two pieces. CI now bakes the resolved hive label directly: * GH Actions (.github/workflows/build-cli-native-archives.yml): pull_request -> pr-<github.event.pull_request.number> * AzDO (eng/pipelines/templates/build_sign_native.yml): Build.Reason == PullRequest -> pr-<System.PullRequest.PullRequestNumber>, with a digit-only regex pre-check that throws if the agent ever hands us an unresolved macro or non-numeric value (clearer failure attribution than waiting for the runtime IsValidChannel check). Runtime simplifications: * IdentityChannelReader.ParsePrNumber: deleted (~40 lines). * IdentityChannelReader.ResolveChannel: now validates shape via the new IsValidChannel helper. Accepts the four fixed strings (stable | staging | daily | local) or `pr-<digits>`. Rejects the legacy literal `pr` (no suffix) so a CI misconfiguration cannot silently mis-route packages. * CliExecutionContext: `prNumber` ctor parameter dropped; PrNumber and IdentityChannel properties dropped; Channel is now a plain auto-property. The `channel=='pr' && prNumber is null` throw guard goes away — the invariant is now structurally enforced by the channel string itself. * Program.cs: the `if (channel == "pr") { ParsePrNumber(...) }` block collapses to one line. PrebuiltAppHostServer.cs: the one IdentityChannel reference becomes Channel; behavior identical because the only check is `== "local"` and `local` is unchanged. Tests: * AssemblyMetadataChannelTests: smoke-tests the baked value against IsValidChannel (was: set-membership against the legacy 5 values). * IdentityChannelReaderTests: drops the ParsePrNumber theory entirely, adds the IsValidChannel truth table, and adds a Throws-based set covering invalid shapes (including the legacy `pr` literal as a regression guard for the CI change). * CliExecutionContextTests: rewritten as a thin getter test (the type is now a holder, not a join). * CliBootstrapTests: drops the IdentityChannel branch on the PrNumber assertion. * InitCommandTests, PrebuiltAppHostServerTests, ChannelReseedTests: builders drop the `prNumber:` argument; callers that previously passed `channel: "pr", prNumber: 12345` now pass `channel: "pr-12345"`. All 2881 Aspire.Cli.Tests pass; src/Aspire.Cli and tests build clean on net10.0/arm64. Out of scope: the shell-script PR-suffix detection in setup-local-cli.sh (commit 160801c) parses nupkg filenames stamped by VersionSuffix, not assembly metadata, so it is not affected by this change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli): cover picker & channel-resolution gaps on the channel branch Adds direct tests for behaviors the branch already implements but that were only covered transitively, so a regression here would surface in unrelated suites rather than naming the cause. - CliExecutionContextTests: 4 GetHiveCount() tests (missing dir → 0, empty dir → 0, populated count, ignores stray files). GetHiveCount() gates the channel picker in update, the channel sub-menu in add, and explicit-channel inclusion in IntegrationPackageSearchService — so a bug here previously manifested as cross-command flake. - UpdateCommandTests: three picker-contract tests. * WithHivesAndConfiguredChannel and WithHivesAndLocallyConfiguredChannel close the (hive-present × configured-channel) intersection — existing tests cover one axis at a time but not the intersection that PR-build users actually hit. * WithHives_PromptOffersChannelsInPackagingServiceOrder is the prompt-content contract: every existing prompt callback either flag-sets or .First()s the choices; nothing verified the names, order, or 'Name (SourceDetails)' label format. Reorderings or label drops would otherwise be silently green. - PrebuiltAppHostServerChannelResolutionTests: two migration-safety tests for the new aspire.config.json ?? legacy AspireJsonConfiguration fallback. Covers the legacy-only path (no new file present) and the precedence when both files exist with different channels — guards against an accidental ?? operand swap. - TemplateNuGetConfigServiceTests: three IncludePrHives matrix tests. The existing three resolver tests all pass IncludePrHives: false; the true row (the aspire new path) and the (true × no-hives-on-disk) AND-gate were uncovered. Adds: IncludePrHivesTrue_WithHivesPresent_AllChannelsParticipate, IncludePrHivesTrue_NoHivesOnDisk_RestrictsToImplicit, and IncludePrHivesFalse_IgnoresHivesEvenWhenPresent (the aspire init policy). Verified locally: 72/72 of the affected test classes pass; 56/56 unaffected NewCommandTests still pass. Three gaps from the audit are intentionally deferred: - NewCommand local-build channel bias and global-config-fallback removal (the audit's T1/G8) target ResolveCliTemplateVersionAsync, which only fires for TemplateRuntime.Cli templates. Reaching it via aspire-starter is impossible — it's TemplateRuntime.DotNet and takes a different resolution path. The clean fix is to extract the channel-selection block to an internal helper that takes (channels, executionContextChannel, configuredChannelName) and tests it as a pure function across all rows; that refactor is out of scope for a pure test-add commit. - A scaffold-then-resolve end-to-end round-trip (G5) is now reachable in principle via the new G6 read-side tests plus existing ChannelReseedTests on the write side, so the marginal value of a joint test is small. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli): simplify channel-test suite — dedupe, fold theories, drop reflection Eleven targeted test simplifications across the new channel-resolution suite. Net: 15 files touched, -284 lines, no coverage dropped (verified by case-by-case audit: every assertion in every removed/folded test still has at least one equivalent assertion remaining). 1. Delete CliMetadataPackagingTests.cs — its sole "metadata exists + non-empty" assertion is a strict subset of AssemblyMetadataChannelTests.AspireCliChannel_AssemblyMetadata_HasValidShape (IsValidChannel already rejects null/empty). 2. CliBootstrapTests: BuildApplication_LocallyBuiltCli_… and BuildApplication_CliExecutionContextChannel_… asserted the same thing. Keep one; extract GetBakedEntryAssemblyChannel() so the GetCustomAttributes<AssemblyMetadataAttribute>().Single(...) lookup isn't open-coded. 3. UpdateCommandTests: drop SelfUpdate_StableChannel_DoesNotDeleteGlobalChannel — the SelfUpdate_DoesNotWriteChannelToGlobalConfiguration theory's "update --self --channel stable" row already asserts no-delete (and no-set, so it's a strict superset). 4. AssemblyMetadataChannelTests: replace fragile CliAssembly_BakesChannel_AsLocal_MatchingCsprojDefault (would fail under /p:AspireCliChannel=stable CI builds) with Csproj_DeclaresAspireCliChannelDefault_AsLocal — inspects the csproj XML for the `<AspireCliChannel Condition="'$(AspireCliChannel)' == ''">local` element. Guards the same property (the declared default) without coupling to the build configuration of the test host. 5. AddCommandTests: extract RunAddRedisWithHiveScenarioAsync to share the workspace + prompter + project locator + AddPackage-capture scaffolding across the three hive tests (WithPrHive / WithLocalHive / WithLocalAndPrHives). Each test now declares only the hive layout and the SearchPackagesAsync mock. 6. ChannelReseedTests: NoExplicitChannel (4 InlineData rows) + ExplicitChannel (Fact) → one [Theory] with five rows over (contextChannel, explicitChannel, expected). Drop the CreateExecutionContext→BuildContext one-line wrapper and switch from Directory.CreateTempSubdirectory + try/finally to TemporaryWorkspace (project convention). 7. IdentityChannelReaderTests: the IsValidChannel_MatchesExpectedTruthTable theory exhaustively asserts the shape contract; the assembly-roundtrip tests only need to verify the wiring from ReadChannel through IsValidChannel. Reduce ReadChannel_AssemblyHasMetadataForValidChannel_ReturnsValue (6 rows) and ReadChannel_InvalidChannelValue_Throws (10 rows) to one representative [Fact] each; drop ReadChannel_AssemblyHasEmptyChannelMetadata_Throws (covered by the empty-string row in the truth table). 8. PrebuiltAppHostServer.ResolveChannelName: private → internal (the assembly already has [InternalsVisibleTo("Aspire.Cli.Tests")]). Drop reflection in PrebuiltAppHostServerChannelResolutionTests (4 sites) and PrebuiltAppHostServerTests (1 site). 9. CliExecutionContextTests: Channel_DefaultsToLocal_WhenNotSpecified now uses CreateContext(channel: null) instead of inlining the 5-line ctor that the helper already encapsulates. 10. Move Ctor_NullAssembly_ThrowsArgumentNullException from CliBootstrapTests to IdentityChannelReaderTests — it's about reader ctor validation, not bootstrap-wiring DI. 11. TemplateNuGetConfigServiceTests: fold three IncludePrHives Facts (TrueWithHives / TrueNoHives / FalseWithHives) into one [Theory] with three rows over (includePrHives, createHiveOnDisk, expectedVersion, expectImplicitChannel, expectedChannelName). Verified: build clean; 180 tests across the affected classes pass locally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(specs): correct bundle.md self-update flow after global-channel write removal The self-update process description listed a 7-step flow ending with "Saves selected channel to global settings". That step was removed across the v3 acquisition work — the CLI no longer writes the channel globally (acquisition scripts dropped save_global_settings; aspire update --self stopped persisting --channel). Identity is now baked into the binary via AspireCliChannel assembly metadata; per-project overrides live in aspire.config.json#channel. Drop step 7 and add a short note explaining the new model so docs match reality. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(cli): mark global-channel fallback transitional in UpdateCommand precedence Identity-channel is baked into the CLI via AspireCliChannel assembly metadata; the acquisition scripts and aspire update --self no longer seed a global "channel" field. The step-2 fallback in ConfigurationService.GetConfigurationFromDirectoryAsync still reads the global file, however, so aspire update's documented step-3 precedence ("global config 'channel'") is reachable. That's intentional for now — users who deliberately ran \`aspire config set -g channel <x>\` on the new CLI keep their preference honored by aspire update — but it's worth flagging at both the call site (UpdateCommand precedence comment) and the impl (ConfigurationService) so a future reader doesn't assume the global read is also dead. Add TODO markers calling out that the fallback can be removed once telemetry confirms negligible global-channel usage. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(acquisition): auto-detect raw-build vs tarball in --local-dir flow Cherry-picks the helper bodies from 721811c but reshapes the user surface: instead of a separate --local-binary flag, install_from_local_dir auto-detects from directory contents — tarball glob match → archive flow; recursive aspire-exe match → raw-build flow. Drops the LOCAL_BINARY=false variable, --local-binary arg parser, and requires-LocalDir guard from the original commit (no longer needed with auto-detect). Preserves bash/pwsh parity. Existing LocalDir_* tests pass unmodified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(acquisition): release scripts must not write global channel field PR1 invariant: install.sh / install.ps1 no longer write `~/.aspire/aspire.config.json#channel`. Adds Install_DryRun_DoesNotWriteGlobalChannelField to the bash and pwsh test classes, asserting both that stdout under --dry-run / -WhatIf contains no channel-write log line and that the global config file isn't created under MockHome. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): rename CLI identity & project-requested channel symbols Disambiguate the two distinct channel concepts that PrebuiltAppHostServer threads through restore: - CliExecutionContext.Channel -> IdentityChannel The hive label baked into the CLI binary via [AssemblyMetadata("AspireCliChannel", ...)]. Identity of the running CLI, not what any project asks for. - PrebuiltAppHostServer.ResolveChannelName() -> ResolveRequestedChannel() Reads aspire.config.json#channel (with legacy fallback). The project's request to restore — independent of which CLI is running. - Parameter / local `channelName` -> `requestedChannel` Across RestoreNuGetPackagesAsync, BuildIntegrationProjectAsync, TryCreateTemporaryNuGetConfigAsync, GetNuGetSourcesAsync. XML doc on IdentityChannel makes the identity-vs-request distinction explicit. No behaviour change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): PSM guard keys on resolved channel, not CLI identity PrebuiltAppHostServer.TryCreateTemporaryNuGetConfigAsync used to short-circuit package source mapping (PSM) emission when the running CLI's IdentityChannel was "local". That dropped PSM for legitimate scenarios: - locally-built CLI (IdentityChannel == "local") running an apphost whose aspire.config.json#channel asks for "pr-12345" / "daily" / "staging" — restore silently used the ambient NuGet config (PSM dropped), instead of the channel's package source mappings. The guard now keys on the resolved channel.Name. The local hive has no real mappings, so emitting PSM for it would constrain restore to nothing; for every other channel (stable, staging, daily, pr-*) PSM must emit so restore honours the channel's package source mappings — regardless of which CLI identity is running. Keying on channel.Name (rather than the input requestedChannel) is robust to alias/normalization in the channel lookup above the guard. Test matrix now covers the full identity x requested cross-product: - LocalIdentity_LocalRequested_ReturnsNull - LocalIdentity_PrRequested_EmitsConfig (regression case) - StableIdentity_StableRequested_EmitsConfig - StableIdentity_LocalRequested_ReturnsNull - DailyIdentity_DailyRequested_EmitsConfig - PrIdentity_DifferentPrRequested_EmitsConfig - LocalIdentity_StagingRequested_EmitsConfigWithGlobalPackagesFolder (also pins that ConfigureGlobalPackagesFolder propagates to the emitted nuget.config; staging is the only channel today with that setting) - LocalRequested_ReturnsNull_RegardlessOfIdentity [Theory x4] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): split TemplatePackageQuery, move init's local fallback to caller TemplatePackageQuery.ChannelOverride carried two concepts: - aspire init forwarded _executionContext.IdentityChannel (CLI identity). - aspire new forwarded inputs.Channel (--channel / user request). The resolver compensated with a "channelName == 'local' and not found -> fall back to implicit" branch. That rule is only semantically correct for the identity caller: an explicit `aspire new --channel local` on a machine without a local hive should surface a ChannelNotFoundException, not silently switch to ambient NuGet. Rename the field to RequestedChannel and move the fallback contract into InitCommand (the only caller that wants it). The resolver becomes a pure single-concept lookup — drops the special-case branch entirely. User-visible effects: - `aspire init` on a local-CLI without ~/.aspire/hives/local: still falls back to implicit (init catches ChannelNotFound and retries with RequestedChannel: null). - `aspire new --channel local` without the hive: now throws ChannelNotFoundException with the standard "Valid options are: ..." message instead of silently using ambient NuGet. Test coverage: - ResolveTemplatePackage_RequestedChannel_NotFound_Throws - ResolveTemplatePackage_RequestedChannel_Matches_ReturnsThatChannel - existing InitCommand_OnLocalChannelCli_WithNoLocalHive_FallsBackToImplicitChannel still passes (now exercises the fallback in InitCommand, not the resolver). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(cli): document dual-source contract on Template*NuGetConfigAsync The string? channelName parameter on PromptToCreateOrUpdateNuGetConfigAsync and CreateOrUpdateNuGetConfigWithoutPromptAsync is passed by: - EmptyTemplate / aspire new : inputs.Channel (project request) - InitCommand : CliExecutionContext.IdentityChannel (running CLI identity) The lookup against IPackagingService.GetChannelsAsync is name-equivalent for both, so the existing single-parameter shape stays. Update the XML docs to document the dual-source contract explicitly so future readers do not read the parameter name as request-only (the audit flagged this as a maintainability trap). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): `aspire update` no longer pins identity when channel is implicit GuestAppHostProject.UpdatePackagesAsync used to write `config.Channel = _executionContext.IdentityChannel` whenever the update's resolved PackageChannel was implicit. That silently propagated dev/PR-build CLI identities into projects: a developer running `aspire update` with a locally-built CLI would pin `channel: "local"` into the project's aspire.config.json, surfacing identity into a file the user expected to remain untouched. `aspire update` is a no-pin path: the user is updating packages, not initialising. Only write config.Channel when the update actually resolved an explicit channel (--channel, per-project pin, prompt selection). The scaffolding / build-time paths intentionally do auto-pin identity; those are first-write contracts and unaffected. The pre-existing UpdateCommand_WithoutHives_UsesImplicitChannelWithoutPrompting test used a mock TestProjectUpdater that bypassed GuestAppHostProject entirely, so this bug was unit-test-invisible. New regression test UpdatePackagesAsync_ImplicitChannel_DoesNotPinIdentityIntoConfig exercises the real path and was verified red before applying the fix (Actual: "pr-99999" before fix, null after). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(cli): clarify `IsStagingChannelEnabled` reads layered configuration The staging-channel gate in KnownFeatures.IsStagingChannelEnabled reads configuration["channel"] (env vars + command-line + per-project aspire.config.json#channel + global config), NOT CliExecutionContext.IdentityChannel. A CLI baked with AspireCliChannel=staging does NOT auto-enable staging in the packaging service unless the user has also set the configuration value explicitly (e.g. via `aspire config set channel staging` or `--channel staging`). This is by design: identity selects which hive directory the CLI ships in; feature gating goes through user-visible configuration. Audit flagged the asymmetry as undocumented; adding a <remarks> block so future maintainers do not read the gate as identity-driven. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(cli): `aspire run` no longer mutates `config.Channel` GuestAppHostProject.RunAsync and ScaffoldingService.ScaffoldGuestLanguageAsync each silently wrote config.Channel back to aspire.config.json after build / scaffold prepare. Both writes reduced to one of two cases: - The project already had a channel pinned -> buildResult.ChannelName / prepareResult.ChannelName matched it (both come from AspireConfigFile.Load(...)?.Channel) -> no-op file write. - The project had no channel pinned -> buildResult.ChannelName was null, falling through to `?? _executionContext.IdentityChannel` -> silent identity pin on every `aspire run`. Neither case is useful work. The C# apphost path (DotNetBasedAppHostServerProject) never writes config.Channel and has worked fine in this regard, so the asymmetry was branch-introduced (PR microsoft#16820's channel refactor), not load-bearing. After this commit: - `aspire run` is pure-read for config.Channel (guest + dotnet symmetric). - Scaffolding writes config.Channel exactly once, at seed time (ScaffoldingService:75) via the existing config.Save(...) at line 82. - `aspire update` writes config.Channel only when the user resolved an explicit channel (already pinned by the previous commit). Three writes total across the CLI, each tied to a concrete user-driven action. No silent pinning anywhere. New regression test RunAsync_DoesNotMutateConfigChannel (Theory x2: seededChannel="stable" and null) was verified red before applying the fix (both cases produced Actual: "pr-99999"; after fix both stay at the seeded value). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * style(cli): standardize channel-name comparisons Addresses PR microsoft#16820 reviewer feedback about inconsistent channel-name case sensitivity. Uses StringComparison.Ordinal for CLI channel names, including the PSM local-channel guard, because these channel values are baked/configured as well-known lowercase identifiers rather than free-form case-insensitive labels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): use StartsWith for hosting package match Addresses PR microsoft#16820 reviewer feedback asking whether the pinned local-hive package scan should use StartsWith instead of Contains. The filter is intended to keep Aspire.Hosting packages, so matching the package-id prefix avoids accidental substring matches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * style(cli): use channel constants in identity reader Addresses PR microsoft#16820 reviewer feedback to use PackageChannelNames constants for baked identity-channel validation. Keeps the fixed channel set centralized while preserving the existing pr-<N> prefix validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(acquisition): dry-run helpers return non-PR sentinel; fall back to local hive extract_version_suffix_from_packages (bash) / Get-VersionSuffixFromPackages (pwsh) short-circuit in --dry-run / -WhatIf and previously returned the literal 'pr.1234.a1b2c3d4'. The new --local-dir auto-detect logic at the call site feeds that value through the regex ^pr\.([0-9]+)\.[0-9a-g]+$ to derive a hive label, so the mock unconditionally forced hive_label='pr-1234' regardless of what was actually in --local-dir, making the intended 'local' fallback unreachable in dry-run. Return the non-PR-shaped sentinel 'local' instead, which the caller's regex intentionally does not match, so the dry-run path now behaves the same as the real-world 'no PR suffix detected' path: hive_label='local'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli): drop ClipackPropagationTests — coverage via AssemblyMetadataChannelTests The deleted tests asserted XML shape on eng/clipack/Common.projitems (MSBuild idiom: <MSBuild Targets="Publish" Properties="…@(AdditionalProperties)" />). That couples the test to a particular MSBuild construct: a behavior-preserving refactor (e.g. forwarding through a helper target or hand-built Properties= string) would fail the test, and an XML-valid but behaviorally-wrong refactor would still pass it. The downstream contract — "the baked CLI binary has AspireCliChannel in its AssemblyMetadata" — is covered by Aspire.Cli.Tests.AssemblyMetadataChannelTests, which reads the actual baked metadata and validates it against IdentityChannelReader.IsValidChannel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(acquisition): drop helper-name asserts and scrub PR1 narrative comments In ReleaseScript{Shell,PowerShell}Tests: 1. Drop Assert.DoesNotContain("save_global_settings" / "Save-GlobalSettings", …). Those are internal helper-function names; renaming the helper would break the test even though the contract still holds. The artifact-level check (File.Exists(globalConfig) == false) and the path-name check on dry-run output (DoesNotContain "aspire.config.json") already cover the real contract. 2. Replace "PR1 invariant" comments with statements of the invariant itself: install scripts must not write a global aspire.config.json — the channel is baked into the CLI binary at build time and read via IdentityChannelReader, so a global channel field would shadow the baked value. The string "PR1" encodes development-time PR sequencing and rots post-merge. 43/43 ReleaseScript{Shell,PowerShell} tests pass after the change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli/e2e): sharpen LocalHive comments as strategy-parametric design intent CLI E2E tests intentionally run against multiple CliInstallStrategy modes selected via CliInstallStrategy.Detect: LocalHive (local-built archive), InstallScript (downloaded by get-aspire-cli{,-pr}.{sh,ps1}), and pre-existing CLI on PATH. Only LocalHive returns a non-null channel from PrepareLocalChannel; the other strategies rely on the CLI's baked channel plus ambient NuGet feeds. The old comment ("For LocalHive runs, point the freshly-created project at the local channel…") read like a runtime env-sniff. Replace with a comment that names the strategy explicitly and contrasts LocalHive against the null-returning strategies, so readers see the if-branch as design intent rather than runtime hedging. Touched sites (no behavior change): - ChannelUpdateWorkflowTests.cs (1 site, replaced existing comment) - TypeScriptCodegenValidationTests.cs (1 site, added comment) - TypeScriptPolyglotTests.cs (4 sites: two channelArgument ternaries and two WriteLocalChannelSettings if-blocks, comment adapted per site) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(acquisition): tighten dry-run hive-label assertion to exact phrase In LocalDir_DryRun_WithGitHubRunIdEnvSet_UsesLocalHiveLabel and its PowerShell sibling LocalDir_WhatIf_WithGitHubRunIdEnvSet_UsesLocalHiveLabel, the existing assertion was too weak: Assert.Contains("local", result.Output, StringComparison.OrdinalIgnoreCase); The substring "local" appears in the fixture path (local-artifacts) and in neighboring log lines ("from local packages"), so the assertion passes even when the actual hive label is wrong (e.g. pr-<N> from a dry-run mock that collides with the auto-detect regex). Tighten to the exact phrase the script emits at install_aspire_cli / Resolve-HiveLabel: Assert.Contains("Using hive label: local", result.Output, StringComparison.Ordinal); The existing Assert.DoesNotContain("run-99999", …) guard is preserved — it protects a different regression (GITHUB_RUN_ID leaking into the hive label when --local-dir / -LocalDir is used). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(acquisition): --run-id-only flow is rejected with actionable guidance Without --pr-number / --hive-label, the script previously dropped packages into hives/run-<workflowRunId>/packages. With the v3 design, the installed CLI's CliExecutionContext.Channel is baked at build time from AspireCliChannel and is one of pr-<N>/staging/daily/local — never run-<id>. Packages installed into a run-<id> hive are therefore unreachable from the CLI. Reject early with an actionable error directing the user to pass --pr-number (preferred) or --hive-label matching the baked AspireCliChannel. This is the Option B path from the v3-pr1 build-infra review; Option A (querying the binary's baked channel) would require new CLI surface area. Tests: - PRScriptShellTests / PRScriptPowerShellTests: add regression guards asserting the rejection, plus tighten the --run-id-as-first-arg parsing tests to pair --run-id with --hive-label so they still verify positional parsing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(acquisition): polyglot-validation is PR-only and fails loud off-PR Two coupled changes that close a silent-failure path on push events: 1. .github/workflows/tests.yml: gate the polyglot_validation job on github.event_name == 'pull_request'. Mirrors cli_starter_validation_windows. ci.yml triggers tests.yml on push to main/release/** as well as PRs, but the polyglot validation flow has no way to compute a hive label that the non-PR CLI archive (channel=daily/staging) will read from. Also exempt polyglot_validation.result == 'skipped' from the non-PR failure gate in test_summary. 2. .github/workflows/polyglot-validation/setup-local-cli.sh: remove the silent fall-back to HIVE_LABEL=local when no PR suffix is detected on the built nupkgs and fail loud instead. This is now a guard rather than a real code path — the workflow guard above ensures the script only runs on PR events where the suffix is always present — but the loud error is important defense in depth if the guard ever regresses. Option A (deriving the hive label from the CLI binary's baked AspireCliChannel) would require new CLI surface area and is not pursued here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(cli): add fallback-removal regression for DotNetBasedAppHostServerProject + NewCommand Mirrors PrebuiltAppHostServerChannelResolutionTests to cover the remaining two readers from which PR1 removed the global-channel read fallback (IConfigurationService.GetConfigurationAsync("channel", ...)): - DotNetBasedAppHostServerProject — direct-instantiation behavioral guards: per-project aspire.config.json is honored, legacy AspireJsonConfiguration is honored when the new file is absent, the new format wins when both exist, and a null channel is returned when no per-project state is set. - NewCommand — tripwire IConfigurationService that throws on any GetConfigurationAsync/GetConfigurationFromDirectoryAsync call for the 'channel' key (mirrors InitCommand_DoesNotConsultGlobalConfigurationService\ ForChannelKey). Addresses the gap noted in PR1 review feedback that 'coverage relies on the existing tests still passing after the fallback delete' for these two readers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): channel-name comparisons are case-insensitive PR1 added several channel-name comparisons using StringComparison.Ordinal, which made user-supplied channel values (config, --channel, baked metadata read paths) reject mixed-case inputs. Three CI tests caught it: - KnownFeaturesTests.IsStagingChannelEnabled_IsCaseInsensitive_ForChannelValue - KnownFeaturesTests.IsStagingChannelEnabled_IsCaseInsensitive_ForUppercaseChannelValue - VersionHelperTests.IsLocalBuildChannel_RecognizesAllLocalChannelForms("LOCAL") Switch all channel-name matching to OrdinalIgnoreCase across the call sites that compare values against PackageChannelNames.* literals or PackageChannel records. IdentityChannelReader keeps Ordinal intentionally — it strictly validates the lowercase value baked into our own assembly metadata and uses case-sensitivity as a misconfiguration tripwire. Also addresses PR microsoft#16820 review comment r3228883227. * docs: document channel identity, supported names, and hives Adds a Channel Identity subsection in docs/specs/bundle.md covering the AspireCliChannel → [AssemblyMetadata] → IdentityChannelReader → CliExecutionContext.IdentityChannel pipeline, plus tables for the supported channel names and the hive types on disk. Adds a user-facing channel-names callout to docs/dogfooding-pull-requests.md so users know the valid values for aspire.config.json#channel and `aspire update --channel`, including the `local` hive used by developer builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(squad): log channel-docs session (livingston) Agent: livingston Files created: - .squad/decisions.md (merged inbox) - .squad/orchestration-log/2026-05-12T22-39-42Z-livingston.md - .squad/log/2026-05-12T22-39-42Z-channel-docs.md Decision inbox (4 items) merged and cleared. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix two regressions in `aspire update`
* Update no longer crashes when the AppHost's pinned Aspire.AppHost.Sdk
version cannot be resolved (e.g. moving between PR builds after the
hive was refreshed). UpdateCommand now calls the project locator in
TrustConfiguredPath mode, which skips MSBuild validation of the
configured AppHost path so ProjectUpdater can rewrite the SDK pin
via its existing fallback parser.
* Update no longer defaults to the Implicit ("daily") channel when a
PR-built CLI is run against an AppHost that has no per-project or
global channel pin. UpdateCommand now consults
ExecutionContext.IdentityChannel as a default before the prompt
fallback, restoring the pre-microsoft#16820 behavior without re-introducing
any global channel writes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Strip orphaned PR-hive sources from <packageSources> on update
When 'aspire update' merges a project's NuGet.config to a new channel, any
~/.aspire/hives/pr-<N>/packages entry that lives only in <packageSources>
(no corresponding <packageSourceMapping> element) used to survive the merge.
RemoveEmptyPackageSourceElements only cleans up <packageSource> mapping
entries that become empty during the merge, so a source that was never
mapped — or whose mapping was rewritten by an earlier merge — would linger
forever. As soon as the hive directory is replaced on disk (which now
happens routinely for refreshed PR builds), 'dotnet restore' fails with
NU1301: The local source '...' doesn't exist.
Add a final pass over <packageSources> that removes any safe-to-remove
source (the existing IsSourceSafeToRemove heuristic — paths under
.aspire/hives) that is not in sourcesInUse and not in the new channel's
RequiredSources.
Add two regression tests covering both shapes: original config with a
PR-hive source mapped (already cleaned by the empty-element pass; locked
down) and original config with the same PR-hive source listed but with no
mapping element (the actual failure mode).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix PR install PATH dedup and refuse PR-channel --self update
Two additional regressions that compounded the aspire update problems:
(D) get-aspire-cli-pr.{sh,ps1} appended a fresh PATH entry on every PR
install because the install path embeds the PR number
(/Users/midenn/.aspire/dogfood/pr-<N>/bin), so the literal-line dedup in
add_to_path never matched any prior PR install. Detect any existing
dogfood/pr-*/bin line and replace it in place when adding a new one;
do the same for the Windows user PATH registry entry.
(E) aspire update --self prompts {Stable, Daily, Staging} and routes
through _cliDownloader.DownloadLatestCliAsync. PR channels have no
cliDownloadBaseUrl, so a PR-built CLI silently moved the user off
the PR build whenever they ran --self. Refuse with a clear message
pointing at the acquisition script (the supported refresh path for
PR installs) when the running CLI's IdentityChannel starts with
'pr-'. An explicit --channel still opts out.
Tests:
- AddToPath_PrInstall_ReplacesExistingDogfoodPrLine
- AddToPath_PrInstall_WithNoPriorDogfoodLine_AppendsAsBefore
- AddToPath_NonPrInstall_AppendsAndDoesNotMatchDogfoodHeuristic
- UpdateCommand_SelfUpdate_WhenIdentityChannelIsPr_RefusesWithAcquisitionScriptHint
- UpdateCommand_SelfUpdate_WhenIdentityChannelIsPrAndExplicitChannelGiven_AllowsDownload
- UpdateCommand_SelfUpdate_WhenIdentityChannelIsDaily_AllowsDownload
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Revert PR-channel refusal on aspire update --self
Per user feedback: 'aspire update --self' should remain a valid
ejection mechanism for PR builds. The hard-coded Stable/Daily/Staging
prompt already lets a PR-built CLI move to a real channel; refusing
with an acquisition-script hint was unhelpful gatekeeping.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Persist resolved channel from `aspire new` (.NET scaffold)
The .NET starter and apphost templates went through DotNetTemplateFactory
without ever writing aspire.config.json#channel. The TypeScript starter
(CliTemplateFactory.TypeScriptStarterTemplate) and the empty-template /
init paths (ScaffoldingService) already mirrored the resolved channel
into the per-project config. The .NET path was an asymmetric gap.
Without a pin in the scaffolded project, `aspire update` skips its
local-config precedence step and falls through to either an interactive
prompt (when hives exist) or the Implicit/nuget.org channel — silently
moving a project scaffolded by a PR-built or daily CLI onto stable.
Mirror the TS pattern: when the resolved channel is Explicit (pr-<N>,
daily, staging, local), call AspireConfigFile.LoadOrCreate + Save with
the channel name. Implicit channels (stable/nuget.org) intentionally
stay unpinned so the user's ambient NuGet config governs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Extend channel pin to Python and Go starters
aspire-py-starter and aspire-go-starter previously declined to persist
the resolved channel into aspire.config.json, on the rationale that
PrebuiltAppHostServer aggregates package sources from every registered
channel when no pin exists. That rationale only held for a daily CLI —
on a PR-built CLI the next 'aspire update' falls through to a daily
prompt or the Implicit/nuget.org channel because the local-config step
in the channel-resolution precedence finds nothing.
Mirror the TypeScript starter / .NET starter pattern: when NewCommand
resolves an Explicit channel, write it to aspire.config.json#channel
before the SDK generation step. Implicit channels (stable/nuget.org)
remain unpinned.
Extend NewCommandTemplateConfigPersistenceTests to cover GoStarter and
PythonStarter across all three pin scenarios (identity unregistered,
identity matches, --channel overrides), and drop the now-obsolete
NonChannelPinningStarter_NeverPersistsChannel tripwire.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Skip persistent profile/PATH writes for PR installs
PR installs land under <prefix>/dogfood/pr-<N>/bin, a per-PR path used
for session-scoped dogfooding. Writing it into ~/.zshrc / ~/.bashrc (Unix)
or HKCU\Environment (Windows) silently demoted a developer's daily/stable
install on every new shell until they hunted down the stale entry.
Treat PR installs as session-only: update the current session PATH and
print the activation hint so the user can opt into persistence manually,
but leave shell profiles and the Windows user PATH untouched. Non-PR
routes through these same scripts (release channels, --install-prefix
overrides) keep the previous persistent-PATH behavior.
The earlier dogfood/pr-* dedup pass in add_to_path / Update-PathEnvironment
is no longer needed and has been removed; the obsolete add_to_path PR
dedup tests are replaced with a single test pinning the simpler append
contract.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Drop PR-N / date narrative from committed comments
Repo convention is that committed comments describe the current contract
as seen against origin/main; they should not reference internal PR-N
identifiers or branch-evolution narrative. Rewrite the affected block
headers in UpdateCommandTests.cs (and any matching narrative in
UpdateCommand.cs / ProjectLocator.cs found by audit) to describe what
the code / test group guards, in terms of contract rather than history.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Mitch Denny <midenn@orangecake.localdomain>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: mitchdenny <mitchdenny@users.noreply.github.com>
Co-authored-by: Mitch Denny <midenn@Mac.localdomain>
Summary
The CLI's channel identity lives in build-time assembly metadata and is read at startup, eliminating cross-install drift where one acquisition route could silently overwrite the channel another route had selected. The "which channel is this CLI?" question is answerable from the binary itself.
Channel identity is a single MSBuild property,
AspireCliChannel, emitted as[AssemblyMetadata("AspireCliChannel", ...)]onAspire.Cli. CI sets it per build kind:stablefor releases,stagingfor release branches,dailyfor main,pr-<N>for PR validation, andlocalfor./build.shoutput.IdentityChannelReaderreads the attribute at startup and populatesCliExecutionContext.IdentityChannel/CliExecutionContext.Channel. CI bakes the fullpr-<N>hive label directly, with a digit-only pre-check that fails the build if a build agent ever hands over an unresolved macro.User- and maintainer-facing changes
CLI behavior
aspire update --selfdoes not write~/.aspire/aspire.config.json#channel; the chosen channel applies only to the self-update, and subsequent scaffolding resolves channel per project.channelkey; the legacy file is preserved verbatim.channelkey —DotNetBasedAppHostServerProject,PrebuiltAppHostServer, andNewCommandresolve channel from per-project state or CLI flags.Per-project channel
aspire new(and the Python / TypeScript starter scaffolders) seed the chosen channel into the new project'saspire.config.json#channel. Per-project channel preference is the single source of truth post-scaffolding.TemplateNuGetConfigService: an explicitlocalchannel override with no local hive on disk falls back to the implicit (ambient NuGet) channel rather than failing.local(PackageChannelNames.Local = "local"), uniform across all acquisition routes.Packaging
PackageChannelenumerates flat-folder hives directly for local channels, sincedotnet package searchdoes not support local folder sources (this is the layout produced by./build.sh --pack).VersionHelper.IsLocalBuildChannelrecognizeslocal,pr-*, andrun-*.Install scripts
get-aspire-cli-pr.{sh,ps1}andget-aspire-cli.{sh,ps1}share a single three-branch hive-label resolution:pr-<N>from apr.<N>.g<sha>version suffix, elselocal.--local-binary/-LocalBinaryflag (paired with--local-dir/-LocalDir) installs a rawdotnet build/dotnet publishoutput tree directly, skipping the archive search-and-extract path. Useful for testing against an uncompressed build.--local-binarywithout--local-diris rejected.Testing
Aspire.Cli.TestsandAspire.Acquisition.Testscover assembly-metadata smoke,IdentityChannelReader, channel reseed,aspire updatenon-writes, config migration, flat-folder package enumeration, the PR/release install scripts' hive-label resolution, andaspire addcross-channel behavior. Test coverage asserts behavior directly on the constructors of the affected app-host project factories.