feat: Enhance Task Runner with simulation and failure policy support
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added tests for output projection and failure policy population in TaskPackPlanner. - Introduced new failure policy manifest in TestManifests. - Implemented simulation endpoints in the web service for task execution. - Created TaskRunnerServiceOptions for configuration management. - Updated appsettings.json to include TaskRunner configuration. - Enhanced PackRunWorkerService to handle execution graphs and state management. - Added support for parallel execution and conditional steps in the worker service. - Updated documentation to reflect new features and changes in execution flow.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,8 @@ internal interface IBackendOperationsClient
|
||||
|
||||
Task<PolicySimulationResult> SimulatePolicyAsync(string policyId, PolicySimulationInput input, CancellationToken cancellationToken);
|
||||
|
||||
Task<TaskRunnerSimulationResult> SimulateTaskRunnerAsync(TaskRunnerSimulationRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<PolicyActivationResult> ActivatePolicyRevisionAsync(string policyId, int version, PolicyActivationRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<OfflineKitDownloadResult> DownloadOfflineKitAsync(string? bundleId, string destinationDirectory, bool overwrite, bool resume, CancellationToken cancellationToken);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Cli.Services.Models;
|
||||
|
||||
internal sealed record TaskRunnerSimulationRequest(string Manifest, JsonObject? Inputs);
|
||||
|
||||
internal sealed record TaskRunnerSimulationResult(
|
||||
string PlanHash,
|
||||
TaskRunnerSimulationFailurePolicy FailurePolicy,
|
||||
IReadOnlyList<TaskRunnerSimulationStep> Steps,
|
||||
IReadOnlyList<TaskRunnerSimulationOutput> Outputs,
|
||||
bool HasPendingApprovals);
|
||||
|
||||
internal sealed record TaskRunnerSimulationFailurePolicy(int MaxAttempts, int BackoffSeconds, bool ContinueOnError);
|
||||
|
||||
internal sealed record TaskRunnerSimulationStep(
|
||||
string Id,
|
||||
string TemplateId,
|
||||
string Kind,
|
||||
bool Enabled,
|
||||
string Status,
|
||||
string? StatusReason,
|
||||
string? Uses,
|
||||
string? ApprovalId,
|
||||
string? GateMessage,
|
||||
int? MaxParallel,
|
||||
bool ContinueOnError,
|
||||
IReadOnlyList<TaskRunnerSimulationStep> Children);
|
||||
|
||||
internal sealed record TaskRunnerSimulationOutput(
|
||||
string Name,
|
||||
string Type,
|
||||
bool RequiresRuntimeValue,
|
||||
string? PathExpression,
|
||||
string? ValueExpression);
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Cli.Services.Models.Transport;
|
||||
|
||||
internal sealed class TaskRunnerSimulationRequestDocument
|
||||
{
|
||||
public string Manifest { get; set; } = string.Empty;
|
||||
|
||||
public JsonObject? Inputs { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class TaskRunnerSimulationResponseDocument
|
||||
{
|
||||
public string PlanHash { get; set; } = string.Empty;
|
||||
|
||||
public TaskRunnerSimulationFailurePolicyDocument? FailurePolicy { get; set; }
|
||||
|
||||
public List<TaskRunnerSimulationStepDocument>? Steps { get; set; }
|
||||
|
||||
public List<TaskRunnerSimulationOutputDocument>? Outputs { get; set; }
|
||||
|
||||
public bool HasPendingApprovals { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class TaskRunnerSimulationFailurePolicyDocument
|
||||
{
|
||||
public int MaxAttempts { get; set; }
|
||||
|
||||
public int BackoffSeconds { get; set; }
|
||||
|
||||
public bool ContinueOnError { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class TaskRunnerSimulationStepDocument
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public string TemplateId { get; set; } = string.Empty;
|
||||
|
||||
public string Kind { get; set; } = string.Empty;
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
public string? StatusReason { get; set; }
|
||||
|
||||
public string? Uses { get; set; }
|
||||
|
||||
public string? ApprovalId { get; set; }
|
||||
|
||||
public string? GateMessage { get; set; }
|
||||
|
||||
public int? MaxParallel { get; set; }
|
||||
|
||||
public bool ContinueOnError { get; set; }
|
||||
|
||||
public List<TaskRunnerSimulationStepDocument>? Children { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class TaskRunnerSimulationOutputDocument
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
public bool RequiresRuntimeValue { get; set; }
|
||||
|
||||
public string? PathExpression { get; set; }
|
||||
|
||||
public string? ValueExpression { get; set; }
|
||||
}
|
||||
@@ -13,6 +13,7 @@ internal static class CliMetrics
|
||||
private static readonly Counter<long> OfflineKitDownloadCounter = Meter.CreateCounter<long>("stellaops.cli.offline.kit.download.count");
|
||||
private static readonly Counter<long> OfflineKitImportCounter = Meter.CreateCounter<long>("stellaops.cli.offline.kit.import.count");
|
||||
private static readonly Counter<long> PolicySimulationCounter = Meter.CreateCounter<long>("stellaops.cli.policy.simulate.count");
|
||||
private static readonly Counter<long> TaskRunnerSimulationCounter = Meter.CreateCounter<long>("stellaops.cli.taskrunner.simulate.count");
|
||||
private static readonly Counter<long> PolicyActivationCounter = Meter.CreateCounter<long>("stellaops.cli.policy.activate.count");
|
||||
private static readonly Counter<long> SourcesDryRunCounter = Meter.CreateCounter<long>("stellaops.cli.sources.dryrun.count");
|
||||
private static readonly Counter<long> AocVerifyCounter = Meter.CreateCounter<long>("stellaops.cli.aoc.verify.count");
|
||||
@@ -57,6 +58,12 @@ internal static class CliMetrics
|
||||
new("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)
|
||||
});
|
||||
|
||||
public static void RecordTaskRunnerSimulation(string outcome)
|
||||
=> TaskRunnerSimulationCounter.Add(1, new KeyValuePair<string, object?>[]
|
||||
{
|
||||
new("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)
|
||||
});
|
||||
|
||||
public static void RecordPolicyActivation(string outcome)
|
||||
=> PolicyActivationCounter.Add(1, new KeyValuePair<string, object?>[]
|
||||
{
|
||||
|
||||
@@ -8,7 +8,8 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -1535,11 +1536,11 @@ public sealed class CommandHandlersTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandlePolicySimulateAsync_MapsErrorCodes()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalOut = Console.Out;
|
||||
|
||||
public async Task HandlePolicySimulateAsync_MapsErrorCodes()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalOut = Console.Out;
|
||||
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
|
||||
{
|
||||
SimulationException = new PolicyApiException("Missing inputs", HttpStatusCode.BadRequest, "ERR_POL_003")
|
||||
@@ -1566,18 +1567,185 @@ public sealed class CommandHandlersTests
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal(21, Environment.ExitCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandlePolicyActivateAsync_DisplaysInteractiveSummary()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleTaskRunnerSimulateAsync_WritesInteractiveSummary()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalConsole = AnsiConsole.Console;
|
||||
|
||||
var console = new TestConsole();
|
||||
console.Width(120);
|
||||
console.Interactive();
|
||||
console.EmitAnsiSequences();
|
||||
AnsiConsole.Console = console;
|
||||
|
||||
const string manifest = """
|
||||
apiVersion: stellaops.io/pack.v1
|
||||
kind: TaskPack
|
||||
metadata:
|
||||
name: sample-pack
|
||||
spec:
|
||||
steps:
|
||||
- id: prepare
|
||||
run:
|
||||
uses: builtin:prepare
|
||||
- id: approval
|
||||
gate:
|
||||
approval:
|
||||
id: security-review
|
||||
message: Security approval required.
|
||||
""";
|
||||
|
||||
using var manifestFile = new TempFile("pack.yaml", Encoding.UTF8.GetBytes(manifest));
|
||||
|
||||
var simulationResult = new TaskRunnerSimulationResult(
|
||||
"hash-abc123",
|
||||
new TaskRunnerSimulationFailurePolicy(3, 15, false),
|
||||
new[]
|
||||
{
|
||||
new TaskRunnerSimulationStep(
|
||||
"prepare",
|
||||
"prepare",
|
||||
"Run",
|
||||
true,
|
||||
"succeeded",
|
||||
null,
|
||||
"builtin:prepare",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
Array.Empty<TaskRunnerSimulationStep>()),
|
||||
new TaskRunnerSimulationStep(
|
||||
"approval",
|
||||
"approval",
|
||||
"GateApproval",
|
||||
true,
|
||||
"pending",
|
||||
"requires-approval",
|
||||
null,
|
||||
"security-review",
|
||||
"Security approval required.",
|
||||
null,
|
||||
false,
|
||||
Array.Empty<TaskRunnerSimulationStep>())
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new TaskRunnerSimulationOutput("bundlePath", "file", false, "artifacts/report.json", null)
|
||||
},
|
||||
true);
|
||||
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
|
||||
{
|
||||
TaskRunnerSimulationResult = simulationResult
|
||||
};
|
||||
var provider = BuildServiceProvider(backend);
|
||||
|
||||
try
|
||||
{
|
||||
await CommandHandlers.HandleTaskRunnerSimulateAsync(
|
||||
provider,
|
||||
manifestFile.Path,
|
||||
inputsPath: null,
|
||||
format: null,
|
||||
outputPath: null,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, Environment.ExitCode);
|
||||
Assert.NotNull(backend.LastTaskRunnerSimulationRequest);
|
||||
Assert.Contains("approval", console.Output, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Plan Hash", console.Output, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AnsiConsole.Console = originalConsole;
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleTaskRunnerSimulateAsync_WritesJsonOutput()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalOut = Console.Out;
|
||||
|
||||
const string manifest = """
|
||||
apiVersion: stellaops.io/pack.v1
|
||||
kind: TaskPack
|
||||
metadata:
|
||||
name: sample-pack
|
||||
spec:
|
||||
steps:
|
||||
- id: prepare
|
||||
run:
|
||||
uses: builtin:prepare
|
||||
""";
|
||||
|
||||
using var manifestFile = new TempFile("pack.yaml", Encoding.UTF8.GetBytes(manifest));
|
||||
using var inputsFile = new TempFile("inputs.json", Encoding.UTF8.GetBytes("{\"dryRun\":false}"));
|
||||
using var outputDirectory = new TempDirectory();
|
||||
var outputPath = Path.Combine(outputDirectory.Path, "simulation.json");
|
||||
|
||||
var simulationResult = new TaskRunnerSimulationResult(
|
||||
"hash-xyz789",
|
||||
new TaskRunnerSimulationFailurePolicy(2, 10, true),
|
||||
Array.Empty<TaskRunnerSimulationStep>(),
|
||||
Array.Empty<TaskRunnerSimulationOutput>(),
|
||||
false);
|
||||
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
|
||||
{
|
||||
TaskRunnerSimulationResult = simulationResult
|
||||
};
|
||||
var provider = BuildServiceProvider(backend);
|
||||
|
||||
using var writer = new StringWriter();
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
await CommandHandlers.HandleTaskRunnerSimulateAsync(
|
||||
provider,
|
||||
manifestFile.Path,
|
||||
inputsFile.Path,
|
||||
format: "json",
|
||||
outputPath: outputPath,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, Environment.ExitCode);
|
||||
Assert.NotNull(backend.LastTaskRunnerSimulationRequest);
|
||||
|
||||
var consoleOutput = writer.ToString();
|
||||
Assert.Contains("\"planHash\":\"hash-xyz789\"", consoleOutput, StringComparison.Ordinal);
|
||||
|
||||
var fileOutput = await File.ReadAllTextAsync(outputPath);
|
||||
Assert.Contains("\"planHash\":\"hash-xyz789\"", fileOutput, StringComparison.Ordinal);
|
||||
|
||||
Assert.True(backend.LastTaskRunnerSimulationRequest!.Inputs!.TryGetPropertyValue("dryRun", out var dryRunNode));
|
||||
Assert.False(dryRunNode!.GetValue<bool>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandlePolicyActivateAsync_DisplaysInteractiveSummary()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalConsole = AnsiConsole.Console;
|
||||
|
||||
var console = new TestConsole();
|
||||
@@ -2397,7 +2565,15 @@ public sealed class CommandHandlersTests
|
||||
new ReadOnlyCollection<PolicySimulationRuleDelta>(Array.Empty<PolicySimulationRuleDelta>())),
|
||||
null);
|
||||
public PolicyApiException? SimulationException { get; set; }
|
||||
public (string PolicyId, PolicySimulationInput Input)? LastPolicySimulation { get; private set; }
|
||||
public (string PolicyId, PolicySimulationInput Input)? LastPolicySimulation { get; private set; }
|
||||
public TaskRunnerSimulationRequest? LastTaskRunnerSimulationRequest { get; private set; }
|
||||
public TaskRunnerSimulationResult TaskRunnerSimulationResult { get; set; } = new(
|
||||
string.Empty,
|
||||
new TaskRunnerSimulationFailurePolicy(1, 0, false),
|
||||
Array.Empty<TaskRunnerSimulationStep>(),
|
||||
Array.Empty<TaskRunnerSimulationOutput>(),
|
||||
false);
|
||||
public Exception? TaskRunnerSimulationException { get; set; }
|
||||
public PolicyActivationResult ActivationResult { get; set; } = new PolicyActivationResult(
|
||||
"activated",
|
||||
new PolicyActivationRevision(
|
||||
@@ -2486,17 +2662,28 @@ public sealed class CommandHandlersTests
|
||||
public Task<RuntimePolicyEvaluationResult> EvaluateRuntimePolicyAsync(RuntimePolicyEvaluationRequest request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(RuntimePolicyResult);
|
||||
|
||||
public Task<PolicySimulationResult> SimulatePolicyAsync(string policyId, PolicySimulationInput input, CancellationToken cancellationToken)
|
||||
{
|
||||
LastPolicySimulation = (policyId, input);
|
||||
if (SimulationException is not null)
|
||||
{
|
||||
throw SimulationException;
|
||||
}
|
||||
|
||||
return Task.FromResult(SimulationResult);
|
||||
}
|
||||
|
||||
public Task<PolicySimulationResult> SimulatePolicyAsync(string policyId, PolicySimulationInput input, CancellationToken cancellationToken)
|
||||
{
|
||||
LastPolicySimulation = (policyId, input);
|
||||
if (SimulationException is not null)
|
||||
{
|
||||
throw SimulationException;
|
||||
}
|
||||
|
||||
return Task.FromResult(SimulationResult);
|
||||
}
|
||||
|
||||
public Task<TaskRunnerSimulationResult> SimulateTaskRunnerAsync(TaskRunnerSimulationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
LastTaskRunnerSimulationRequest = request;
|
||||
if (TaskRunnerSimulationException is not null)
|
||||
{
|
||||
throw TaskRunnerSimulationException;
|
||||
}
|
||||
|
||||
return Task.FromResult(TaskRunnerSimulationResult);
|
||||
}
|
||||
|
||||
public Task<PolicyActivationResult> ActivatePolicyRevisionAsync(string policyId, int version, PolicyActivationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
LastPolicyActivation = (policyId, version, request);
|
||||
|
||||
Reference in New Issue
Block a user