diff --git a/src/Aspire.Cli/Utils/ConsoleActivityLogger.cs b/src/Aspire.Cli/Utils/ConsoleActivityLogger.cs
index 5a723f60c2a..5c1153ddd55 100644
--- a/src/Aspire.Cli/Utils/ConsoleActivityLogger.cs
+++ b/src/Aspire.Cli/Utils/ConsoleActivityLogger.cs
@@ -304,18 +304,21 @@ public void WriteSummary()
///
/// Formats a single key-value pair for the pipeline summary display.
+ /// Values may contain markdown links which are converted to clickable links when supported.
///
private string FormatPipelineSummaryKvp(string key, string value)
{
if (_enableColor)
{
var escapedKey = key.EscapeMarkup();
- var escapedValue = value.EscapeMarkup();
- return $" [blue]{escapedKey}[/]: {escapedValue}";
+ var convertedValue = MarkdownToSpectreConverter.ConvertToSpectre(value);
+ convertedValue = HighlightMessage(convertedValue);
+ return $" [blue]{escapedKey}[/]: {convertedValue}";
}
else
{
- return $" {key}: {value}";
+ var plainValue = MarkdownToSpectreConverter.ConvertLinksToPlainText(value);
+ return $" {key}: {plainValue}";
}
}
diff --git a/src/Aspire.Cli/Utils/MarkdownToSpectreConverter.cs b/src/Aspire.Cli/Utils/MarkdownToSpectreConverter.cs
index bc8d3d5c6fd..000fdfcc437 100644
--- a/src/Aspire.Cli/Utils/MarkdownToSpectreConverter.cs
+++ b/src/Aspire.Cli/Utils/MarkdownToSpectreConverter.cs
@@ -61,6 +61,16 @@ public static string ConvertToSpectre(string markdown)
return result;
}
+ ///
+ /// Converts markdown links to plain text.
+ ///
+ /// The markdown text to convert.
+ /// The text with markdown links converted to the plain text format text (url).
+ public static string ConvertLinksToPlainText(string markdown)
+ {
+ return LinkRegex().Replace(markdown, "$1 ($2)");
+ }
+
private static string ConvertHeaders(string text)
{
// Convert ###### Header 6 (most specific first)
diff --git a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs
index 19a62a9e3e5..7325e690d4b 100644
--- a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs
+++ b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs
@@ -133,18 +133,19 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet
/// The Azure provisioning context.
private static void AddToPipelineSummary(PipelineStepContext ctx, ProvisioningContext provisioningContext)
{
- // Safely access the nested properties with null checks for reference types
- // AzureLocation is a struct so it cannot be null
- var resourceGroupName = provisioningContext.ResourceGroup?.Name ?? "unknown";
- var subscriptionId = provisioningContext.Subscription?.Id.Name ?? "unknown";
+ var resourceGroupName = provisioningContext.ResourceGroup.Name;
+ var subscriptionId = provisioningContext.Subscription.Id.Name;
var location = provisioningContext.Location.Name;
-#pragma warning disable ASPIREPIPELINES001 // PipelineSummary is experimental
+ var tenantId = provisioningContext.Tenant.TenantId;
+ var tenantSegment = tenantId.HasValue ? $"#@{tenantId.Value}" : "#";
+ var portalUrl = $"https://portal.azure.com/{tenantSegment}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/overview";
+ var resourceGroupValue = $"[{resourceGroupName}]({portalUrl})";
+
ctx.Summary.Add("☁️ Target", "Azure");
- ctx.Summary.Add("📦 Resource Group", resourceGroupName);
+ ctx.Summary.Add("📦 Resource Group", resourceGroupValue);
ctx.Summary.Add("🔑 Subscription", subscriptionId);
ctx.Summary.Add("🌐 Location", location);
-#pragma warning restore ASPIREPIPELINES001
}
private Task PublishAsync(PipelineStepContext context)
diff --git a/tests/Aspire.Cli.Tests/Utils/ConsoleActivityLoggerTests.cs b/tests/Aspire.Cli.Tests/Utils/ConsoleActivityLoggerTests.cs
new file mode 100644
index 00000000000..9410d688b19
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Utils/ConsoleActivityLoggerTests.cs
@@ -0,0 +1,141 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text;
+using Aspire.Cli.Utils;
+using Spectre.Console;
+
+namespace Aspire.Cli.Tests.Utils;
+
+public class ConsoleActivityLoggerTests
+{
+ [Fact]
+ public void WriteSummary_WithMarkdownLinkInPipelineSummary_RendersClickableLink()
+ {
+ var output = new StringBuilder();
+ var console = AnsiConsole.Create(new AnsiConsoleSettings
+ {
+ Ansi = AnsiSupport.Yes,
+ ColorSystem = ColorSystemSupport.TrueColor,
+ Out = new AnsiConsoleOutput(new StringWriter(output))
+ });
+
+ var hostEnvironment = TestHelpers.CreateInteractiveHostEnvironment();
+ var logger = new ConsoleActivityLogger(console, hostEnvironment, forceColor: true);
+
+ var summary = new List>
+ {
+ new("☁️ Target", "Azure"),
+ new("📦 Resource Group", "VNetTest5 [link](https://portal.azure.com/#/resource/subscriptions/sub-id/resourceGroups/VNetTest5/overview)"),
+ new("🔑 Subscription", "sub-id"),
+ new("🌐 Location", "eastus"),
+ };
+
+ logger.SetFinalResult(true, summary);
+ logger.WriteSummary();
+
+ var result = output.ToString();
+
+ // Verify the markdown link was converted to a Spectre link
+ Assert.Contains("VNetTest5", result);
+
+ const string expectedUrl =
+ @"https://portal\.azure\.com/#/resource/subscriptions/sub-id/resourceGroups/VNetTest5/overview";
+ string hyperlinkPattern =
+ $@"\u001b\]8;[^;]*;{expectedUrl}\u001b\\.*link.*\u001b\]8;;\u001b\\";
+ Assert.Matches(hyperlinkPattern, result);
+ }
+
+ [Fact]
+ public void WriteSummary_WithMarkdownLinkInPipelineSummary_NoColor_RendersPlainTextWithUrl()
+ {
+ var output = new StringBuilder();
+ var console = AnsiConsole.Create(new AnsiConsoleSettings
+ {
+ Ansi = AnsiSupport.No,
+ ColorSystem = ColorSystemSupport.NoColors,
+ Out = new AnsiConsoleOutput(new StringWriter(output))
+ });
+
+ var hostEnvironment = TestHelpers.CreateNonInteractiveHostEnvironment();
+ var logger = new ConsoleActivityLogger(console, hostEnvironment, forceColor: false);
+
+ var portalUrl = "https://portal.azure.com/";
+ var summary = new List>
+ {
+ new("📦 Resource Group", $"VNetTest5 [link]({portalUrl})"),
+ };
+
+ logger.SetFinalResult(true, summary);
+ logger.WriteSummary();
+
+ var result = output.ToString();
+
+ // When color is disabled, markdown links should be converted to plain text: text (url)
+ Assert.Contains($"VNetTest5 link ({portalUrl})", result);
+ }
+
+ [Fact]
+ public void WriteSummary_WithMarkdownLinkInPipelineSummary_ColorWithoutInteractive_RendersPlainUrl()
+ {
+ var output = new StringBuilder();
+ var console = AnsiConsole.Create(new AnsiConsoleSettings
+ {
+ Ansi = AnsiSupport.Yes,
+ ColorSystem = ColorSystemSupport.TrueColor,
+ Out = new AnsiConsoleOutput(new StringWriter(output))
+ });
+
+ // Non-interactive host but color enabled (e.g., CI environments with ANSI support)
+ var hostEnvironment = TestHelpers.CreateNonInteractiveHostEnvironment();
+ var logger = new ConsoleActivityLogger(console, hostEnvironment, forceColor: true);
+
+ var portalUrl = "https://portal.azure.com/";
+ var summary = new List>
+ {
+ new("📦 Resource Group", $"VNetTest5 [link]({portalUrl})"),
+ };
+
+ logger.SetFinalResult(true, summary);
+ logger.WriteSummary();
+
+ var result = output.ToString();
+
+ // When color is enabled but interactive output is not supported,
+ // HighlightMessage converts Spectre link markup to plain URLs
+ Assert.Contains("VNetTest5", result);
+ Assert.Contains(portalUrl, result);
+
+ // Should NOT contain the OSC 8 hyperlink escape sequence since we're non-interactive
+ Assert.DoesNotContain("\u001b]8;", result);
+ }
+
+ [Fact]
+ public void WriteSummary_WithPlainTextPipelineSummary_RendersCorrectly()
+ {
+ var output = new StringBuilder();
+ var console = AnsiConsole.Create(new AnsiConsoleSettings
+ {
+ Ansi = AnsiSupport.Yes,
+ ColorSystem = ColorSystemSupport.TrueColor,
+ Out = new AnsiConsoleOutput(new StringWriter(output))
+ });
+
+ var hostEnvironment = TestHelpers.CreateInteractiveHostEnvironment();
+ var logger = new ConsoleActivityLogger(console, hostEnvironment, forceColor: true);
+
+ var summary = new List>
+ {
+ new("☁️ Target", "Azure"),
+ new("🌐 Location", "eastus"),
+ };
+
+ logger.SetFinalResult(true, summary);
+ logger.WriteSummary();
+
+ var result = output.ToString();
+
+ Assert.Contains("Azure", result);
+ Assert.Contains("eastus", result);
+ }
+}