Skip to content

Commit 2f03451

Browse files
Copilotdavidfowlcaptainsafia
authored
Refactor publishing state model and CLI protocol to aggregate CompletionState at all levels (#10037)
Co-authored-by: davidfowl <[email protected]> Co-authored-by: captainsafia <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Safia Abdalla <[email protected]>
1 parent 970453d commit 2f03451

15 files changed

+481
-236
lines changed

src/Aspire.Cli/Backchannel/BackchannelDataTypes.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,25 @@ internal sealed class PublishingActivityData
9999
/// </summary>
100100
public required string StatusText { get; init; }
101101

102+
/// <summary>
103+
/// Gets the completion state of the publishing activity.
104+
/// </summary>
105+
public string CompletionState { get; init; } = CompletionStates.InProgress;
106+
102107
/// <summary>
103108
/// Gets a value indicating whether the publishing activity is complete.
104109
/// </summary>
105-
public bool IsComplete { get; init; }
110+
public bool IsComplete => CompletionState is not CompletionStates.InProgress;
106111

107112
/// <summary>
108113
/// Gets a value indicating whether the publishing activity encountered an error.
109114
/// </summary>
110-
public bool IsError { get; init; }
115+
public bool IsError => CompletionState is CompletionStates.CompletedWithError;
111116

112117
/// <summary>
113118
/// Gets a value indicating whether the publishing activity completed with warnings.
114119
/// </summary>
115-
public bool IsWarning { get; init; }
120+
public bool IsWarning => CompletionState is CompletionStates.CompletedWithWarning;
116121

117122
/// <summary>
118123
/// Gets the identifier of the step this task belongs to (only applicable for tasks).
@@ -143,3 +148,14 @@ internal class BackchannelLogEntry
143148
public required DateTimeOffset Timestamp { get; set; }
144149
public required string CategoryName { get; set; }
145150
}
151+
152+
/// <summary>
153+
/// Constants for completion state values.
154+
/// </summary>
155+
internal static class CompletionStates
156+
{
157+
public const string InProgress = "InProgress";
158+
public const string Completed = "Completed";
159+
public const string CompletedWithWarning = "CompletedWithWarning";
160+
public const string CompletedWithError = "CompletedWithError";
161+
}

src/Aspire.Cli/Commands/PublishCommandBase.cs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ internal abstract class PublishCommandBase : BaseCommand
2525
protected readonly IProjectLocator _projectLocator;
2626
protected readonly AspireCliTelemetry _telemetry;
2727

28+
private static bool IsCompletionStateComplete(string completionState) =>
29+
completionState is CompletionStates.Completed or CompletionStates.CompletedWithWarning or CompletionStates.CompletedWithError;
30+
31+
private static bool IsCompletionStateError(string completionState) =>
32+
completionState == CompletionStates.CompletedWithError;
33+
34+
private static bool IsCompletionStateWarning(string completionState) =>
35+
completionState == CompletionStates.CompletedWithWarning;
36+
2837
protected PublishCommandBase(string name, string description, IDotNetCliRunner runner, IInteractionService interactionService, IProjectLocator projectLocator, AspireCliTelemetry telemetry)
2938
: base(name, description)
3039
{
@@ -225,7 +234,7 @@ public static async Task<bool> ProcessPublishingActivitiesAsync(IAsyncEnumerable
225234
{
226235
if (publishingActivity.Type == PublishingActivityTypes.PublishComplete)
227236
{
228-
return !publishingActivity.Data.IsError;
237+
return !IsCompletionStateError(publishingActivity.Data.CompletionState);
229238
}
230239
}
231240

@@ -267,7 +276,8 @@ public static async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsync
267276
Id = activity.Data.Id,
268277
Title = activity.Data.StatusText,
269278
Number = stepCounter++,
270-
StartTime = DateTime.UtcNow
279+
StartTime = DateTime.UtcNow,
280+
CompletionState = activity.Data.CompletionState
271281
};
272282

273283
steps[activity.Data.Id] = stepInfo;
@@ -279,15 +289,14 @@ public static async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsync
279289
}
280290
// If the step is complete, update the step info, clear out any pending progress tasks, and
281291
// display the completion status associated with the the step.
282-
else if (activity.Data.IsComplete)
292+
else if (IsCompletionStateComplete(activity.Data.CompletionState))
283293
{
284-
stepInfo.IsComplete = true;
285-
stepInfo.IsError = activity.Data.IsError;
294+
stepInfo.CompletionState = activity.Data.CompletionState;
286295
stepInfo.CompletionText = activity.Data.StatusText;
287296

288297
await currentStepProgress.DisposeAsync();
289298

290-
if (stepInfo.IsError)
299+
if (IsCompletionStateError(stepInfo.CompletionState))
291300
{
292301
AnsiConsole.MarkupLine($"[red bold]❌ FAILED:[/] {stepInfo.CompletionText.EscapeMarkup()}");
293302
}
@@ -329,7 +338,8 @@ public static async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsync
329338
{
330339
Id = activity.Data.Id,
331340
StatusText = activity.Data.StatusText,
332-
StartTime = DateTime.UtcNow
341+
StartTime = DateTime.UtcNow,
342+
CompletionState = activity.Data.CompletionState
333343
};
334344

335345
tasks[activity.Data.Id] = task;
@@ -345,14 +355,12 @@ public static async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsync
345355
}
346356

347357
task.StatusText = activity.Data.StatusText;
348-
task.IsComplete = activity.Data.IsComplete;
349-
task.IsError = activity.Data.IsError;
350-
task.IsWarning = activity.Data.IsWarning;
358+
task.CompletionState = activity.Data.CompletionState;
351359

352-
if (task.IsError || task.IsWarning || task.IsComplete)
360+
if (IsCompletionStateComplete(activity.Data.CompletionState))
353361
{
354-
var prefix = task.IsError ? "[red]✗ FAILED:[/]" :
355-
task.IsWarning ? "[yellow]⚠ WARNING:[/]" : "[green]✓ DONE:[/]";
362+
var prefix = IsCompletionStateError(task.CompletionState) ? "[red]✗ FAILED:[/]" :
363+
IsCompletionStateWarning(task.CompletionState) ? "[yellow]⚠ WARNING:[/]" : "[green]✓ DONE:[/]";
356364
task.ProgressTask.Description = $" {prefix} {task.StatusText.EscapeMarkup()}";
357365
task.CompletionMessage = activity.Data.CompletionMessage;
358366

@@ -373,7 +381,7 @@ public static async Task<bool> ProcessAndDisplayPublishingActivitiesAsync(IAsync
373381
}
374382
}
375383

376-
var hasErrors = publishingActivity?.Data.IsError ?? false;
384+
var hasErrors = publishingActivity is not null && IsCompletionStateError(publishingActivity.Data.CompletionState);
377385

378386
if (publishingActivity is not null)
379387
{
@@ -434,8 +442,7 @@ private class StepInfo
434442
public string Title { get; set; } = string.Empty;
435443
public int Number { get; set; }
436444
public DateTime StartTime { get; set; }
437-
public bool IsComplete { get; set; }
438-
public bool IsError { get; set; }
445+
public string CompletionState { get; set; } = CompletionStates.InProgress;
439446
public string CompletionText { get; set; } = string.Empty;
440447
public Dictionary<string, TaskInfo> Tasks { get; } = [];
441448
}
@@ -445,9 +452,7 @@ private class TaskInfo
445452
public string Id { get; set; } = string.Empty;
446453
public string StatusText { get; set; } = string.Empty;
447454
public DateTime StartTime { get; set; }
448-
public bool IsComplete { get; set; }
449-
public bool IsError { get; set; }
450-
public bool IsWarning { get; set; }
455+
public string CompletionState { get; set; } = CompletionStates.InProgress;
451456
public string? CompletionMessage { get; set; }
452457
public ProgressTask? ProgressTask { get; set; }
453458
}

src/Aspire.Hosting.Azure/AzurePublishingContext.cs

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -80,38 +80,32 @@ public async Task WriteModelAsync(DistributedApplicationModel model, AzureEnviro
8080
cancellationToken
8181
).ConfigureAwait(false);
8282

83-
(string Message, bool IsError) stepInfo;
84-
85-
try
83+
await using (step.ConfigureAwait(false))
8684
{
87-
await WriteAzureArtifactsOutputAsync(step, model, environment, cancellationToken).ConfigureAwait(false);
85+
var writeTask = await step.CreateTaskAsync("Writing Azure Bicep templates", cancellationToken).ConfigureAwait(false);
8886

89-
await SaveToDiskAsync(outputPath).ConfigureAwait(false);
87+
await using (writeTask.ConfigureAwait(false))
88+
{
89+
try
90+
{
91+
await WriteAzureArtifactsOutputAsync(step, model, environment, cancellationToken).ConfigureAwait(false);
9092

91-
stepInfo = (
92-
$"Azure Bicep templates written successfully to {outputPath}.",
93-
false
94-
);
95-
}
96-
catch (Exception ex)
97-
{
98-
stepInfo = (
99-
$"Failed to write Azure Bicep templates: {ex.Message}",
100-
true
101-
);
93+
await SaveToDiskAsync(outputPath).ConfigureAwait(false);
10294

103-
Logger.LogError(ex, "Failed to write Azure Bicep templates to {OutputPath}", outputPath);
104-
}
95+
await writeTask.CompleteAsync($"Azure Bicep templates written successfully to {outputPath}.", cancellationToken).ConfigureAwait(false);
96+
}
97+
catch (Exception ex)
98+
{
99+
await writeTask.FailAsync($"Failed to write Azure Bicep templates: {ex.Message}", cancellationToken).ConfigureAwait(false);
105100

106-
await ProgressReporter.CompleteStepAsync(
107-
step,
108-
stepInfo.Message,
109-
stepInfo.IsError,
110-
cancellationToken
111-
).ConfigureAwait(false);
101+
Logger.LogError(ex, "Failed to write Azure Bicep templates to {OutputPath}", outputPath);
102+
throw;
103+
}
104+
}
105+
}
112106
}
113107

114-
private async Task WriteAzureArtifactsOutputAsync(PublishingStep step, DistributedApplicationModel model, AzureEnvironmentResource environment, CancellationToken _)
108+
private async Task WriteAzureArtifactsOutputAsync(PublishingStep step, DistributedApplicationModel model, AzureEnvironmentResource environment, CancellationToken cancellationToken)
115109
{
116110
var outputDirectory = new DirectoryInfo(outputPath);
117111
if (!outputDirectory.Exists)
@@ -220,7 +214,7 @@ static BicepValue<string> ResolveValue(object val)
220214

221215
var computeEnvironmentTask = await step.CreateTaskAsync(
222216
"Analyzing model for compute environments.",
223-
cancellationToken: default
217+
cancellationToken: cancellationToken
224218
).ConfigureAwait(false);
225219

226220
foreach (var resource in bicepResourcesToPublish)
@@ -232,7 +226,7 @@ static BicepValue<string> ResolveValue(object val)
232226

233227
var task = await step.CreateTaskAsync(
234228
$"Processing Azure resource {resource.Name}",
235-
cancellationToken: default
229+
cancellationToken: cancellationToken
236230
)
237231
.ConfigureAwait(false);
238232

@@ -262,22 +256,22 @@ static BicepValue<string> ResolveValue(object val)
262256

263257
await task.SucceedAsync(
264258
$"Wrote bicep module for resource {resource.Name} to {module.Path}",
265-
cancellationToken: default
259+
cancellationToken: cancellationToken
266260
).ConfigureAwait(false);
267261
}
268262

269263
var (message, state) = computeEnvironments.Count switch
270264
{
271-
0 => ("No azure compute environments found in the model.", TaskCompletionState.CompletedWithWarning),
272-
_ => ($"Found {computeEnvironments.Count} compute environment(s) in the model.", TaskCompletionState.Completed)
265+
0 => ("No azure compute environments found in the model.", CompletionState.CompletedWithWarning),
266+
_ => ($"Found {computeEnvironments.Count} compute environment(s) in the model.", CompletionState.Completed)
273267
};
274268

275269
// Report the completion of the compute environment task.
276270
await ProgressReporter.CompleteTaskAsync(
277271
computeEnvironmentTask,
278272
state,
279273
message,
280-
cancellationToken: default
274+
cancellationToken: cancellationToken
281275
).ConfigureAwait(false);
282276

283277
var outputs = new Dictionary<string, BicepOutputReference>();

src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,25 @@ internal sealed class PublishingActivityData
8181
/// </summary>
8282
public required string StatusText { get; init; }
8383

84+
/// <summary>
85+
/// Gets the completion state of the publishing activity.
86+
/// </summary>
87+
public string CompletionState { get; init; } = CompletionStates.InProgress;
88+
8489
/// <summary>
8590
/// Gets a value indicating whether the publishing activity is complete.
8691
/// </summary>
87-
public bool IsComplete { get; init; }
92+
public bool IsComplete => CompletionState is not CompletionStates.InProgress;
8893

8994
/// <summary>
9095
/// Gets a value indicating whether the publishing activity encountered an error.
9196
/// </summary>
92-
public bool IsError { get; init; }
97+
public bool IsError => CompletionState is CompletionStates.CompletedWithError;
9398

9499
/// <summary>
95100
/// Gets a value indicating whether the publishing activity completed with warnings.
96101
/// </summary>
97-
public bool IsWarning { get; init; }
102+
public bool IsWarning => CompletionState is CompletionStates.CompletedWithWarning;
98103

99104
/// <summary>
100105
/// Gets the identifier of the step this task belongs to (only applicable for tasks).
@@ -116,3 +121,14 @@ internal static class PublishingActivityTypes
116121
public const string Task = "task";
117122
public const string PublishComplete = "publish-complete";
118123
}
124+
125+
/// <summary>
126+
/// Constants for completion state values.
127+
/// </summary>
128+
internal static class CompletionStates
129+
{
130+
public const string InProgress = "InProgress";
131+
public const string Completed = "Completed";
132+
public const string CompletedWithWarning = "CompletedWithWarning";
133+
public const string CompletedWithError = "CompletedWithError";
134+
}

src/Aspire.Hosting/CompatibilitySuppressions.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,21 @@
5252
</Suppression>
5353
<Suppression>
5454
<DiagnosticId>CP0006</DiagnosticId>
55-
<Target>M:Aspire.Hosting.Publishing.IPublishingActivityProgressReporter.CompletePublishAsync(System.Boolean,System.Threading.CancellationToken)</Target>
55+
<Target>M:Aspire.Hosting.Publishing.IPublishingActivityProgressReporter.CompletePublishAsync(System.Nullable{Aspire.Hosting.Publishing.CompletionState},System.Threading.CancellationToken)</Target>
5656
<Left>lib/net8.0/Aspire.Hosting.dll</Left>
5757
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
5858
<IsBaselineSuppression>true</IsBaselineSuppression>
5959
</Suppression>
6060
<Suppression>
6161
<DiagnosticId>CP0006</DiagnosticId>
62-
<Target>M:Aspire.Hosting.Publishing.IPublishingActivityProgressReporter.CompleteStepAsync(Aspire.Hosting.Publishing.PublishingStep,System.String,System.Boolean,System.Threading.CancellationToken)</Target>
62+
<Target>M:Aspire.Hosting.Publishing.IPublishingActivityProgressReporter.CompleteStepAsync(Aspire.Hosting.Publishing.PublishingStep,System.String,Aspire.Hosting.Publishing.CompletionState,System.Threading.CancellationToken)</Target>
6363
<Left>lib/net8.0/Aspire.Hosting.dll</Left>
6464
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
6565
<IsBaselineSuppression>true</IsBaselineSuppression>
6666
</Suppression>
6767
<Suppression>
6868
<DiagnosticId>CP0006</DiagnosticId>
69-
<Target>M:Aspire.Hosting.Publishing.IPublishingActivityProgressReporter.CompleteTaskAsync(Aspire.Hosting.Publishing.PublishingTask,Aspire.Hosting.Publishing.TaskCompletionState,System.String,System.Threading.CancellationToken)</Target>
69+
<Target>M:Aspire.Hosting.Publishing.IPublishingActivityProgressReporter.CompleteTaskAsync(Aspire.Hosting.Publishing.PublishingTask,Aspire.Hosting.Publishing.CompletionState,System.String,System.Threading.CancellationToken)</Target>
7070
<Left>lib/net8.0/Aspire.Hosting.dll</Left>
7171
<Right>lib/net8.0/Aspire.Hosting.dll</Right>
7272
<IsBaselineSuppression>true</IsBaselineSuppression>

src/Aspire.Hosting/DistributedApplicationRunner.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ await eventing.PublishAsync<AfterPublishEvent>(
4343
new AfterPublishEvent(serviceProvider, model), stoppingToken
4444
).ConfigureAwait(false);
4545

46-
await activityReporter.CompletePublishAsync(true, stoppingToken).ConfigureAwait(false);
46+
// We pass null here so th aggregate state can be calculated based on the state of
47+
// each of the publish steps that have been enumerated.
48+
await activityReporter.CompletePublishAsync(completionState: null, stoppingToken).ConfigureAwait(false);
4749

4850
// If we are running in publish mode and a backchannel is being
4951
// used then we don't want to stop the app host. Instead the
@@ -58,7 +60,7 @@ await eventing.PublishAsync<AfterPublishEvent>(
5860
catch (Exception ex)
5961
{
6062
logger.LogError(ex, "Failed to publish the distributed application.");
61-
await activityReporter.CompletePublishAsync(false, stoppingToken).ConfigureAwait(false);
63+
await activityReporter.CompletePublishAsync(CompletionState.CompletedWithError, stoppingToken).ConfigureAwait(false);
6264

6365
if (!backchannelService.IsBackchannelExpected)
6466
{

src/Aspire.Hosting/Publishing/NullPublishingActivityProgressReporter.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,16 @@ public Task<PublishingStep> CreateStepAsync(string title, CancellationToken canc
3131
/// <inheritdoc/>
3232
public Task<PublishingTask> CreateTaskAsync(PublishingStep step, string statusText, CancellationToken cancellationToken)
3333
{
34-
var task = new PublishingTask(Guid.NewGuid().ToString(), step.Id, statusText);
34+
var task = new PublishingTask(Guid.NewGuid().ToString(), step.Id, statusText, step);
3535
task.Reporter = this;
36+
step.AddTask(task);
3637
return Task.FromResult(task);
3738
}
3839

3940
/// <inheritdoc/>
40-
public Task CompleteStepAsync(PublishingStep step, string completionText, bool isError = false, CancellationToken cancellationToken = default)
41+
public Task CompleteStepAsync(PublishingStep step, string completionText, CompletionState completionState, CancellationToken cancellationToken = default)
4142
{
42-
step.IsComplete = true;
43+
step.CompletionState = completionState;
4344
step.CompletionText = completionText;
4445
return Task.CompletedTask;
4546
}
@@ -52,15 +53,15 @@ public Task UpdateTaskAsync(PublishingTask task, string statusText, Cancellation
5253
}
5354

5455
/// <inheritdoc/>
55-
public Task CompleteTaskAsync(PublishingTask task, TaskCompletionState completionState, string? completionMessage = null, CancellationToken cancellationToken = default)
56+
public Task CompleteTaskAsync(PublishingTask task, CompletionState completionState, string? completionMessage = null, CancellationToken cancellationToken = default)
5657
{
5758
task.CompletionState = completionState;
5859
task.CompletionMessage = completionMessage ?? string.Empty;
5960
return Task.CompletedTask;
6061
}
6162

6263
/// <inheritdoc/>
63-
public Task CompletePublishAsync(bool success, CancellationToken cancellationToken)
64+
public Task CompletePublishAsync(CompletionState? completionState = null, CancellationToken cancellationToken = default)
6465
{
6566
return Task.CompletedTask;
6667
}

0 commit comments

Comments
 (0)