Repository navigation
Standardize NuGet restore and channel-update policy across AppHosts - #19763
Conversation
There was a problem hiding this comment.
Pull request overview
Unifies polyglot integration restore behavior across generated AppHost projects, including source resolution, closure generation, cache layout, and runtime configuration.
Changes:
- Adds shared C# project, restore-source, closure, and environment helpers.
- Updates prebuilt and repository-mode AppHost generation.
- Expands restore fingerprinting and project-reference tests.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs |
Tests restore configuration, closure outputs, and cache normalization. |
src/Aspire.Cli/Utils/CliPathHelper.cs |
Adds path and staging-cache helpers. |
src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs |
Adopts shared restore and closure infrastructure. |
src/Aspire.Cli/Projects/IntegrationRestoreSourceResolver.cs |
Centralizes channel and source resolution. |
src/Aspire.Cli/Projects/IntegrationClosureEnvironment.cs |
Centralizes runtime integration environment variables. |
src/Aspire.Cli/Projects/IntegrationClosureBuilder.cs |
Centralizes closure project generation and manifest reading. |
src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs |
Uses the shared C# project model. |
src/Aspire.Cli/Projects/CSharpProjectFile.cs |
Introduces a generated C# project model. |
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19763Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19763" |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs:750
- This only checks
PackageSourceMappings, but the no-channel path returns all explicit channel URLs inAdditionalSourceswhile leavingPackageSourceMappingsnull. If one of those channels contains a SAS/user-info URL, it is written into the long-livedIntegrationRestore.csprojasRestoreAdditionalProjectSources, and restore-cache skipping remains enabled. Include all effective sources in the credential check and keep the generated project/config ephemeral (or reject the ambiguous no-channel case) so credentials are never persisted.
var hasCredentialBearingRestoreSource = restoreSources.PackageSourceMappings?.Any(
static mapping => PackageSourceOverrideMappings.HasCredentialMaterial(mapping.Source)) == true;
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs:164
- This set now stores project paths, but
OrdinalIgnoreCasecollapses distinct paths such as/src/Foo/Foo.csprojand/src/foo/foo.csprojon case-sensitive Linux/macOS filesystems, silently omitting one validProjectReference. Use ordinal path identity here; the restore graph uses the same rule specifically to avoid dropping distinct paths (PrebuiltAppHostServer.cs:378-380).
var addedProjects = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
c9c2ea5 to
7ea840a
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🟡 Changes recommended
Case-sensitive identity comparisons can preserve overridden NuGet sources or credentials.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 12/12 changed files
- Comments generated: 1
- Review effort level: Balanced
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🔵 Needs a closer look
NuGet’s Unix $NAME variables are incorrectly converted into literal relative filesystem paths.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/Aspire.Cli/Packaging/NuGetConfigComposer.cs:251
- On macOS/Linux, NuGet supports
$NAMEenvironment variables in config values, butEnvironment.ExpandEnvironmentVariablesonly expands%NAME%. A valid source such as$PACKAGEHOME/packagestherefore remains relative here and is rewritten to<config-dir>/$PACKAGEHOME/packages, causing restore to use the wrong path. Expand NuGet's platform-specific syntax (or use NuGet.Configuration to obtain the effective value) before resolving relative paths.
- Files reviewed: 12/12 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Update the JSON SDK version independently of channel-name persistence. Explicit stable updates clear previous channel pins while implicit updates preserve the existing selection. Cover successful updates, metadata-only repairs, decline, restore rollback, and channel-removal display. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d38799a7-a99c-48c5-b45b-9d6594dd1830
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🔵 Needs a closer look
The 189-file, restore- and security-sensitive change needs final human review and a successful current-head CI run.
0 open findings
Files not reviewed (2)
- src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs: Generated file
- src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs: Generated file
🧠 Review effort: Balanced
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
Give no-op fixtures matching SDK metadata and expect accepted stable updates to clear prior channel pins. Update C# E2E preview assertions and verify declining leaves the complete configuration unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d38799a7-a99c-48c5-b45b-9d6594dd1830
There was a problem hiding this comment.
🔵 Needs a closer look
The broad security-sensitive restore, credential, persistence, and rollback changes require final human validation.
0 open findings
Files not reviewed (2)
- src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs: Generated file
- src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs: Generated file
🧠 Review effort: Balanced
Merge origin/main at 517d566, preserving shared NuGet restore policy and AppHost workload identity while incorporating npm hosting integrations. Adapt package-entry and modern AppHost fixtures, and verify the combined npm, NuGet, and project-reference closure. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d38799a7-a99c-48c5-b45b-9d6594dd1830
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🟡 Changes recommended
Relative template source overrides are resolved from the generated overlay directory, breaking local --source installation.
1 open finding
Files not reviewed (2)
- src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs: Generated file
- src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs: Generated file
🧠 Review effort: Balanced
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
Merge origin/main at 9cc6730. Preserve native restore-policy projection and transactional updates while integrating package-only AppHosts and imported or conditional SDK declarations. Track imported files for rollback, avoid acquiring the deprecated SDK for package-only AppHosts, and adapt upstream fixtures to source eligibility filtering. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d38799a7-a99c-48c5-b45b-9d6594dd1830
There was a problem hiding this comment.
🔵 Needs a closer look
Cross-cutting restore policy, configuration inheritance, credential handling, and update transactions require final human review.
0 open findings
1 resolved since last review
Files not reviewed (2)
- src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs: Generated file
- src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs: Generated file
🧠 Review effort: Balanced
Merge origin/main at 4096792. Preserve the repository guards and read-only workflow permissions while retaining actions:read across the specialized test workflow chain for artifact-backed acquisition tests. Add structural permission coverage for the reusable-workflow boundaries. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d38799a7-a99c-48c5-b45b-9d6594dd1830
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🔵 Needs a closer look
Broad changes to restore, configuration persistence, and authentication handling require final human review and approval.
0 open findings
Files not reviewed (2)
- src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs: Generated file
- src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs: Generated file
🧠 Review effort: Balanced
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
/deployment-test |
|
🚀 Deployment tests starting on PR #19763... This will deploy to real Azure infrastructure. Results will be posted here when complete. |
PR #19763 readiness assessmentRecommendationConditionally merge-ready for the PR-specific restore and channel-policy changes, pending completion of macOS CLI CI and required approval. The targeted scenarios below found no new blocking regression in mixed polyglot restore, accepted channel transitions, or persisted-file rollback. This is a focused readiness assessment, not a complete line-by-line review or a blanket statement that every CLI scenario works. Two real failures were observed: accepted file-based C# updates fail while trying to edit a synthetic project path, and failed TypeScript updates return exit code zero. Both behaviors follow logic already present in the local PR and artifact identity
The normal PR installer was used without changing HOME, USERPROFILE, APPDATA, or the user's normal CLI installation: iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19763 -InstallPath '$root' -SkipExtension -SkipPath"
& $cli --version
gh pr view 19763 --repo microsoft/aspire --json headRefOid --jq .headRefOidResult: Installation passed; installed version matched the current PR commit. The head was rechecked after execution and remained unchanged. Changes analyzedRelevant production surfaces are package-only in-process NuGet restore, SDK-backed mixed package/project restore, source mappings and configuration hierarchy, generated polyglot SDKs, C# transactional channel updates, and polyglot channel metadata persistence. The PR also changes CLI templates, tests, GitHub Actions test runners, and MSBuild CI test properties. It does not change the dashboard, client integrations, or hosting-library APIs in the reviewed diff. Local scenariosIn the commands below, 1. Cold package-only restore with custom mappings — PASSEDCreated a fresh TypeScript AppHost: & $cli new aspire-empty --language typescript --name MixedReadiness --output "$root\MixedReadiness" --source $hive --version 17.0.0-pr.19763.g24e280b7 --localhost-tld false --suppress-agent-init --non-interactive
& $cli add Aspire.Hosting.Redis --version 17.0.0-pr.19763.g24e280b7 --source $hive --apphost apphost.mts --non-interactiveThe user-owned test NuGet configuration cleared inherited sources and mappings, mapped Result: Both commands exited 0. The Redis reference was persisted and its API generated using a fresh package cache. Evidence: 2. Cold mixed package/project restore and runtime — PASSEDAdded & $cli restore --apphost apphost.mts --non-interactive
& $cli start --apphost apphost.mts --isolated --non-interactive --format Json
& $cli wait readiness-worker --status up --timeout 120 --apphost apphost.mts --non-interactive
& $cli describe --apphost apphost.mts --format Json --non-interactive
& $cli stop --apphost apphost.mts --non-interactiveResult: Restore, start, wait, and stop exited 0. Generated SDK contained both This tests the SDK-backed integration graph, custom API generation, assembly loading, RPC invocation, resource execution, and exact-target shutdown—not just compilation. Evidence: 3. Accepted TypeScript PR → stable → PR round-trip — PASSEDCreated a separate TypeScript AppHost pinned to & $cli update --channel stable --yes --non-interactive --apphost apphost.mts
& $cli restore --apphost apphost.mts --non-interactive
& $cli update --channel pr-19763 --yes --non-interactive --apphost apphost.mts
& $cli restore --apphost apphost.mts --non-interactiveA different empty package directory was selected before every command. Result: All four commands exited 0. Stable selected SDK Evidence: 4. Accepted project-based C# PR → stable → PR round-trip — PASSEDThe fixture uses an ordinary & $cli update --channel stable --yes --non-interactive --apphost ChannelReadiness.csproj
dotnet restore ChannelReadiness.csproj --verbosity quiet
& $cli update --channel pr-19763 --yes --non-interactive --nuget-config-dir . --apphost ChannelReadiness.csproj
dotnet restore ChannelReadiness.csproj --verbosity quietA different empty package directory was selected before every command. Result: All four commands exited 0. Project package version, SDK metadata, channel metadata, and persisted NuGet policy moved together. Stable used Evidence: 5. C# candidate restore failure and rollback — PASSEDAfter returning to PR, added a fixture-only MSBuild target that rejects restore when the package version changes away from the PR version. This permits discovery and package editing, but fails validation of the updated stable candidate. & $cli update --channel stable --yes --non-interactive --apphost ChannelReadiness.csprojExpected: Nonzero exit; project, NuGet configuration, and Aspire metadata restored to their original bytes. Result: Exit 13 with an explicit restore failure. All three owned files were byte-for-byte unchanged. After removing only the deliberately injected failure target, native restore with another empty cache succeeded. Evidence: 6. TypeScript regeneration failure — PERSISTENCE PASSED; EXIT CONTRACT FAILEDAdded a more-specific ambient mapping for & $cli update --channel stable --yes --non-interactive --apphost apphost.mtsExpected: Explicit failure, nonzero exit, and unchanged persisted SDK/channel configuration. Result: The CLI reported failure to restore After removing only the deliberately blocking mapping, a cold ordinary restore succeeded. Attribution: The existing Code: Evidence: 7. Mixed-graph add with an unavailable project dependency — PASSEDAdded a fixture-only nonexistent package reference to the project integration, then attempted to add PostgreSQL from the PR hive using another empty cache: & $cli add Aspire.Hosting.PostgreSQL --version 17.0.0-pr.19763.g24e280b7 --source $hive --apphost apphost.mts --non-interactiveExpected: Graph restore fails clearly; PostgreSQL is not partially persisted. Result: Exit 5 with the missing dependency and applicable source-mapping diagnostic. The persisted package list was byte-for-byte unchanged. Removing the injected missing reference and restoring with another empty cache succeeded. Evidence: 8. Accepted file-based C# update — FAILED, PRE-EXISTING LOGICThe unmodified C# empty template generated by the PR CLI uses & $cli restore --apphost apphost.cs --non-interactive
& $cli update --apphost apphost.cs --channel stable --yes --non-interactiveResult: Initial restore exited 0. The accepted update exited 1: MSBuild reports The synthetic metadata was reproduced with both the default .NET 11 RC CLI and a direct invocation of the installed .NET 10.0.402 CLI assembly. The defining-file selection and imported-file update logic are already present in the local Impact: Successful accepted updates of file-based C# AppHosts were not validated. The project-based C# success above is not a substitute for that scenario. Evidence: Current-head CI evidenceMain run: https://github.com/microsoft/aspire/actions/runs/38631694503 Completed job logs were downloaded directly rather than relying only on green check names.
The downloaded language and test logs are retained in the session artifacts directory. CI infrastructure validation
Remaining hairy areas and limits
Final dispositionNo newly introduced merge-blocking regression was established by this targeted assessment. The strongest PR-specific boundaries passed: cold mixed restore plus execution; accepted TypeScript and project-based C# channel round-trips; ordinary cold restores after persistence; and rollback following candidate failure. This supports merging the PR once required CI and review complete, while explicitly recording the two pre-existing failures. It does not support describing all local scenarios as passing. Artifacts and cleanup
|
Map file-based MSBuild defining-project identities back to the source AppHost while preserving real imported declarations and rollback. Report SDK regeneration failures through the existing nonzero update error path before migrations or CLI self-update. Add native metadata, package editing, restore, rollback, and command-exit coverage with the agreed localized failure message. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d38799a7-a99c-48c5-b45b-9d6594dd1830
Tests selectorSelects the full PR test matrix + all PR-gated jobs (ALL) — a rule matching '.github/workflows/run-tests.yml' selects ALL Selection computed for commit |
There was a problem hiding this comment.
🔵 Needs a closer look
Broad restore and configuration changes require human approval; validation was static, with builds and tests not run.
0 open findings
Files not reviewed (2)
- src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs: Generated file
- src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs: Generated file
🧠 Review effort: Balanced
CI confidence report🔴 BLOCKED The required validation gaps below still block confidence. Failing PR/CI
🔴 Ordinary regression and platform coverage — Three attempt-1 test-job failures remain unclassified despite the successful retry.
Coverage not demonstrated🔴 Deployment E2E final-tree coverage — Those named deployment consumers directly exercise changed CLI template and AppHost behavior, but the available run is stale and does not validate the audited tree.
Audited commit: 0ef87afe Coverage catalogThe items below show the affected workflows, tests, artifacts and other validation evidence. Workflows and pipelines🔴 1. GitHub execution and coverage — A workflow or test-lane gap remains.
🟢 1a. Affected workflow final-definition execution — Verified
🟢 run-tests.yml final-definition execution
🟢 specialized-test-runner.yml final-definition execution
🟢 tests-outerloop.yml final-definition execution
🟢 tests-quarantine.yml final-definition execution
🟢 1b. Outerloop scenario coverage — Verified
🟢 Outerloop consumer coverage
🟢 1c. Quarantine scenario coverage — Verified
🟢 Quarantine consumer coverage
🔴 1d. Deployment E2E coverage — Validation not demonstrated
⚪ 2. Internal Azure pipeline/stage execution — Not applicable
🟢 3. CI selection/result gates — Verified
🟢 Runsheet selection and result gates
Functional and platform coverage🔴 4. Ordinary functional/regression/platform coverage — Validation not demonstrated
Produced outputs and artifacts⚪ 5. Artifact/release-consumer integrity — Not applicable
Static checks and dependency safety🟢 6. Lint/static analysis/generated consistency — Verified
🟢 ShellCheck🟢 PSScriptAnalyzer
🟢 Actionlint🟢 Zizmor⚪ 7. Dependency/action/task/tool version safety — Not applicable
Security and agent behavior🟢 8. Security/trust-boundary review — Verified
🟢 Changed workflow trust boundaries
⚪ 9. Representative agent behavior — Not applicable
|
Ankit Jain (@radical) I think your analyzer is raising false negatives due to focusing on single commits. The deployment tests were run on the previous commit and didn't show any issues related to this PR (there were failures, but all due to deployment flakiness, not restore issues). The logs are also available for the three failed attempt 1 jobs; they were flakey tests unrelated to this change. |

Description
Polyglot integration restore, package/template discovery, and C# channel updates currently compose NuGet policy in different ways. This can make the packages offered by discovery disagree with what can be restored, change configuration behavior when an integration project reference is added, or leave persisted AppHost configuration inconsistent with an update.
This PR shares NuGet configuration composition across those paths. Aspire-selected sources and mappings are applied alongside NuGet's native configuration hierarchy, reusing existing source aliases and inheriting their authentication and transport settings. Package and project integrations resolve together, discovery checks source eligibility, and explicit C# channel updates preview configuration changes before restoring and persisting them.
Polyglot integration restore
IntegrationRestore.csprojcontaining the direct integration packages and projects. NuGet resolves their shared dependency graph and reports incompatible requirements during restore.Configuration discovery is anchored at the AppHost rather than the generated cache directory. Both restore implementations use the same selected SDK, channel, source override, and evaluated NuGet settings. Overlays express the selected source/mapping policy; operations requiring a separate package cache use one instead of the ordinary global-packages folder.
Referenced projects continue to discover configuration from their own directories. Opt-in MSBuild properties provide hosting-version and credential-free source hints without automatically replacing the projects' restore configuration.
Integration and template discovery
Bundled and SDK-driven discovery check effective package-source mappings before offering ordinary search or exact-version results. Bundled discovery filters before cross-source deduplication so an ineligible feed's higher version cannot hide an eligible result.
An explicit
--sourcerestricts discovery and template installation to that source while retaining an existing selected alias's NuGet settings. Dependency restore has a different scope: after polyglotaspire addselects a package, its package ID is mapped to the explicit source, but other eligible sources remain available for dependencies. The selected integration feed is not assumed to contain every dependency.C# AppHost updates
Unqualified updates preserve the current native NuGet policy rather than selecting replacement feeds from CLI or Aspire channel metadata. Explicit channel updates compose a configuration candidate and show it through the existing NuGet preview and approval flow. Project/package edits are staged, the updated graph is restored through native MSBuild, and configuration and metadata changes are persisted only after restore succeeds. Handled failures roll back owned file edits.
An unresolved AppHost SDK can be repaired before strict restore-policy validation, without allowing unknown or overridden policy to be persisted. Fallback project parsing also normalizes MSBuild project-reference separators across platforms.
When an existing mapping policy is Aspire-only, the candidate can add
*mappings for enabled, previously unmapped ambient feeds so unrelated dependencies remain eligible. These mapping changes appear in the same approval; inherited feed URLs and credentials are not copied into the local file. Unrelated mapping restrictions are retained. Explicit stable selection returns to existing ambient feeds without adding NuGet.org; an explicitly supplied service-index override remains a separate source selection.Known trade-offs
aspire adddoes not persist per-integration channel or source provenance. A package selected from another channel or an explicit source can restore for that invocation but require a project-wide channel change or user-owned NuGet configuration for a later cold restore.NUGET_FALLBACK_PACKAGES. Manifest asset resolution across those folders remains tracked by Support NuGet fallback package folders in polyglot package restores #20631.NuGet configuration and diagnostics
NuGet remains responsible for evaluating configuration and handling authentication and trust settings. Overlays reuse existing source aliases and leave those settings in their original hierarchy rather than copying them into generated configuration.
Credential-bearing explicit
--sourcevalues are rejected. For configured sources containing inline credentials, diagnostic handling suppresses raw invocation logging and applies best-effort redaction using known URL spellings and credential components. This is not a general-purpose credential scrubber. Generated NuGet artifacts are not scrubbed and can still contain configured source URLs; credential providers and credential sections remain preferred over inline URL credentials.Related: #20631 tracks the remaining fallback-package-folder limitation.
Checklist
<remarks />and<code />elements on your triple slash comments?