277 lines
13 KiB
C#
277 lines
13 KiB
C#
using Sdt.Config;
|
|
|
|
namespace Sdt.Core;
|
|
|
|
public sealed class WorkflowExecutor(
|
|
IWorkflowPlanner planner,
|
|
IToolProbe toolProbe,
|
|
IPrereqInstaller installer,
|
|
IActionRunner actionRunner,
|
|
IRequirementResolver requirementResolver)
|
|
{
|
|
private readonly IWorkflowPlanner _planner = planner;
|
|
private readonly IToolProbe _toolProbe = toolProbe;
|
|
private readonly IPrereqInstaller _installer = installer;
|
|
private readonly IActionRunner _actionRunner = actionRunner;
|
|
private readonly IRequirementResolver _requirementResolver = requirementResolver;
|
|
|
|
public async Task<WorkflowExecutionResult> ExecuteAsync(
|
|
WorkflowDefinition rootWorkflow,
|
|
IReadOnlyDictionary<string, WorkflowDefinition> allWorkflows,
|
|
DevToolConfig config,
|
|
string projectRoot,
|
|
Func<string, InstallPlan, Task<bool>> confirmInstallAsync,
|
|
Action<string, bool> onOutput,
|
|
Action<RunEvent>? onEvent = null,
|
|
IReadOnlyDictionary<string, string>? envOverrides = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var results = new List<WorkflowStepResult>();
|
|
var plan = _planner.ResolvePlan(rootWorkflow, allWorkflows);
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowStarted,
|
|
Message: $"Workflow '{rootWorkflow.Id}' started.",
|
|
WorkflowId: rootWorkflow.Id));
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowPlanned,
|
|
Message: $"Execution plan contains {plan.Count} workflow(s).",
|
|
WorkflowId: rootWorkflow.Id));
|
|
|
|
if (plan.Count == 0)
|
|
{
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowCompleted,
|
|
Message: "No executable workflow steps were found.",
|
|
WorkflowId: rootWorkflow.Id,
|
|
Success: false));
|
|
return new WorkflowExecutionResult(
|
|
Success: false,
|
|
StopReason: ExecutionStopReason.ValidationFailed,
|
|
Message: "This workflow has no executable steps.",
|
|
Steps: results);
|
|
}
|
|
|
|
foreach (var workflow in plan)
|
|
{
|
|
foreach (var step in workflow.Steps)
|
|
{
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowStepStarted,
|
|
Message: $"Step '{step.Label}' started.",
|
|
WorkflowId: workflow.Id,
|
|
StepId: step.Id));
|
|
|
|
var requires = _requirementResolver.Resolve(step);
|
|
foreach (var req in requires)
|
|
{
|
|
var probe = await _toolProbe.ProbeAsync(req.Tool, projectRoot, config, envOverrides, cancellationToken).ConfigureAwait(false);
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.ProbeChecked,
|
|
Message: probe.IsAvailable
|
|
? $"Tool '{req.Tool}' is available."
|
|
: $"Tool '{req.Tool}' is missing.",
|
|
WorkflowId: workflow.Id,
|
|
StepId: step.Id,
|
|
Tool: req.Tool,
|
|
Success: probe.IsAvailable));
|
|
if (probe.IsAvailable)
|
|
continue;
|
|
|
|
if (!string.IsNullOrWhiteSpace(probe.Details))
|
|
{
|
|
onOutput($"Probe detail [{req.Tool}]: {probe.Details}", false);
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.ProbeFailed,
|
|
Message: probe.Details,
|
|
WorkflowId: workflow.Id,
|
|
StepId: step.Id,
|
|
Tool: req.Tool,
|
|
Success: false));
|
|
}
|
|
|
|
if (req.InstallPolicy == InstallPolicy.Never)
|
|
{
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowCompleted,
|
|
Message: $"Missing prerequisite '{req.Tool}'.",
|
|
WorkflowId: rootWorkflow.Id,
|
|
Tool: req.Tool,
|
|
Success: false));
|
|
return new WorkflowExecutionResult(
|
|
Success: false,
|
|
StopReason: ExecutionStopReason.MissingPrereq,
|
|
Message: $"Missing prerequisite '{req.Tool}' for step '{step.Label}'.",
|
|
Steps: results);
|
|
}
|
|
|
|
var installPlan = await _installer.GetInstallPlanAsync(
|
|
req.Tool,
|
|
projectRoot,
|
|
config,
|
|
cancellationToken).ConfigureAwait(false);
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.InstallPlanPrepared,
|
|
Message: installPlan.Summary,
|
|
WorkflowId: workflow.Id,
|
|
StepId: step.Id,
|
|
Tool: req.Tool,
|
|
Success: installPlan.Supported));
|
|
if (!installPlan.Supported || installPlan.Commands.Count == 0)
|
|
{
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowCompleted,
|
|
Message: $"No installer plan available for '{req.Tool}'.",
|
|
WorkflowId: rootWorkflow.Id,
|
|
Tool: req.Tool,
|
|
Success: false));
|
|
return new WorkflowExecutionResult(
|
|
Success: false,
|
|
StopReason: ExecutionStopReason.MissingPrereq,
|
|
Message: $"Missing prerequisite '{req.Tool}' and no installer plan is available.",
|
|
Steps: results);
|
|
}
|
|
|
|
var approved = req.InstallPolicy == InstallPolicy.Auto
|
|
? true
|
|
: await confirmInstallAsync(req.Tool, installPlan).ConfigureAwait(false);
|
|
|
|
if (!approved)
|
|
{
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.InstallDeclined,
|
|
Message: $"Install declined for '{req.Tool}'.",
|
|
WorkflowId: workflow.Id,
|
|
StepId: step.Id,
|
|
Tool: req.Tool,
|
|
Success: false));
|
|
return new WorkflowExecutionResult(
|
|
Success: false,
|
|
StopReason: ExecutionStopReason.UserDeclined,
|
|
Message: $"Install declined for missing prerequisite '{req.Tool}'.",
|
|
Steps: results);
|
|
}
|
|
|
|
foreach (var installCommand in installPlan.Commands)
|
|
{
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.InstallCommandStarted,
|
|
Message: $"{installCommand.Command} {string.Join(" ", installCommand.Args)}",
|
|
WorkflowId: workflow.Id,
|
|
StepId: step.Id,
|
|
Tool: req.Tool));
|
|
|
|
var installResult = await _installer.RunInstallAsync(
|
|
installCommand,
|
|
projectRoot,
|
|
onOutput,
|
|
envOverrides,
|
|
cancellationToken).ConfigureAwait(false);
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.InstallCommandCompleted,
|
|
Message: $"Install command exited {installResult.ExitCode}.",
|
|
WorkflowId: workflow.Id,
|
|
StepId: step.Id,
|
|
Tool: req.Tool,
|
|
Success: installResult.Success,
|
|
ExitCode: installResult.ExitCode));
|
|
|
|
if (!installResult.Success)
|
|
{
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowCompleted,
|
|
Message: $"Install failed for '{req.Tool}'.",
|
|
WorkflowId: rootWorkflow.Id,
|
|
Tool: req.Tool,
|
|
Success: false));
|
|
return new WorkflowExecutionResult(
|
|
Success: false,
|
|
StopReason: ExecutionStopReason.InstallFailed,
|
|
Message: $"Failed to install prerequisite '{req.Tool}'.",
|
|
Steps: results);
|
|
}
|
|
}
|
|
|
|
var verifyProbe = await _toolProbe.ProbeAsync(req.Tool, projectRoot, config, envOverrides, cancellationToken).ConfigureAwait(false);
|
|
if (!verifyProbe.IsAvailable)
|
|
{
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowCompleted,
|
|
Message: $"Tool '{req.Tool}' still missing after install.",
|
|
WorkflowId: rootWorkflow.Id,
|
|
Tool: req.Tool,
|
|
Success: false));
|
|
return new WorkflowExecutionResult(
|
|
Success: false,
|
|
StopReason: ExecutionStopReason.InstallFailed,
|
|
Message: $"Prerequisite '{req.Tool}' is still missing after install attempt.",
|
|
Steps: results);
|
|
}
|
|
}
|
|
|
|
var runResult = await _actionRunner.RunStepAsync(
|
|
step,
|
|
projectRoot,
|
|
onOutput,
|
|
envOverrides,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
results.Add(new WorkflowStepResult(
|
|
workflow.Id,
|
|
step.Id,
|
|
string.IsNullOrWhiteSpace(step.Label) ? step.Id : step.Label,
|
|
runResult));
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowStepCompleted,
|
|
Message: $"Step '{step.Label}' exited {runResult.ExitCode}.",
|
|
WorkflowId: workflow.Id,
|
|
StepId: step.Id,
|
|
Success: runResult.Success,
|
|
ExitCode: runResult.ExitCode));
|
|
|
|
if (!runResult.Success)
|
|
{
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowCompleted,
|
|
Message: $"Step '{step.Label}' failed.",
|
|
WorkflowId: rootWorkflow.Id,
|
|
Success: false,
|
|
ExitCode: runResult.ExitCode));
|
|
return new WorkflowExecutionResult(
|
|
Success: false,
|
|
StopReason: ExecutionStopReason.CommandFailed,
|
|
Message: $"Step '{step.Label}' failed with exit code {runResult.ExitCode}.",
|
|
Steps: results);
|
|
}
|
|
}
|
|
}
|
|
|
|
onEvent?.Invoke(new RunEvent(
|
|
Category: "workflow",
|
|
Type: RunEventType.WorkflowCompleted,
|
|
Message: "Workflow completed successfully.",
|
|
WorkflowId: rootWorkflow.Id,
|
|
Success: true));
|
|
return new WorkflowExecutionResult(
|
|
Success: true,
|
|
StopReason: null,
|
|
Message: "Workflow completed successfully.",
|
|
Steps: results);
|
|
}
|
|
}
|