From e5b355e575e1b0a57e4e30b086ee9fd4072f32c6 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Tue, 17 Feb 2026 10:23:42 -0800 Subject: [PATCH 1/2] Fix Windows pipeline image to use windows.vs2022.amd64.open (#14492) * Fix Windows pipeline image to use windows.vs2022.amd64.open * Use windows.vs2026preview.scout.amd64 for public pipeline Windows pool Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/templates/public-pipeline-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/templates/public-pipeline-template.yml b/eng/pipelines/templates/public-pipeline-template.yml index 5c394939d03..e560f93ed19 100644 --- a/eng/pipelines/templates/public-pipeline-template.yml +++ b/eng/pipelines/templates/public-pipeline-template.yml @@ -86,7 +86,7 @@ stages: pool: name: $(DncEngPublicBuildPool) - demands: ImageOverride -equals 1es-windows-2022-open + demands: ImageOverride -equals windows.vs2026preview.scout.amd64.open variables: - name: _buildScript From 24e33c914b9cb07037a50a9b89ee7ad60ab5681a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:02:24 +0000 Subject: [PATCH 2/2] Add Azure portal link for Resource Group in deploy pipeline summary (#14434) * Add Azure portal link for Resource Group in pipeline summary When printing the Resource Group in the pipeline summary of `aspire deploy`, include a clickable link to the Azure portal resource group page. The link uses the format: https://portal.azure.com/#@{tenantId}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/overview Changes: - AzureEnvironmentResource.AddToPipelineSummary: construct markdown link for resource group - ConsoleActivityLogger.FormatPipelineSummaryKvp: convert markdown to Spectre markup for clickable links - Add ConsoleActivityLoggerTests for the new markdown rendering behavior Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> * Clean up the code * Fix tests * More test fixups * Refactor code * Update src/Aspire.Cli/Utils/MarkdownToSpectreConverter.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add test for color-enabled non-interactive rendering path Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> * fix test --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> Co-authored-by: Eric Erhardt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Utils/ConsoleActivityLogger.cs | 9 +- .../Utils/MarkdownToSpectreConverter.cs | 10 ++ .../AzureEnvironmentResource.cs | 15 +- .../Utils/ConsoleActivityLoggerTests.cs | 141 ++++++++++++++++++ 4 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 tests/Aspire.Cli.Tests/Utils/ConsoleActivityLoggerTests.cs 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); + } +}