Skip to content

Commit 8623fe0

Browse files
Copilotdavidfowl
andauthored
Add ICliHostEnvironment service and --non-interactive flag for clean CI output (#12135)
* Initial plan * Add CI environment detection and disable interactive elements in CI Co-authored-by: davidfowl <[email protected]> * Refactor CIEnvironmentDetector to use DI with IConfiguration Co-authored-by: davidfowl <[email protected]> * Refactor to ICliHostEnvironment with --non-interactive flag support Co-authored-by: davidfowl <[email protected]> * Pass nonInteractive flag directly to CliHostEnvironment constructor Co-authored-by: davidfowl <[email protected]> * Add InteractionService.IsAvailable check for interactivity configuration Co-authored-by: davidfowl <[email protected]> * Fix DI registration for ICliHostEnvironment in tests Co-authored-by: davidfowl <[email protected]> * Add InvalidOperationException to Prompt methods when interactive input not supported Co-authored-by: davidfowl <[email protected]> * Move CI environment variables array to static readonly field Co-authored-by: davidfowl <[email protected]> * Use IConfiguration with using directive instead of fully qualified name Co-authored-by: davidfowl <[email protected]> * Add tests for InteractionService.IsAvailable property Co-authored-by: davidfowl <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: davidfowl <[email protected]>
1 parent ba4d96c commit 8623fe0

35 files changed

+796
-68
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, ICliHostEnvironment hostEnvironment)
20+
: base("deploy", DeployCommandStrings.Description, runner, interactionService, projectLocator, telemetry, sdkInstaller, features, updateNotifier, executionContext, hostEnvironment)
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, ICliHostEnvironment hostEnvironment)
38+
: base("publish", PublishCommandStrings.Description, runner, interactionService, projectLocator, telemetry, sdkInstaller, features, updateNotifier, executionContext, hostEnvironment)
3939
{
4040
ArgumentNullException.ThrowIfNull(prompter);
4141
_prompter = prompter;

src/Aspire.Cli/Commands/PublishCommandBase.cs

Lines changed: 23 additions & 4 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 ICliHostEnvironment _hostEnvironment;
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, ICliHostEnvironment hostEnvironment)
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(hostEnvironment);
5153

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

5861
var projectOption = new Option<FileInfo?>("--project")
5962
{
@@ -122,6 +125,12 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
122125

123126
var env = new Dictionary<string, string>();
124127

128+
// Set interactivity enabled based on host environment capabilities
129+
if (!_hostEnvironment.SupportsInteractiveInput)
130+
{
131+
env[KnownConfigNames.InteractivityEnabled] = "false";
132+
}
133+
125134
var waitForDebugger = parseResult.GetValue<bool?>("--wait-for-debugger") ?? false;
126135
if (waitForDebugger)
127136
{
@@ -345,7 +354,7 @@ public async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsyncEnumera
345354
{
346355
var stepCounter = 1;
347356
var steps = new Dictionary<string, StepInfo>();
348-
var logger = new ConsoleActivityLogger();
357+
var logger = new ConsoleActivityLogger(_hostEnvironment);
349358
logger.StartSpinner();
350359
PublishingActivity? publishingActivity = null;
351360

@@ -731,16 +740,26 @@ private class TaskInfo
731740
/// <summary>
732741
/// Starts the terminal infinite progress bar.
733742
/// </summary>
734-
private static void StartTerminalProgressBar()
743+
private void StartTerminalProgressBar()
735744
{
745+
// Skip terminal progress bar in non-interactive environments
746+
if (!_hostEnvironment.SupportsInteractiveOutput)
747+
{
748+
return;
749+
}
736750
Console.Write("\u001b]9;4;3\u001b\\");
737751
}
738752

739753
/// <summary>
740754
/// Stops the terminal progress bar.
741755
/// </summary>
742-
private static void StopTerminalProgressBar()
756+
private void StopTerminalProgressBar()
743757
{
758+
// Skip terminal progress bar in non-interactive environments
759+
if (!_hostEnvironment.SupportsInteractiveOutput)
760+
{
761+
return;
762+
}
744763
Console.Write("\u001b]9;4;0\u001b\\");
745764
}
746765
}

src/Aspire.Cli/Commands/RootCommand.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ public RootCommand(
5656
debugOption.Recursive = true;
5757
Options.Add(debugOption);
5858

59+
var nonInteractiveOption = new Option<bool>("--non-interactive");
60+
nonInteractiveOption.Description = "Run the command in non-interactive mode, disabling all interactive prompts and spinners";
61+
nonInteractiveOption.Recursive = true;
62+
Options.Add(nonInteractiveOption);
63+
5964
var waitForDebuggerOption = new Option<bool>("--wait-for-debugger");
6065
waitForDebuggerOption.Description = RootCommandStrings.WaitForDebuggerArgumentDescription;
6166
waitForDebuggerOption.Recursive = true;

src/Aspire.Cli/Interaction/ConsoleInteractionService.cs

Lines changed: 29 additions & 5 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 ICliHostEnvironment _hostEnvironment;
2122

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

3033
public async Task<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> action)
3134
{
32-
// In debug mode, avoid interactive progress as it conflicts with debug logging
33-
if (_executionContext.DebugMode)
35+
// In debug mode or non-interactive environments, avoid interactive progress as it conflicts with debug logging
36+
if (_executionContext.DebugMode || !_hostEnvironment.SupportsInteractiveOutput)
3437
{
3538
DisplaySubtleMessage(statusText);
3639
return await action();
@@ -43,8 +46,8 @@ public async Task<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> action)
4346

4447
public void ShowStatus(string statusText, Action action)
4548
{
46-
// In debug mode, avoid interactive progress as it conflicts with debug logging
47-
if (_executionContext.DebugMode)
49+
// In debug mode or non-interactive environments, avoid interactive progress as it conflicts with debug logging
50+
if (_executionContext.DebugMode || !_hostEnvironment.SupportsInteractiveOutput)
4851
{
4952
DisplaySubtleMessage(statusText);
5053
action();
@@ -59,6 +62,12 @@ public void ShowStatus(string statusText, Action action)
5962
public async Task<string> PromptForStringAsync(string promptText, string? defaultValue = null, Func<string, ValidationResult>? validator = null, bool isSecret = false, bool required = false, CancellationToken cancellationToken = default)
6063
{
6164
ArgumentNullException.ThrowIfNull(promptText, nameof(promptText));
65+
66+
if (!_hostEnvironment.SupportsInteractiveInput)
67+
{
68+
throw new InvalidOperationException(InteractionServiceStrings.InteractiveInputNotSupported);
69+
}
70+
6271
var prompt = new TextPrompt<string>(promptText)
6372
{
6473
IsSecret = isSecret,
@@ -86,6 +95,11 @@ public async Task<T> PromptForSelectionAsync<T>(string promptText, IEnumerable<T
8695
ArgumentNullException.ThrowIfNull(choices, nameof(choices));
8796
ArgumentNullException.ThrowIfNull(choiceFormatter, nameof(choiceFormatter));
8897

98+
if (!_hostEnvironment.SupportsInteractiveInput)
99+
{
100+
throw new InvalidOperationException(InteractionServiceStrings.InteractiveInputNotSupported);
101+
}
102+
89103
// Check if the choices collection is empty to avoid throwing an InvalidOperationException
90104
if (!choices.Any())
91105
{
@@ -108,6 +122,11 @@ public async Task<IReadOnlyList<T>> PromptForSelectionsAsync<T>(string promptTex
108122
ArgumentNullException.ThrowIfNull(choices, nameof(choices));
109123
ArgumentNullException.ThrowIfNull(choiceFormatter, nameof(choiceFormatter));
110124

125+
if (!_hostEnvironment.SupportsInteractiveInput)
126+
{
127+
throw new InvalidOperationException(InteractionServiceStrings.InteractiveInputNotSupported);
128+
}
129+
111130
// Check if the choices collection is empty to avoid throwing an InvalidOperationException
112131
if (!choices.Any())
113132
{
@@ -203,6 +222,11 @@ public void DisplayCancellationMessage()
203222

204223
public Task<bool> ConfirmAsync(string promptText, bool defaultValue = true, CancellationToken cancellationToken = default)
205224
{
225+
if (!_hostEnvironment.SupportsInteractiveInput)
226+
{
227+
throw new InvalidOperationException(InteractionServiceStrings.InteractiveInputNotSupported);
228+
}
229+
206230
return _ansiConsole.ConfirmAsync(promptText, defaultValue, cancellationToken);
207231
}
208232

src/Aspire.Cli/Program.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ private static string GetGlobalSettingsPath()
5454

5555
private static async Task<IHost> BuildApplicationAsync(string[] args)
5656
{
57+
// Check for --non-interactive flag early
58+
var nonInteractive = args?.Any(a => a == "--non-interactive") ?? false;
59+
5760
var settings = new HostApplicationBuilderSettings
5861
{
5962
Configuration = new ConfigurationManager()
@@ -109,6 +112,11 @@ private static async Task<IHost> BuildApplicationAsync(string[] args)
109112
// Shared services.
110113
builder.Services.AddSingleton(_ => BuildCliExecutionContext(debugMode));
111114
builder.Services.AddSingleton(BuildAnsiConsole);
115+
builder.Services.AddSingleton<ICliHostEnvironment>(provider =>
116+
{
117+
var configuration = provider.GetRequiredService<IConfiguration>();
118+
return new CliHostEnvironment(configuration, nonInteractive);
119+
});
112120
AddInteractionServices(builder);
113121
builder.Services.AddSingleton<IProjectLocator, ProjectLocator>();
114122
builder.Services.AddSingleton<ISolutionLocator, SolutionLocator>();
@@ -262,7 +270,8 @@ private static void AddInteractionServices(HostApplicationBuilder builder)
262270
var ansiConsole = provider.GetRequiredService<IAnsiConsole>();
263271
ansiConsole.Profile.Width = 256; // VS code terminal will handle wrapping so set a large width here.
264272
var executionContext = provider.GetRequiredService<CliExecutionContext>();
265-
var consoleInteractionService = new ConsoleInteractionService(ansiConsole, executionContext);
273+
var hostEnvironment = provider.GetRequiredService<ICliHostEnvironment>();
274+
var consoleInteractionService = new ConsoleInteractionService(ansiConsole, executionContext, hostEnvironment);
266275
return new ExtensionInteractionService(consoleInteractionService,
267276
provider.GetRequiredService<IExtensionBackchannel>(),
268277
extensionPromptEnabled);
@@ -279,7 +288,8 @@ private static void AddInteractionServices(HostApplicationBuilder builder)
279288
{
280289
var ansiConsole = provider.GetRequiredService<IAnsiConsole>();
281290
var executionContext = provider.GetRequiredService<CliExecutionContext>();
282-
return new ConsoleInteractionService(ansiConsole, executionContext);
291+
var hostEnvironment = provider.GetRequiredService<ICliHostEnvironment>();
292+
return new ConsoleInteractionService(ansiConsole, executionContext, hostEnvironment);
283293
});
284294
}
285295
}

src/Aspire.Cli/Resources/InteractionServiceStrings.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Aspire.Cli/Resources/InteractionServiceStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,7 @@
214214
<data name="CustomChoiceLabel" xml:space="preserve">
215215
<value>Other (specify next)</value>
216216
</data>
217+
<data name="InteractiveInputNotSupported" xml:space="preserve">
218+
<value>Interactive input is not supported in this environment. Use the --non-interactive flag or ensure the CLI is running in an interactive terminal.</value>
219+
</data>
217220
</root>

src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)