Skip to content

Commit a2ed2de

Browse files
Copilotdavidfowl
andcommitted
Refactor CIEnvironmentDetector to use DI with IConfiguration
Co-authored-by: davidfowl <[email protected]>
1 parent 6d64a21 commit a2ed2de

File tree

12 files changed

+142
-82
lines changed

12 files changed

+142
-82
lines changed

src/Aspire.Cli/Commands/DeployCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ internal sealed class DeployCommand : PublishCommandBase
1616
{
1717
private readonly Option<bool> _clearCacheOption;
1818

19-
public DeployCommand(IDotNetCliRunner runner, IInteractionService interactionService, IProjectLocator projectLocator, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext)
20-
: base("deploy", DeployCommandStrings.Description, runner, interactionService, projectLocator, telemetry, sdkInstaller, features, updateNotifier, executionContext)
19+
public DeployCommand(IDotNetCliRunner runner, IInteractionService interactionService, IProjectLocator projectLocator, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, ICIEnvironmentDetector ciDetector)
20+
: base("deploy", DeployCommandStrings.Description, runner, interactionService, projectLocator, telemetry, sdkInstaller, features, updateNotifier, executionContext, ciDetector)
2121
{
2222
_clearCacheOption = new Option<bool>("--clear-cache")
2323
{

src/Aspire.Cli/Commands/PublishCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ internal sealed class PublishCommand : PublishCommandBase
3434
{
3535
private readonly IPublishCommandPrompter _prompter;
3636

37-
public PublishCommand(IDotNetCliRunner runner, IInteractionService interactionService, IProjectLocator projectLocator, IPublishCommandPrompter prompter, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext)
38-
: base("publish", PublishCommandStrings.Description, runner, interactionService, projectLocator, telemetry, sdkInstaller, features, updateNotifier, executionContext)
37+
public PublishCommand(IDotNetCliRunner runner, IInteractionService interactionService, IProjectLocator projectLocator, IPublishCommandPrompter prompter, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, ICIEnvironmentDetector ciDetector)
38+
: base("publish", PublishCommandStrings.Description, runner, interactionService, projectLocator, telemetry, sdkInstaller, features, updateNotifier, executionContext, ciDetector)
3939
{
4040
ArgumentNullException.ThrowIfNull(prompter);
4141
_prompter = prompter;

src/Aspire.Cli/Commands/PublishCommandBase.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ internal abstract class PublishCommandBase : BaseCommand
2727
protected readonly IDotNetSdkInstaller _sdkInstaller;
2828

2929
private readonly IFeatures _features;
30+
private readonly ICIEnvironmentDetector _ciDetector;
3031

3132
protected abstract string OperationCompletedPrefix { get; }
3233
protected abstract string OperationFailedPrefix { get; }
@@ -40,20 +41,22 @@ private static bool IsCompletionStateError(string completionState) =>
4041
private static bool IsCompletionStateWarning(string completionState) =>
4142
completionState == CompletionStates.CompletedWithWarning;
4243

43-
protected PublishCommandBase(string name, string description, IDotNetCliRunner runner, IInteractionService interactionService, IProjectLocator projectLocator, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext)
44+
protected PublishCommandBase(string name, string description, IDotNetCliRunner runner, IInteractionService interactionService, IProjectLocator projectLocator, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, ICIEnvironmentDetector ciDetector)
4445
: base(name, description, features, updateNotifier, executionContext, interactionService)
4546
{
4647
ArgumentNullException.ThrowIfNull(runner);
4748
ArgumentNullException.ThrowIfNull(projectLocator);
4849
ArgumentNullException.ThrowIfNull(telemetry);
4950
ArgumentNullException.ThrowIfNull(sdkInstaller);
5051
ArgumentNullException.ThrowIfNull(features);
52+
ArgumentNullException.ThrowIfNull(ciDetector);
5153

5254
_runner = runner;
5355
_projectLocator = projectLocator;
5456
_telemetry = telemetry;
5557
_sdkInstaller = sdkInstaller;
5658
_features = features;
59+
_ciDetector = ciDetector;
5760

5861
var projectOption = new Option<FileInfo?>("--project")
5962
{
@@ -345,7 +348,7 @@ public async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsyncEnumera
345348
{
346349
var stepCounter = 1;
347350
var steps = new Dictionary<string, StepInfo>();
348-
var logger = new ConsoleActivityLogger();
351+
var logger = new ConsoleActivityLogger(_ciDetector);
349352
logger.StartSpinner();
350353
PublishingActivity? publishingActivity = null;
351354

@@ -731,10 +734,10 @@ private class TaskInfo
731734
/// <summary>
732735
/// Starts the terminal infinite progress bar.
733736
/// </summary>
734-
private static void StartTerminalProgressBar()
737+
private void StartTerminalProgressBar()
735738
{
736739
// Skip terminal progress bar in CI environments
737-
if (CIEnvironmentDetector.IsCI)
740+
if (_ciDetector.IsCI)
738741
{
739742
return;
740743
}
@@ -744,10 +747,10 @@ private static void StartTerminalProgressBar()
744747
/// <summary>
745748
/// Stops the terminal progress bar.
746749
/// </summary>
747-
private static void StopTerminalProgressBar()
750+
private void StopTerminalProgressBar()
748751
{
749752
// Skip terminal progress bar in CI environments
750-
if (CIEnvironmentDetector.IsCI)
753+
if (_ciDetector.IsCI)
751754
{
752755
return;
753756
}

src/Aspire.Cli/Interaction/ConsoleInteractionService.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,22 @@ internal class ConsoleInteractionService : IInteractionService
1818

1919
private readonly IAnsiConsole _ansiConsole;
2020
private readonly CliExecutionContext _executionContext;
21+
private readonly ICIEnvironmentDetector _ciDetector;
2122

22-
public ConsoleInteractionService(IAnsiConsole ansiConsole, CliExecutionContext executionContext)
23+
public ConsoleInteractionService(IAnsiConsole ansiConsole, CliExecutionContext executionContext, ICIEnvironmentDetector ciDetector)
2324
{
2425
ArgumentNullException.ThrowIfNull(ansiConsole);
2526
ArgumentNullException.ThrowIfNull(executionContext);
27+
ArgumentNullException.ThrowIfNull(ciDetector);
2628
_ansiConsole = ansiConsole;
2729
_executionContext = executionContext;
30+
_ciDetector = ciDetector;
2831
}
2932

3033
public async Task<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> action)
3134
{
3235
// In debug mode or CI environments, avoid interactive progress as it conflicts with debug logging
33-
if (_executionContext.DebugMode || CIEnvironmentDetector.IsCI)
36+
if (_executionContext.DebugMode || _ciDetector.IsCI)
3437
{
3538
DisplaySubtleMessage(statusText);
3639
return await action();
@@ -44,7 +47,7 @@ public async Task<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> action)
4447
public void ShowStatus(string statusText, Action action)
4548
{
4649
// In debug mode or CI environments, avoid interactive progress as it conflicts with debug logging
47-
if (_executionContext.DebugMode || CIEnvironmentDetector.IsCI)
50+
if (_executionContext.DebugMode || _ciDetector.IsCI)
4851
{
4952
DisplaySubtleMessage(statusText);
5053
action();

src/Aspire.Cli/Program.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ private static async Task<IHost> BuildApplicationAsync(string[] args)
109109
// Shared services.
110110
builder.Services.AddSingleton(_ => BuildCliExecutionContext(debugMode));
111111
builder.Services.AddSingleton(BuildAnsiConsole);
112+
builder.Services.AddSingleton<ICIEnvironmentDetector, CIEnvironmentDetector>();
112113
AddInteractionServices(builder);
113114
builder.Services.AddSingleton<IProjectLocator, ProjectLocator>();
114115
builder.Services.AddSingleton<ISolutionLocator, SolutionLocator>();
@@ -262,7 +263,8 @@ private static void AddInteractionServices(HostApplicationBuilder builder)
262263
var ansiConsole = provider.GetRequiredService<IAnsiConsole>();
263264
ansiConsole.Profile.Width = 256; // VS code terminal will handle wrapping so set a large width here.
264265
var executionContext = provider.GetRequiredService<CliExecutionContext>();
265-
var consoleInteractionService = new ConsoleInteractionService(ansiConsole, executionContext);
266+
var ciDetector = provider.GetRequiredService<ICIEnvironmentDetector>();
267+
var consoleInteractionService = new ConsoleInteractionService(ansiConsole, executionContext, ciDetector);
266268
return new ExtensionInteractionService(consoleInteractionService,
267269
provider.GetRequiredService<IExtensionBackchannel>(),
268270
extensionPromptEnabled);
@@ -279,7 +281,8 @@ private static void AddInteractionServices(HostApplicationBuilder builder)
279281
{
280282
var ansiConsole = provider.GetRequiredService<IAnsiConsole>();
281283
var executionContext = provider.GetRequiredService<CliExecutionContext>();
282-
return new ConsoleInteractionService(ansiConsole, executionContext);
284+
var ciDetector = provider.GetRequiredService<ICIEnvironmentDetector>();
285+
return new ConsoleInteractionService(ansiConsole, executionContext, ciDetector);
283286
});
284287
}
285288
}

src/Aspire.Cli/Utils/CIEnvironmentDetector.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.Extensions.Configuration;
5+
46
namespace Aspire.Cli.Utils;
57

68
/// <summary>
79
/// Detects if the current process is running in a CI environment.
810
/// </summary>
9-
internal static class CIEnvironmentDetector
11+
internal interface ICIEnvironmentDetector
12+
{
13+
/// <summary>
14+
/// Gets whether the current process is running in a CI environment.
15+
/// </summary>
16+
bool IsCI { get; }
17+
}
18+
19+
/// <summary>
20+
/// Default implementation that detects CI environments from configuration.
21+
/// </summary>
22+
internal sealed class CIEnvironmentDetector(IConfiguration configuration) : ICIEnvironmentDetector
1023
{
1124
/// <summary>
1225
/// Gets whether the current process is running in a CI environment.
1326
/// </summary>
14-
public static bool IsCI { get; } = DetectCI();
27+
public bool IsCI { get; } = DetectCI(configuration);
1528

16-
private static bool DetectCI()
29+
private static bool DetectCI(IConfiguration configuration)
1730
{
1831
// Check for common CI environment variables
1932
// https://github.com/watson/ci-info/blob/master/vendors.json
@@ -36,7 +49,7 @@ private static bool DetectCI()
3649

3750
foreach (var envVar in ciEnvVars)
3851
{
39-
var value = Environment.GetEnvironmentVariable(envVar);
52+
var value = configuration[envVar];
4053
if (!string.IsNullOrEmpty(value))
4154
{
4255
// For CI variable, only return true if it's "true" or "1"

src/Aspire.Cli/Utils/ConsoleActivityLogger.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace Aspire.Cli.Utils;
1818
internal sealed class ConsoleActivityLogger
1919
{
2020
private readonly bool _enableColor;
21+
private readonly ICIEnvironmentDetector _ciDetector;
2122
private readonly object _lock = new();
2223
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
2324
private readonly Dictionary<string, string> _stepColors = new();
@@ -43,12 +44,13 @@ internal sealed class ConsoleActivityLogger
4344
private const string InProgressSymbol = "→";
4445
private const string InfoSymbol = "i";
4546

46-
public ConsoleActivityLogger(bool? forceColor = null)
47+
public ConsoleActivityLogger(ICIEnvironmentDetector ciDetector, bool? forceColor = null)
4748
{
49+
_ciDetector = ciDetector;
4850
_enableColor = forceColor ?? DetectColorSupport();
4951

5052
// Disable spinner in CI environments
51-
if (CIEnvironmentDetector.IsCI)
53+
if (_ciDetector.IsCI)
5254
{
5355
_spinning = false;
5456
}
@@ -92,7 +94,7 @@ public void StartTask(string taskKey, string displayName, string? startingMessag
9294
public void StartSpinner()
9395
{
9496
// Skip spinner in CI environments
95-
if (CIEnvironmentDetector.IsCI || _spinning)
97+
if (_ciDetector.IsCI || _spinning)
9698
{
9799
return;
98100
}
@@ -256,7 +258,7 @@ public void WriteSummary(string? dashboardUrl = null)
256258
{
257259
// Render dashboard URL as clickable link in interactive terminals, plain in CI
258260
var url = dashboardUrl;
259-
if (CIEnvironmentDetector.IsCI || !_enableColor)
261+
if (_ciDetector.IsCI || !_enableColor)
260262
{
261263
AnsiConsole.MarkupLine($"Dashboard: {url.EscapeMarkup()}");
262264
}
@@ -392,7 +394,7 @@ private static IEnumerable<string> SplitLinesPreserve(string message)
392394
options: RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
393395

394396
// Escapes non-URL portions for Spectre markup while preserving injected [link] markup unescaped.
395-
private static string HighlightAndEscape(string input)
397+
private string HighlightAndEscape(string input)
396398
{
397399
if (string.IsNullOrEmpty(input))
398400
{
@@ -406,7 +408,7 @@ private static string HighlightAndEscape(string input)
406408
}
407409

408410
// In CI environments, just output URLs as-is without [link] markup
409-
if (CIEnvironmentDetector.IsCI)
411+
if (_ciDetector.IsCI)
410412
{
411413
return input.EscapeMarkup();
412414
}
@@ -431,12 +433,12 @@ private static string HighlightAndEscape(string input)
431433
return sb.ToString();
432434
}
433435

434-
private static bool DetectColorSupport()
436+
private bool DetectColorSupport()
435437
{
436438
try
437439
{
438440
// In CI environments, we should still use ANSI colors for better readability
439-
if (CIEnvironmentDetector.IsCI)
441+
if (_ciDetector.IsCI)
440442
{
441443
// Most modern CI systems support ANSI colors
442444
return true;

tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public async Task PromptForSelectionAsync_EmptyChoices_ThrowsEmptyChoicesExcepti
1414
{
1515
// Arrange
1616
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."));
17-
var interactionService = new ConsoleInteractionService(AnsiConsole.Console, executionContext);
17+
var interactionService = new ConsoleInteractionService(AnsiConsole.Console, executionContext, TestHelpers.CreateNonCIDetector());
1818
var choices = Array.Empty<string>();
1919

2020
// Act & Assert
@@ -27,7 +27,7 @@ public async Task PromptForSelectionsAsync_EmptyChoices_ThrowsEmptyChoicesExcept
2727
{
2828
// Arrange
2929
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."));
30-
var interactionService = new ConsoleInteractionService(AnsiConsole.Console, executionContext);
30+
var interactionService = new ConsoleInteractionService(AnsiConsole.Console, executionContext, TestHelpers.CreateNonCIDetector());
3131
var choices = Array.Empty<string>();
3232

3333
// Act & Assert
@@ -48,7 +48,7 @@ public void DisplayError_WithMarkupCharacters_DoesNotCauseMarkupParsingError()
4848
});
4949

5050
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."));
51-
var interactionService = new ConsoleInteractionService(console, executionContext);
51+
var interactionService = new ConsoleInteractionService(console, executionContext, TestHelpers.CreateNonCIDetector());
5252
var errorMessage = "The JSON value could not be converted to <Type>. Path: $.values[0].Type | LineNumber: 0 | BytePositionInLine: 121.";
5353

5454
// Act - this should not throw an exception due to markup parsing
@@ -73,7 +73,7 @@ public void DisplaySubtleMessage_WithMarkupCharacters_DoesNotCauseMarkupParsingE
7373
});
7474

7575
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."));
76-
var interactionService = new ConsoleInteractionService(console, executionContext);
76+
var interactionService = new ConsoleInteractionService(console, executionContext, TestHelpers.CreateNonCIDetector());
7777
var message = "Path with <brackets> and [markup] characters";
7878

7979
// Act - this should not throw an exception due to markup parsing
@@ -98,7 +98,7 @@ public void DisplayLines_WithMarkupCharacters_DoesNotCauseMarkupParsingError()
9898
});
9999

100100
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."));
101-
var interactionService = new ConsoleInteractionService(console, executionContext);
101+
var interactionService = new ConsoleInteractionService(console, executionContext, TestHelpers.CreateNonCIDetector());
102102
var lines = new[]
103103
{
104104
("stdout", "Command output with <angle> brackets"),
@@ -129,7 +129,7 @@ public void DisplayMarkdown_WithBasicMarkdown_ConvertsToSpectreMarkup()
129129
});
130130

131131
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."));
132-
var interactionService = new ConsoleInteractionService(console, executionContext);
132+
var interactionService = new ConsoleInteractionService(console, executionContext, TestHelpers.CreateNonCIDetector());
133133
var markdown = "# Header\nThis is **bold** and *italic* text with `code`.";
134134

135135
// Act
@@ -156,7 +156,7 @@ public void DisplayMarkdown_WithPlainText_DoesNotThrow()
156156
});
157157

158158
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."));
159-
var interactionService = new ConsoleInteractionService(console, executionContext);
159+
var interactionService = new ConsoleInteractionService(console, executionContext, TestHelpers.CreateNonCIDetector());
160160
var plainText = "This is just plain text without any markdown.";
161161

162162
// Act
@@ -181,7 +181,7 @@ public async Task ShowStatusAsync_InDebugMode_DisplaysSubtleMessageInsteadOfSpin
181181
});
182182

183183
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."), debugMode: true);
184-
var interactionService = new ConsoleInteractionService(console, executionContext);
184+
var interactionService = new ConsoleInteractionService(console, executionContext, TestHelpers.CreateNonCIDetector());
185185
var statusText = "Processing request...";
186186
var result = "test result";
187187

@@ -208,7 +208,7 @@ public void ShowStatus_InDebugMode_DisplaysSubtleMessageInsteadOfSpinner()
208208
});
209209

210210
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."), debugMode: true);
211-
var interactionService = new ConsoleInteractionService(console, executionContext);
211+
var interactionService = new ConsoleInteractionService(console, executionContext, TestHelpers.CreateNonCIDetector());
212212
var statusText = "Processing synchronous request...";
213213
var actionCalled = false;
214214

0 commit comments

Comments
 (0)