Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
24400c8
Support configurable named integration sources
JamesNK Oct 28, 2026
ac257c2
Add E2E coverage for local integration source configuration
JamesNK Oct 29, 2026
cb26f64
Adapt integration source persistence to current NuGet configuration APIs
JamesNK Oct 31, 2026
1c9f17e
Preserve configured integration feeds during NuGet updates
JamesNK Nov 2, 2026
d610725
Use configured integration source keys for NuGet feeds
JamesNK Nov 3, 2026
1e13f41
Polish configuration source UX and share NuGet targeting
JamesNK Nov 3, 2026
ca94145
Render source trust warnings in yellow
JamesNK Nov 3, 2026
829cdf7
Scope integration searches and log discovery diagnostics
JamesNK Nov 3, 2026
b56e210
Validate explicitly configured default integration patterns
JamesNK Nov 3, 2026
b7dc18b
Reuse existing integration feed aliases safely
JamesNK Nov 3, 2026
e577c86
Use shared discovery for MCP integration listings
JamesNK Nov 3, 2026
6ae9d5e
Bound NuGet discovery pagination and report incomplete results
JamesNK Nov 3, 2026
7f5fca4
Fix named integration feed switching
JamesNK Nov 4, 2026
8ed7c81
Correct NuGet search warning localization note
JamesNK Nov 4, 2026
10f4017
Skip local-hive setup for named integration sources
JamesNK Nov 4, 2026
5ec6ab8
Fix named-source CLI E2E expectations
JamesNK Nov 4, 2026
19d98d9
Fix pagination test subset assumptions
JamesNK Nov 4, 2026
facba88
Roll back named integration configuration when adding fails
JamesNK Nov 4, 2026
82485d0
Preserve unrelated integration mappings for exact source overrides
JamesNK Nov 4, 2026
706067e
Warn on failed integration feeds and require explicit package selection
JamesNK Nov 4, 2026
f240f61
Fix configurable integration-source CI failures
JamesNK Nov 4, 2026
3345627
Isolate unavailable local integration feeds during discovery
JamesNK Nov 4, 2026
d61bfb4
Remove obsolete SDK mappings from retained integration feeds
JamesNK Nov 4, 2026
8153568
Designers
JamesNK Nov 4, 2026
368217a
Safeguard private feed routing and preserve E2E dependency sources
JamesNK Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/specs/cli-config-sources-1p-scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ The relevant excerpt from the project's file is:

This is an excerpt, not a replacement for the generated file. Maya keeps its AppHost, SDK, package, and other project-owned settings.

Only [allowlisted top-level settings](cli-config-sources.md#materialize-an-allowlist-into-the-app-configuration) are copied, not the entire source JSON. Currently only `skillSources` is allowed, and it is copied in full when supplied. Other source values, including `docs`, AppHost paths, SDK/channel selection, packages, and arbitrary feature settings, are not imported.
Only [allowlisted top-level settings](cli-config-sources.md#materialize-an-allowlist-into-the-app-configuration) are copied, not the entire source JSON. `skillSources` and `integrationSources` are allowed, and each is copied in full when supplied. Other source values, including `docs`, AppHost paths, SDK/channel selection, packages, and arbitrary feature settings, are not imported.

Maya commits the resulting app file with the application. It contains neither Git credentials nor an absolute tracking path, so another machine can use the saved values immediately.

Expand Down
51 changes: 48 additions & 3 deletions docs/specs/cli-config-sources.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/specs/polyglot-integration-restore.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ When a stable project with custom NuGet configuration moves to a mapping-based s

Moving back to stable removes the source-specific Aspire policy. The project's ambient Aspire mappings become effective again without requiring the user to reconstruct their NuGet configuration.

Feeds explicitly configured in `integrationSources` are exempt from automatic channel cleanup, including their package-source mappings. Protection matches normalized feed locations across aliases and remains active when discovery is disabled. With ambient mapping disabled, channel overlays retain those feeds' eligibility for Aspire packages as well as unrelated dependencies. The implicit `default` entry without a source does not exempt ambient feeds. Removing an explicit source ends its protection without proving that the feed is unused. Invocation-level source overrides and exact package selections can still replace the selected routing. These rules apply to both temporary restore policy and .NET persistence; they do not grant ownership of user or inherited mappings. Adding a named integration may reuse an already sufficient user mapping or append an exact package ID to a local mapping, but it does not replace inherited entries or authorize later channel cleanup of user patterns. Changes to the protected feeds participate in policy cache identities and update-transaction validation.

The same transition must also succeed when the project has no `NuGet.Config`. In that case, stable restore uses normal NuGet defaults, staging restore introduces the staging source through the higher-precedence Aspire policy overlay, and returning to stable removes that overlay contribution.

A project configuration is valid for stable restore when its effective sources and package-source mappings can resolve the requested stable Aspire packages and their dependencies. A repository may therefore clear default sources and use an internal mirror without changing the channel-transition model.
Expand Down
1 change: 1 addition & 0 deletions eng/github-ci/test-trigger-map.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ ignore:
# and are routed to their tests in path_rules above.)
- eng/scripts/debug-*.ps1 # debug-aspire-channel/stable/staging: local channel-switch helpers
- eng/scripts/debug-*.sh
- eng/scripts/config-sources-demo/** # manual demo launchers; no GH-CI consumer
- eng/scripts/validate-cli-symbols.ps1 # AzDO native-AOT symbol publish validation; no GH-CI consumer
- eng/scripts/validate-mac-platform-sso.sh # manual managed-Mac validation; no GH-CI consumer
- eng/generate-catalog.ps1 # Windows/AzDO template catalog generation; no GH-CI consumer
Expand Down
86 changes: 86 additions & 0 deletions eng/scripts/config-sources-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Configuration-source demo

These PowerShell scripts build and run the CLI from this checkout with the demo
configuration source. They emulate the current stable CLI version and use
published packages, so a local package hive is not required.

## Prerequisites

- On Windows, run `.\restore.cmd` from the repository root to set up the checkout's
pinned .NET SDK.
- Have Git installed and authenticated with access to the configuration repository
specified in [run-aspire-new.ps1](run-aspire-new.ps1).
- If a template or integration uses a private NuGet feed, authenticate to that feed
separately using your normal NuGet credential setup. Git authentication does not
grant access to package feeds.
- Install any SDKs or other prerequisites required by the selected template.

Both scripts build the CLI using the checkout's SDK, while child `dotnet`
commands honor the generated app's SDK pin. Environment overrides and the
caller's working directory are restored when the scripts finish.

## Create an AppHost in a specified directory

Run the following from the repository root, changing `$appHostDirectory` to your
desired location:

```powershell
$demoScripts = Join-Path $PWD.Path 'eng\scripts\config-sources-demo'
$appHostDirectory = 'C:\Temp\ConfigSourcesDemo'

& (Join-Path $demoScripts 'run-aspire-new.ps1') `
-WorkingDirectory $appHostDirectory `
-CommandArguments @('--output', '.')
```

The script creates the working directory if necessary. `--output .` creates the
app in that directory rather than a nested directory. Choose the template,
language, and app name when prompted. Accept the configuration source when
prompted to apply the demo settings.

`-CommandArguments` forwards an array of arguments to `aspire new`. In the template picker, choose
**Search for more templates** and leave the search empty to list custom templates.

To use an automatically created temporary directory and the interactive prompts:

```powershell
& (Join-Path $demoScripts 'run-aspire-new.ps1')
```

The script prints the directory as `Demo workspace: ...`.

## Add an integration to the created AppHost

Use the launcher below rather than a bare `aspire add`. The launcher runs this
checkout's CLI; a bare command uses the installed CLI on `PATH`, which may not
include the configuration-source demo changes.

Using the same variables from the creation example, open the integration picker:

```powershell
& (Join-Path $demoScripts 'run-aspire-add.ps1') `
-WorkingDirectory $appHostDirectory
```

Or specify an integration and optional CLI arguments:

```powershell
& (Join-Path $demoScripts 'run-aspire-add.ps1') `
-WorkingDirectory $appHostDirectory redis
```

Arguments after the script parameters are forwarded to `aspire add`, including
options such as `--version 1.0.0` and `--non-interactive`. Without
`-WorkingDirectory`, the add script uses the caller's current directory:

```powershell
Push-Location -LiteralPath $appHostDirectory
try
{
& (Join-Path $demoScripts 'run-aspire-add.ps1')
}
finally
{
Pop-Location
}
```
23 changes: 23 additions & 0 deletions eng/scripts/config-sources-demo/run-aspire-add.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<#
.SYNOPSIS
Runs aspire add from this checkout in the caller's current directory.
.DESCRIPTION
Builds and runs the CLI from source using the configuration-source demo settings
and published packages. Additional arguments are forwarded to aspire add.
.EXAMPLE
& 'C:\Development\Source\aspire\eng\scripts\config-sources-demo\run-aspire-add.ps1'
.EXAMPLE
& 'C:\Development\Source\aspire\eng\scripts\config-sources-demo\run-aspire-add.ps1' redis
.EXAMPLE
& 'C:\Development\Source\aspire\eng\scripts\config-sources-demo\run-aspire-add.ps1' Contoso.Hosting.LocalSource --version 1.0.0 --non-interactive
#>
[CmdletBinding(PositionalBinding = $false)]
param(
[string] $WorkingDirectory = (Get-Location).Path,

[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
[string[]] $Arguments = @()
)

& (Join-Path $PSScriptRoot 'run-aspire-new.ps1') `
-WorkingDirectory $WorkingDirectory -Command add -CommandArguments $Arguments
97 changes: 97 additions & 0 deletions eng/scripts/config-sources-demo/run-aspire-new.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<#
.SYNOPSIS
Runs aspire new from this checkout with the configuration-source demo settings.
.DESCRIPTION
Uses published packages rather than requiring a local package hive. In the template
picker, choose "Search for more templates" and leave the search empty to list custom templates.
Defaults to aspire new. The aspire add launcher reuses this setup with the caller's working directory.
Uses the checkout's pinned SDK to build the CLI, even when the demo app pins a different SDK.
Runs the built CLI directly so child dotnet commands honor the demo app's SDK pin.
.EXAMPLE
.\eng\scripts\config-sources-demo\run-aspire-new.ps1
.EXAMPLE
& 'C:\Development\Source\aspire\eng\scripts\config-sources-demo\run-aspire-new.ps1' -WorkingDirectory $PWD.Path
#>
[CmdletBinding()]
param(
[string] $WorkingDirectory = (Join-Path ([System.IO.Path]::GetTempPath()) "aspire-config-demo-$([guid]::NewGuid().ToString('N'))"),

[ValidateSet('new', 'add')]
[string] $Command = 'new',

[string[]] $CommandArguments = @()
)

$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..\..')).Path
$version = & (Join-Path $repoRoot '.agents\skills\cli-channel-debugging\get-aspire-channel-version.ps1') stable
$settings = @{
ASPIRE_INIT_SOURCE_URL = 'https://msft.ghe.com/coreai/aspire-1p-init-test.git'
ASPIRE_INIT_SOURCE_PATH = 'aspire.config.json'
ASPIRE_INIT_SOURCE_REF = 'jamesnk/source-demo'
ASPIRE_CLI_CHANNEL = 'stable'
ASPIRE_CLI_VERSION = $version
ASPIRE_CLI_COMMIT = $null
ASPIRE_CLI_PACKAGES = $null
ASPIRE_CLI_NUGET_SERVICE_INDEX = $null
MSBUILDTERMINALLOGGER = 'false'
}

$previousSettings = @{}
try
{
foreach ($name in $settings.Keys)
{
$previousSettings[$name] = [Environment]::GetEnvironmentVariable($name, 'Process')
[Environment]::SetEnvironmentVariable($name, $settings[$name], 'Process')
}

$workspace = New-Item -ItemType Directory -Path $WorkingDirectory -Force
Write-Host "Demo workspace: $($workspace.FullName)"
Write-Host "Emulating stable $version with the source-built CLI and published packages."
# dotnet run passes its SDK paths to descendants. Build separately so the app's
# child dotnet commands can select their own SDK instead of mixing SDK versions.
Push-Location -LiteralPath $repoRoot
try
{
$cliProject = Join-Path $repoRoot 'src\Aspire.Cli\Aspire.Cli.csproj'
& (Join-Path $repoRoot 'dotnet.cmd') build $cliProject --configuration Debug -p:SkipNativeBuild=true
if ($LASTEXITCODE -ne 0)
{
throw "CLI build failed with exit code $LASTEXITCODE."
}

$dotnetRoot = (Get-Content -LiteralPath (Join-Path $repoRoot 'artifacts\toolset\sdk.txt') -Raw).Trim()
$dotnet = Join-Path $dotnetRoot 'dotnet.exe'
$cliExecutable = & $dotnet msbuild $cliProject -nologo -p:Configuration=Debug -getProperty:RunCommand
if ($LASTEXITCODE -ne 0)
{
throw "Failed to locate the CLI build output with exit code $LASTEXITCODE."
}
}
finally
{
Pop-Location
}

Push-Location -LiteralPath $workspace.FullName
try
{
& $cliExecutable $Command @CommandArguments
if ($LASTEXITCODE -ne 0)
{
throw "aspire $Command failed with exit code $LASTEXITCODE."
}
}
finally
{
Pop-Location
}
}
finally
{
foreach ($name in $previousSettings.Keys)
{
[Environment]::SetEnvironmentVariable($name, $previousSettings[$name], 'Process')
}
}
34 changes: 34 additions & 0 deletions extension/schemas/aspire-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,40 @@
},
"additionalProperties": false
},
"integrationSources": {
"description": "Named NuGet integration sources and allowed package patterns, merged across configuration layers.",
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"enabled": {
"description": "Whether to search this source. Defaults to true. Disable default to omit built-in channel and NuGet discovery.",
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
]
},
"source": {
"description": "NuGet service index URL or local package directory. The default entry uses built-in discovery when omitted.",
"type": "string"
},
"patterns": {
"description": "Optional allowed package IDs: exact IDs, prefixes ending in *, or * for all packages. Defaults to * when omitted.",
"type": "array",
"items": {}
}
},
"additionalProperties": false
}
},
"packages": {
"description": "Package references for the AppHost. Value is either a string (NuGet, short form — empty = SDK version, otherwise explicit version) or an object with a required \"source\" discriminator (\"nuget\", \"project\", or \"npm\") and per-source fields (version for nuget, path for project/npm).",
"type": "object",
Expand Down
34 changes: 34 additions & 0 deletions extension/schemas/aspire-global-settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,40 @@
},
"additionalProperties": false
},
"integrationSources": {
"description": "Named NuGet integration sources and allowed package patterns, merged across configuration layers.",
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"enabled": {
"description": "Whether to search this source. Defaults to true. Disable default to omit built-in channel and NuGet discovery.",
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
]
},
"source": {
"description": "NuGet service index URL or local package directory. The default entry uses built-in discovery when omitted.",
"type": "string"
},
"patterns": {
"description": "Optional allowed package IDs: exact IDs, prefixes ending in *, or * for all packages. Defaults to * when omitted.",
"type": "array",
"items": {}
}
},
"additionalProperties": false
}
},
"language": {
"description": "The language identifier for this AppHost (e.g., \"typescript\", \"python\"). Used to determine which runtime to use for execution.",
"type": "string"
Expand Down
34 changes: 34 additions & 0 deletions extension/schemas/aspire-settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,40 @@
},
"additionalProperties": false
},
"integrationSources": {
"description": "Named NuGet integration sources and allowed package patterns, merged across configuration layers.",
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"enabled": {
"description": "Whether to search this source. Defaults to true. Disable default to omit built-in channel and NuGet discovery.",
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
]
},
"source": {
"description": "NuGet service index URL or local package directory. The default entry uses built-in discovery when omitted.",
"type": "string"
},
"patterns": {
"description": "Optional allowed package IDs: exact IDs, prefixes ending in *, or * for all packages. Defaults to * when omitted.",
"type": "array",
"items": {}
}
},
"additionalProperties": false
}
},
"language": {
"description": "The language identifier for this AppHost (e.g., \"typescript\", \"python\"). Used to determine which runtime to use for execution.",
"type": "string"
Expand Down
4 changes: 3 additions & 1 deletion src/Aspire.Cli/Agents/AspireSkills/SkillSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Globalization;
using System.Text.Json.Serialization;
using Aspire.Cli.Configuration;
using Aspire.Cli.Git;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -85,7 +86,8 @@ internal static IReadOnlyList<SkillSource> Resolve(IConfiguration configuration)
foreach (var name in names)
{
var settings = section.GetSection(name);
if (settings.Value is not null || !IsIdentifier(name))
SourceNameValidator.Validate(name, $"{SectionName}.{name}");
if (settings.Value is not null)
{
throw AspireSkillsRemoteConfiguration.ExistingConfiguration($"{SectionName}.{name}");
}
Expand Down
Loading
Loading