feat: Implement Policy Engine Evaluation Service and Cache with unit tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

Temp commit to debug
This commit is contained in:
master
2025-11-05 07:35:53 +00:00
parent 40e7f827da
commit 9253620833
125 changed files with 18735 additions and 17215 deletions

View File

@@ -1,13 +1,13 @@
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution;
public interface IPackRunStepExecutor
{
Task<PackRunStepExecutionResult> ExecuteAsync(
PackRunExecutionStep step,
IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters,
CancellationToken cancellationToken);
}
public sealed record PackRunStepExecutionResult(bool Succeeded, string? Error = null);
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution;
public interface IPackRunStepExecutor
{
Task<PackRunStepExecutionResult> ExecuteAsync(
PackRunExecutionStep step,
IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters,
CancellationToken cancellationToken);
}
public sealed record PackRunStepExecutionResult(bool Succeeded, string? Error = null);

View File

@@ -1,86 +1,86 @@
using System.Collections.ObjectModel;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution;
public sealed class PackRunExecutionGraph
{
public static readonly TaskPackPlanFailurePolicy DefaultFailurePolicy = new(1, 0, ContinueOnError: false);
public PackRunExecutionGraph(IReadOnlyList<PackRunExecutionStep> steps, TaskPackPlanFailurePolicy? failurePolicy)
{
Steps = steps ?? throw new ArgumentNullException(nameof(steps));
FailurePolicy = failurePolicy ?? DefaultFailurePolicy;
}
public IReadOnlyList<PackRunExecutionStep> Steps { get; }
public TaskPackPlanFailurePolicy FailurePolicy { get; }
}
public enum PackRunStepKind
{
Unknown = 0,
Run,
GateApproval,
GatePolicy,
Parallel,
Map
}
public sealed class PackRunExecutionStep
{
public PackRunExecutionStep(
string id,
string templateId,
PackRunStepKind kind,
bool enabled,
string? uses,
IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters,
string? approvalId,
string? gateMessage,
int? maxParallel,
bool continueOnError,
IReadOnlyList<PackRunExecutionStep> children)
{
Id = string.IsNullOrWhiteSpace(id) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(id)) : id;
TemplateId = string.IsNullOrWhiteSpace(templateId) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(templateId)) : templateId;
Kind = kind;
Enabled = enabled;
Uses = uses;
Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters));
ApprovalId = approvalId;
GateMessage = gateMessage;
MaxParallel = maxParallel;
ContinueOnError = continueOnError;
Children = children ?? throw new ArgumentNullException(nameof(children));
}
public string Id { get; }
public string TemplateId { get; }
public PackRunStepKind Kind { get; }
public bool Enabled { get; }
public string? Uses { get; }
public IReadOnlyDictionary<string, TaskPackPlanParameterValue> Parameters { get; }
public string? ApprovalId { get; }
public string? GateMessage { get; }
public int? MaxParallel { get; }
public bool ContinueOnError { get; }
public IReadOnlyList<PackRunExecutionStep> Children { get; }
public static IReadOnlyDictionary<string, TaskPackPlanParameterValue> EmptyParameters { get; } =
new ReadOnlyDictionary<string, TaskPackPlanParameterValue>(new Dictionary<string, TaskPackPlanParameterValue>(StringComparer.Ordinal));
public static IReadOnlyList<PackRunExecutionStep> EmptyChildren { get; } =
Array.Empty<PackRunExecutionStep>();
}
using System.Collections.ObjectModel;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution;
public sealed class PackRunExecutionGraph
{
public static readonly TaskPackPlanFailurePolicy DefaultFailurePolicy = new(1, 0, ContinueOnError: false);
public PackRunExecutionGraph(IReadOnlyList<PackRunExecutionStep> steps, TaskPackPlanFailurePolicy? failurePolicy)
{
Steps = steps ?? throw new ArgumentNullException(nameof(steps));
FailurePolicy = failurePolicy ?? DefaultFailurePolicy;
}
public IReadOnlyList<PackRunExecutionStep> Steps { get; }
public TaskPackPlanFailurePolicy FailurePolicy { get; }
}
public enum PackRunStepKind
{
Unknown = 0,
Run,
GateApproval,
GatePolicy,
Parallel,
Map
}
public sealed class PackRunExecutionStep
{
public PackRunExecutionStep(
string id,
string templateId,
PackRunStepKind kind,
bool enabled,
string? uses,
IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters,
string? approvalId,
string? gateMessage,
int? maxParallel,
bool continueOnError,
IReadOnlyList<PackRunExecutionStep> children)
{
Id = string.IsNullOrWhiteSpace(id) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(id)) : id;
TemplateId = string.IsNullOrWhiteSpace(templateId) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(templateId)) : templateId;
Kind = kind;
Enabled = enabled;
Uses = uses;
Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters));
ApprovalId = approvalId;
GateMessage = gateMessage;
MaxParallel = maxParallel;
ContinueOnError = continueOnError;
Children = children ?? throw new ArgumentNullException(nameof(children));
}
public string Id { get; }
public string TemplateId { get; }
public PackRunStepKind Kind { get; }
public bool Enabled { get; }
public string? Uses { get; }
public IReadOnlyDictionary<string, TaskPackPlanParameterValue> Parameters { get; }
public string? ApprovalId { get; }
public string? GateMessage { get; }
public int? MaxParallel { get; }
public bool ContinueOnError { get; }
public IReadOnlyList<PackRunExecutionStep> Children { get; }
public static IReadOnlyDictionary<string, TaskPackPlanParameterValue> EmptyParameters { get; } =
new ReadOnlyDictionary<string, TaskPackPlanParameterValue>(new Dictionary<string, TaskPackPlanParameterValue>(StringComparer.Ordinal));
public static IReadOnlyList<PackRunExecutionStep> EmptyChildren { get; } =
Array.Empty<PackRunExecutionStep>();
}

View File

@@ -1,77 +1,77 @@
using System.Collections.ObjectModel;
using System.Text.Json.Nodes;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution;
public sealed class PackRunExecutionGraphBuilder
{
public PackRunExecutionGraph Build(TaskPackPlan plan)
{
ArgumentNullException.ThrowIfNull(plan);
var steps = plan.Steps.Select(ConvertStep).ToList();
var failurePolicy = plan.FailurePolicy;
return new PackRunExecutionGraph(steps, failurePolicy);
}
private static PackRunExecutionStep ConvertStep(TaskPackPlanStep step)
{
var kind = DetermineKind(step.Type);
var parameters = step.Parameters is null
? PackRunExecutionStep.EmptyParameters
: new ReadOnlyDictionary<string, TaskPackPlanParameterValue>(
new Dictionary<string, TaskPackPlanParameterValue>(step.Parameters, StringComparer.Ordinal));
var children = step.Children is null
? PackRunExecutionStep.EmptyChildren
: step.Children.Select(ConvertStep).ToList();
var maxParallel = TryGetInt(parameters, "maxParallel");
var continueOnError = TryGetBool(parameters, "continueOnError");
return new PackRunExecutionStep(
step.Id,
step.TemplateId,
kind,
step.Enabled,
step.Uses,
parameters,
step.ApprovalId,
step.GateMessage,
maxParallel,
continueOnError,
children);
}
private static PackRunStepKind DetermineKind(string? type)
=> type switch
{
"run" => PackRunStepKind.Run,
"gate.approval" => PackRunStepKind.GateApproval,
"gate.policy" => PackRunStepKind.GatePolicy,
"parallel" => PackRunStepKind.Parallel,
"map" => PackRunStepKind.Map,
_ => PackRunStepKind.Unknown
};
private static int? TryGetInt(IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters, string key)
{
if (!parameters.TryGetValue(key, out var value) || value.Value is not JsonValue jsonValue)
{
return null;
}
return jsonValue.TryGetValue<int>(out var result) ? result : null;
}
private static bool TryGetBool(IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters, string key)
{
if (!parameters.TryGetValue(key, out var value) || value.Value is not JsonValue jsonValue)
{
return false;
}
return jsonValue.TryGetValue<bool>(out var result) && result;
}
}
using System.Collections.ObjectModel;
using System.Text.Json.Nodes;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution;
public sealed class PackRunExecutionGraphBuilder
{
public PackRunExecutionGraph Build(TaskPackPlan plan)
{
ArgumentNullException.ThrowIfNull(plan);
var steps = plan.Steps.Select(ConvertStep).ToList();
var failurePolicy = plan.FailurePolicy;
return new PackRunExecutionGraph(steps, failurePolicy);
}
private static PackRunExecutionStep ConvertStep(TaskPackPlanStep step)
{
var kind = DetermineKind(step.Type);
var parameters = step.Parameters is null
? PackRunExecutionStep.EmptyParameters
: new ReadOnlyDictionary<string, TaskPackPlanParameterValue>(
new Dictionary<string, TaskPackPlanParameterValue>(step.Parameters, StringComparer.Ordinal));
var children = step.Children is null
? PackRunExecutionStep.EmptyChildren
: step.Children.Select(ConvertStep).ToList();
var maxParallel = TryGetInt(parameters, "maxParallel");
var continueOnError = TryGetBool(parameters, "continueOnError");
return new PackRunExecutionStep(
step.Id,
step.TemplateId,
kind,
step.Enabled,
step.Uses,
parameters,
step.ApprovalId,
step.GateMessage,
maxParallel,
continueOnError,
children);
}
private static PackRunStepKind DetermineKind(string? type)
=> type switch
{
"run" => PackRunStepKind.Run,
"gate.approval" => PackRunStepKind.GateApproval,
"gate.policy" => PackRunStepKind.GatePolicy,
"parallel" => PackRunStepKind.Parallel,
"map" => PackRunStepKind.Map,
_ => PackRunStepKind.Unknown
};
private static int? TryGetInt(IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters, string key)
{
if (!parameters.TryGetValue(key, out var value) || value.Value is not JsonValue jsonValue)
{
return null;
}
return jsonValue.TryGetValue<int>(out var result) ? result : null;
}
private static bool TryGetBool(IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters, string key)
{
if (!parameters.TryGetValue(key, out var value) || value.Value is not JsonValue jsonValue)
{
return false;
}
return jsonValue.TryGetValue<bool>(out var result) && result;
}
}

View File

@@ -1,159 +1,159 @@
using System.Collections.ObjectModel;
using System.Linq;
namespace StellaOps.TaskRunner.Core.Execution;
public static class PackRunGateStateUpdater
{
public static PackRunGateStateUpdateResult Apply(
PackRunState state,
PackRunExecutionGraph graph,
PackRunApprovalCoordinator coordinator,
DateTimeOffset timestamp)
{
ArgumentNullException.ThrowIfNull(state);
ArgumentNullException.ThrowIfNull(graph);
ArgumentNullException.ThrowIfNull(coordinator);
var approvals = coordinator.GetApprovals()
.SelectMany(approval => approval.StepIds.Select(stepId => (stepId, approval)))
.GroupBy(tuple => tuple.stepId, StringComparer.Ordinal)
.ToDictionary(
group => group.Key,
group => group.First().approval,
StringComparer.Ordinal);
var mutable = new Dictionary<string, PackRunStepStateRecord>(state.Steps, StringComparer.Ordinal);
var changed = false;
var hasBlockingFailure = false;
foreach (var step in EnumerateSteps(graph.Steps))
{
if (!mutable.TryGetValue(step.Id, out var record))
{
continue;
}
switch (step.Kind)
{
case PackRunStepKind.GateApproval:
if (!approvals.TryGetValue(step.Id, out var approvalState))
{
continue;
}
switch (approvalState.Status)
{
case PackRunApprovalStatus.Pending:
break;
case PackRunApprovalStatus.Approved:
if (record.Status != PackRunStepExecutionStatus.Succeeded || record.StatusReason is not null)
{
mutable[step.Id] = record with
{
Status = PackRunStepExecutionStatus.Succeeded,
StatusReason = null,
LastTransitionAt = timestamp,
NextAttemptAt = null
};
changed = true;
}
break;
case PackRunApprovalStatus.Rejected:
case PackRunApprovalStatus.Expired:
var failureReason = BuildFailureReason(approvalState);
if (record.Status != PackRunStepExecutionStatus.Failed ||
!string.Equals(record.StatusReason, failureReason, StringComparison.Ordinal))
{
mutable[step.Id] = record with
{
Status = PackRunStepExecutionStatus.Failed,
StatusReason = failureReason,
LastTransitionAt = timestamp,
NextAttemptAt = null
};
changed = true;
}
hasBlockingFailure = true;
break;
}
break;
case PackRunStepKind.GatePolicy:
if (record.Status == PackRunStepExecutionStatus.Pending &&
string.Equals(record.StatusReason, "requires-policy", StringComparison.Ordinal))
{
mutable[step.Id] = record with
{
Status = PackRunStepExecutionStatus.Succeeded,
StatusReason = null,
LastTransitionAt = timestamp,
NextAttemptAt = null
};
changed = true;
}
break;
}
}
if (!changed)
{
return new PackRunGateStateUpdateResult(state, hasBlockingFailure);
}
var updatedState = state with
{
UpdatedAt = timestamp,
Steps = new ReadOnlyDictionary<string, PackRunStepStateRecord>(mutable)
};
return new PackRunGateStateUpdateResult(updatedState, hasBlockingFailure);
}
private static IEnumerable<PackRunExecutionStep> EnumerateSteps(IReadOnlyList<PackRunExecutionStep> steps)
{
if (steps.Count == 0)
{
yield break;
}
foreach (var step in steps)
{
yield return step;
if (step.Children.Count > 0)
{
foreach (var child in EnumerateSteps(step.Children))
{
yield return child;
}
}
}
}
private static string BuildFailureReason(PackRunApprovalState state)
{
var baseReason = state.Status switch
{
PackRunApprovalStatus.Rejected => "approval-rejected",
PackRunApprovalStatus.Expired => "approval-expired",
_ => "approval-invalid"
};
if (string.IsNullOrWhiteSpace(state.Summary))
{
return baseReason;
}
var summary = state.Summary.Trim();
return $"{baseReason}:{summary}";
}
}
public readonly record struct PackRunGateStateUpdateResult(PackRunState State, bool HasBlockingFailure);
using System.Collections.ObjectModel;
using System.Linq;
namespace StellaOps.TaskRunner.Core.Execution;
public static class PackRunGateStateUpdater
{
public static PackRunGateStateUpdateResult Apply(
PackRunState state,
PackRunExecutionGraph graph,
PackRunApprovalCoordinator coordinator,
DateTimeOffset timestamp)
{
ArgumentNullException.ThrowIfNull(state);
ArgumentNullException.ThrowIfNull(graph);
ArgumentNullException.ThrowIfNull(coordinator);
var approvals = coordinator.GetApprovals()
.SelectMany(approval => approval.StepIds.Select(stepId => (stepId, approval)))
.GroupBy(tuple => tuple.stepId, StringComparer.Ordinal)
.ToDictionary(
group => group.Key,
group => group.First().approval,
StringComparer.Ordinal);
var mutable = new Dictionary<string, PackRunStepStateRecord>(state.Steps, StringComparer.Ordinal);
var changed = false;
var hasBlockingFailure = false;
foreach (var step in EnumerateSteps(graph.Steps))
{
if (!mutable.TryGetValue(step.Id, out var record))
{
continue;
}
switch (step.Kind)
{
case PackRunStepKind.GateApproval:
if (!approvals.TryGetValue(step.Id, out var approvalState))
{
continue;
}
switch (approvalState.Status)
{
case PackRunApprovalStatus.Pending:
break;
case PackRunApprovalStatus.Approved:
if (record.Status != PackRunStepExecutionStatus.Succeeded || record.StatusReason is not null)
{
mutable[step.Id] = record with
{
Status = PackRunStepExecutionStatus.Succeeded,
StatusReason = null,
LastTransitionAt = timestamp,
NextAttemptAt = null
};
changed = true;
}
break;
case PackRunApprovalStatus.Rejected:
case PackRunApprovalStatus.Expired:
var failureReason = BuildFailureReason(approvalState);
if (record.Status != PackRunStepExecutionStatus.Failed ||
!string.Equals(record.StatusReason, failureReason, StringComparison.Ordinal))
{
mutable[step.Id] = record with
{
Status = PackRunStepExecutionStatus.Failed,
StatusReason = failureReason,
LastTransitionAt = timestamp,
NextAttemptAt = null
};
changed = true;
}
hasBlockingFailure = true;
break;
}
break;
case PackRunStepKind.GatePolicy:
if (record.Status == PackRunStepExecutionStatus.Pending &&
string.Equals(record.StatusReason, "requires-policy", StringComparison.Ordinal))
{
mutable[step.Id] = record with
{
Status = PackRunStepExecutionStatus.Succeeded,
StatusReason = null,
LastTransitionAt = timestamp,
NextAttemptAt = null
};
changed = true;
}
break;
}
}
if (!changed)
{
return new PackRunGateStateUpdateResult(state, hasBlockingFailure);
}
var updatedState = state with
{
UpdatedAt = timestamp,
Steps = new ReadOnlyDictionary<string, PackRunStepStateRecord>(mutable)
};
return new PackRunGateStateUpdateResult(updatedState, hasBlockingFailure);
}
private static IEnumerable<PackRunExecutionStep> EnumerateSteps(IReadOnlyList<PackRunExecutionStep> steps)
{
if (steps.Count == 0)
{
yield break;
}
foreach (var step in steps)
{
yield return step;
if (step.Children.Count > 0)
{
foreach (var child in EnumerateSteps(step.Children))
{
yield return child;
}
}
}
}
private static string BuildFailureReason(PackRunApprovalState state)
{
var baseReason = state.Status switch
{
PackRunApprovalStatus.Rejected => "approval-rejected",
PackRunApprovalStatus.Expired => "approval-expired",
_ => "approval-invalid"
};
if (string.IsNullOrWhiteSpace(state.Summary))
{
return baseReason;
}
var summary = state.Summary.Trim();
return $"{baseReason}:{summary}";
}
}
public readonly record struct PackRunGateStateUpdateResult(PackRunState State, bool HasBlockingFailure);

View File

@@ -1,50 +1,50 @@
using System.Collections.ObjectModel;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution;
public sealed record PackRunState(
string RunId,
string PlanHash,
TaskPackPlanFailurePolicy FailurePolicy,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
IReadOnlyDictionary<string, PackRunStepStateRecord> Steps)
{
public static PackRunState Create(
string runId,
string planHash,
TaskPackPlanFailurePolicy failurePolicy,
IReadOnlyDictionary<string, PackRunStepStateRecord> steps,
DateTimeOffset timestamp)
=> new(
runId,
planHash,
failurePolicy,
timestamp,
timestamp,
new ReadOnlyDictionary<string, PackRunStepStateRecord>(new Dictionary<string, PackRunStepStateRecord>(steps, StringComparer.Ordinal)));
}
public sealed record PackRunStepStateRecord(
string StepId,
PackRunStepKind Kind,
bool Enabled,
bool ContinueOnError,
int? MaxParallel,
string? ApprovalId,
string? GateMessage,
PackRunStepExecutionStatus Status,
int Attempts,
DateTimeOffset? LastTransitionAt,
DateTimeOffset? NextAttemptAt,
string? StatusReason);
public interface IPackRunStateStore
{
Task<PackRunState?> GetAsync(string runId, CancellationToken cancellationToken);
Task SaveAsync(PackRunState state, CancellationToken cancellationToken);
Task<IReadOnlyList<PackRunState>> ListAsync(CancellationToken cancellationToken);
}
using System.Collections.ObjectModel;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution;
public sealed record PackRunState(
string RunId,
string PlanHash,
TaskPackPlanFailurePolicy FailurePolicy,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt,
IReadOnlyDictionary<string, PackRunStepStateRecord> Steps)
{
public static PackRunState Create(
string runId,
string planHash,
TaskPackPlanFailurePolicy failurePolicy,
IReadOnlyDictionary<string, PackRunStepStateRecord> steps,
DateTimeOffset timestamp)
=> new(
runId,
planHash,
failurePolicy,
timestamp,
timestamp,
new ReadOnlyDictionary<string, PackRunStepStateRecord>(new Dictionary<string, PackRunStepStateRecord>(steps, StringComparer.Ordinal)));
}
public sealed record PackRunStepStateRecord(
string StepId,
PackRunStepKind Kind,
bool Enabled,
bool ContinueOnError,
int? MaxParallel,
string? ApprovalId,
string? GateMessage,
PackRunStepExecutionStatus Status,
int Attempts,
DateTimeOffset? LastTransitionAt,
DateTimeOffset? NextAttemptAt,
string? StatusReason);
public interface IPackRunStateStore
{
Task<PackRunState?> GetAsync(string runId, CancellationToken cancellationToken);
Task SaveAsync(PackRunState state, CancellationToken cancellationToken);
Task<IReadOnlyList<PackRunState>> ListAsync(CancellationToken cancellationToken);
}

View File

@@ -1,121 +1,121 @@
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution;
public static class PackRunStepStateMachine
{
public static PackRunStepState Create(DateTimeOffset? createdAt = null)
=> new(PackRunStepExecutionStatus.Pending, Attempts: 0, createdAt, NextAttemptAt: null);
public static PackRunStepState Start(PackRunStepState state, DateTimeOffset startedAt)
{
ArgumentNullException.ThrowIfNull(state);
if (state.Status is not PackRunStepExecutionStatus.Pending)
{
throw new InvalidOperationException($"Cannot start step from status {state.Status}.");
}
return state with
{
Status = PackRunStepExecutionStatus.Running,
LastTransitionAt = startedAt,
NextAttemptAt = null
};
}
public static PackRunStepState CompleteSuccess(PackRunStepState state, DateTimeOffset completedAt)
{
ArgumentNullException.ThrowIfNull(state);
if (state.Status is not PackRunStepExecutionStatus.Running)
{
throw new InvalidOperationException($"Cannot complete step from status {state.Status}.");
}
return state with
{
Status = PackRunStepExecutionStatus.Succeeded,
Attempts = state.Attempts + 1,
LastTransitionAt = completedAt,
NextAttemptAt = null
};
}
public static PackRunStepFailureResult RegisterFailure(
PackRunStepState state,
DateTimeOffset failedAt,
TaskPackPlanFailurePolicy failurePolicy)
{
ArgumentNullException.ThrowIfNull(state);
ArgumentNullException.ThrowIfNull(failurePolicy);
if (state.Status is not PackRunStepExecutionStatus.Running)
{
throw new InvalidOperationException($"Cannot register failure from status {state.Status}.");
}
var attempts = state.Attempts + 1;
if (attempts < failurePolicy.MaxAttempts)
{
var backoff = TimeSpan.FromSeconds(Math.Max(0, failurePolicy.BackoffSeconds));
var nextAttemptAt = failedAt + backoff;
var nextState = state with
{
Status = PackRunStepExecutionStatus.Pending,
Attempts = attempts,
LastTransitionAt = failedAt,
NextAttemptAt = nextAttemptAt
};
return new PackRunStepFailureResult(nextState, PackRunStepFailureOutcome.Retry);
}
var finalState = state with
{
Status = PackRunStepExecutionStatus.Failed,
Attempts = attempts,
LastTransitionAt = failedAt,
NextAttemptAt = null
};
return new PackRunStepFailureResult(finalState, PackRunStepFailureOutcome.Abort);
}
public static PackRunStepState Skip(PackRunStepState state, DateTimeOffset skippedAt)
{
ArgumentNullException.ThrowIfNull(state);
if (state.Status is not PackRunStepExecutionStatus.Pending)
{
throw new InvalidOperationException($"Cannot skip step from status {state.Status}.");
}
return state with
{
Status = PackRunStepExecutionStatus.Skipped,
LastTransitionAt = skippedAt,
NextAttemptAt = null
};
}
}
public sealed record PackRunStepState(
PackRunStepExecutionStatus Status,
int Attempts,
DateTimeOffset? LastTransitionAt,
DateTimeOffset? NextAttemptAt);
public enum PackRunStepExecutionStatus
{
Pending = 0,
Running,
Succeeded,
Failed,
Skipped
}
public readonly record struct PackRunStepFailureResult(PackRunStepState State, PackRunStepFailureOutcome Outcome);
public enum PackRunStepFailureOutcome
{
Retry = 0,
Abort
}
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution;
public static class PackRunStepStateMachine
{
public static PackRunStepState Create(DateTimeOffset? createdAt = null)
=> new(PackRunStepExecutionStatus.Pending, Attempts: 0, createdAt, NextAttemptAt: null);
public static PackRunStepState Start(PackRunStepState state, DateTimeOffset startedAt)
{
ArgumentNullException.ThrowIfNull(state);
if (state.Status is not PackRunStepExecutionStatus.Pending)
{
throw new InvalidOperationException($"Cannot start step from status {state.Status}.");
}
return state with
{
Status = PackRunStepExecutionStatus.Running,
LastTransitionAt = startedAt,
NextAttemptAt = null
};
}
public static PackRunStepState CompleteSuccess(PackRunStepState state, DateTimeOffset completedAt)
{
ArgumentNullException.ThrowIfNull(state);
if (state.Status is not PackRunStepExecutionStatus.Running)
{
throw new InvalidOperationException($"Cannot complete step from status {state.Status}.");
}
return state with
{
Status = PackRunStepExecutionStatus.Succeeded,
Attempts = state.Attempts + 1,
LastTransitionAt = completedAt,
NextAttemptAt = null
};
}
public static PackRunStepFailureResult RegisterFailure(
PackRunStepState state,
DateTimeOffset failedAt,
TaskPackPlanFailurePolicy failurePolicy)
{
ArgumentNullException.ThrowIfNull(state);
ArgumentNullException.ThrowIfNull(failurePolicy);
if (state.Status is not PackRunStepExecutionStatus.Running)
{
throw new InvalidOperationException($"Cannot register failure from status {state.Status}.");
}
var attempts = state.Attempts + 1;
if (attempts < failurePolicy.MaxAttempts)
{
var backoff = TimeSpan.FromSeconds(Math.Max(0, failurePolicy.BackoffSeconds));
var nextAttemptAt = failedAt + backoff;
var nextState = state with
{
Status = PackRunStepExecutionStatus.Pending,
Attempts = attempts,
LastTransitionAt = failedAt,
NextAttemptAt = nextAttemptAt
};
return new PackRunStepFailureResult(nextState, PackRunStepFailureOutcome.Retry);
}
var finalState = state with
{
Status = PackRunStepExecutionStatus.Failed,
Attempts = attempts,
LastTransitionAt = failedAt,
NextAttemptAt = null
};
return new PackRunStepFailureResult(finalState, PackRunStepFailureOutcome.Abort);
}
public static PackRunStepState Skip(PackRunStepState state, DateTimeOffset skippedAt)
{
ArgumentNullException.ThrowIfNull(state);
if (state.Status is not PackRunStepExecutionStatus.Pending)
{
throw new InvalidOperationException($"Cannot skip step from status {state.Status}.");
}
return state with
{
Status = PackRunStepExecutionStatus.Skipped,
LastTransitionAt = skippedAt,
NextAttemptAt = null
};
}
}
public sealed record PackRunStepState(
PackRunStepExecutionStatus Status,
int Attempts,
DateTimeOffset? LastTransitionAt,
DateTimeOffset? NextAttemptAt);
public enum PackRunStepExecutionStatus
{
Pending = 0,
Running,
Succeeded,
Failed,
Skipped
}
public readonly record struct PackRunStepFailureResult(PackRunStepState State, PackRunStepFailureOutcome Outcome);
public enum PackRunStepFailureOutcome
{
Retry = 0,
Abort
}

View File

@@ -1,78 +1,78 @@
using System.Collections.ObjectModel;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution.Simulation;
public sealed class PackRunSimulationEngine
{
private readonly PackRunExecutionGraphBuilder graphBuilder;
public PackRunSimulationEngine()
{
graphBuilder = new PackRunExecutionGraphBuilder();
}
public PackRunSimulationResult Simulate(TaskPackPlan plan)
{
ArgumentNullException.ThrowIfNull(plan);
var graph = graphBuilder.Build(plan);
var steps = graph.Steps.Select(ConvertStep).ToList();
var outputs = BuildOutputs(plan.Outputs);
return new PackRunSimulationResult(steps, outputs, graph.FailurePolicy);
}
private static PackRunSimulationNode ConvertStep(PackRunExecutionStep step)
{
var status = DetermineStatus(step);
var children = step.Children.Count == 0
? PackRunSimulationNode.Empty
: new ReadOnlyCollection<PackRunSimulationNode>(step.Children.Select(ConvertStep).ToList());
return new PackRunSimulationNode(
step.Id,
step.TemplateId,
step.Kind,
step.Enabled,
step.Uses,
step.ApprovalId,
step.GateMessage,
step.Parameters,
step.MaxParallel,
step.ContinueOnError,
status,
children);
}
private static PackRunSimulationStatus DetermineStatus(PackRunExecutionStep step)
{
if (!step.Enabled)
{
return PackRunSimulationStatus.Skipped;
}
return step.Kind switch
{
PackRunStepKind.GateApproval => PackRunSimulationStatus.RequiresApproval,
PackRunStepKind.GatePolicy => PackRunSimulationStatus.RequiresPolicy,
_ => PackRunSimulationStatus.Pending
};
}
private static IReadOnlyList<PackRunSimulationOutput> BuildOutputs(IReadOnlyList<TaskPackPlanOutput> outputs)
{
if (outputs.Count == 0)
{
return PackRunSimulationOutput.Empty;
}
var list = new List<PackRunSimulationOutput>(outputs.Count);
foreach (var output in outputs)
{
list.Add(new PackRunSimulationOutput(output.Name, output.Type, output.Path, output.Expression));
}
return new ReadOnlyCollection<PackRunSimulationOutput>(list);
}
}
using System.Collections.ObjectModel;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution.Simulation;
public sealed class PackRunSimulationEngine
{
private readonly PackRunExecutionGraphBuilder graphBuilder;
public PackRunSimulationEngine()
{
graphBuilder = new PackRunExecutionGraphBuilder();
}
public PackRunSimulationResult Simulate(TaskPackPlan plan)
{
ArgumentNullException.ThrowIfNull(plan);
var graph = graphBuilder.Build(plan);
var steps = graph.Steps.Select(ConvertStep).ToList();
var outputs = BuildOutputs(plan.Outputs);
return new PackRunSimulationResult(steps, outputs, graph.FailurePolicy);
}
private static PackRunSimulationNode ConvertStep(PackRunExecutionStep step)
{
var status = DetermineStatus(step);
var children = step.Children.Count == 0
? PackRunSimulationNode.Empty
: new ReadOnlyCollection<PackRunSimulationNode>(step.Children.Select(ConvertStep).ToList());
return new PackRunSimulationNode(
step.Id,
step.TemplateId,
step.Kind,
step.Enabled,
step.Uses,
step.ApprovalId,
step.GateMessage,
step.Parameters,
step.MaxParallel,
step.ContinueOnError,
status,
children);
}
private static PackRunSimulationStatus DetermineStatus(PackRunExecutionStep step)
{
if (!step.Enabled)
{
return PackRunSimulationStatus.Skipped;
}
return step.Kind switch
{
PackRunStepKind.GateApproval => PackRunSimulationStatus.RequiresApproval,
PackRunStepKind.GatePolicy => PackRunSimulationStatus.RequiresPolicy,
_ => PackRunSimulationStatus.Pending
};
}
private static IReadOnlyList<PackRunSimulationOutput> BuildOutputs(IReadOnlyList<TaskPackPlanOutput> outputs)
{
if (outputs.Count == 0)
{
return PackRunSimulationOutput.Empty;
}
var list = new List<PackRunSimulationOutput>(outputs.Count);
foreach (var output in outputs)
{
list.Add(new PackRunSimulationOutput(output.Name, output.Type, output.Path, output.Expression));
}
return new ReadOnlyCollection<PackRunSimulationOutput>(list);
}
}

View File

@@ -1,131 +1,131 @@
using System.Collections.ObjectModel;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution.Simulation;
public sealed class PackRunSimulationResult
{
public PackRunSimulationResult(
IReadOnlyList<PackRunSimulationNode> steps,
IReadOnlyList<PackRunSimulationOutput> outputs,
TaskPackPlanFailurePolicy failurePolicy)
{
Steps = steps ?? throw new ArgumentNullException(nameof(steps));
Outputs = outputs ?? throw new ArgumentNullException(nameof(outputs));
FailurePolicy = failurePolicy ?? throw new ArgumentNullException(nameof(failurePolicy));
}
public IReadOnlyList<PackRunSimulationNode> Steps { get; }
public IReadOnlyList<PackRunSimulationOutput> Outputs { get; }
public TaskPackPlanFailurePolicy FailurePolicy { get; }
public bool HasPendingApprovals => Steps.Any(ContainsApprovalRequirement);
private static bool ContainsApprovalRequirement(PackRunSimulationNode node)
{
if (node.Status is PackRunSimulationStatus.RequiresApproval or PackRunSimulationStatus.RequiresPolicy)
{
return true;
}
return node.Children.Any(ContainsApprovalRequirement);
}
}
public sealed class PackRunSimulationNode
{
public PackRunSimulationNode(
string id,
string templateId,
PackRunStepKind kind,
bool enabled,
string? uses,
string? approvalId,
string? gateMessage,
IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters,
int? maxParallel,
bool continueOnError,
PackRunSimulationStatus status,
IReadOnlyList<PackRunSimulationNode> children)
{
Id = string.IsNullOrWhiteSpace(id) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(id)) : id;
TemplateId = string.IsNullOrWhiteSpace(templateId) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(templateId)) : templateId;
Kind = kind;
Enabled = enabled;
Uses = uses;
ApprovalId = approvalId;
GateMessage = gateMessage;
Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters));
MaxParallel = maxParallel;
ContinueOnError = continueOnError;
Status = status;
Children = children ?? throw new ArgumentNullException(nameof(children));
}
public string Id { get; }
public string TemplateId { get; }
public PackRunStepKind Kind { get; }
public bool Enabled { get; }
public string? Uses { get; }
public string? ApprovalId { get; }
public string? GateMessage { get; }
public IReadOnlyDictionary<string, TaskPackPlanParameterValue> Parameters { get; }
public int? MaxParallel { get; }
public bool ContinueOnError { get; }
public PackRunSimulationStatus Status { get; }
public IReadOnlyList<PackRunSimulationNode> Children { get; }
public static IReadOnlyList<PackRunSimulationNode> Empty { get; } =
new ReadOnlyCollection<PackRunSimulationNode>(Array.Empty<PackRunSimulationNode>());
}
public enum PackRunSimulationStatus
{
Pending = 0,
Skipped,
RequiresApproval,
RequiresPolicy
}
public sealed class PackRunSimulationOutput
{
public PackRunSimulationOutput(
string name,
string type,
TaskPackPlanParameterValue? path,
TaskPackPlanParameterValue? expression)
{
Name = string.IsNullOrWhiteSpace(name) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)) : name;
Type = string.IsNullOrWhiteSpace(type) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(type)) : type;
Path = path;
Expression = expression;
}
public string Name { get; }
public string Type { get; }
public TaskPackPlanParameterValue? Path { get; }
public TaskPackPlanParameterValue? Expression { get; }
public bool RequiresRuntimeValue =>
(Path?.RequiresRuntimeValue ?? false) ||
(Expression?.RequiresRuntimeValue ?? false);
public static IReadOnlyList<PackRunSimulationOutput> Empty { get; } =
new ReadOnlyCollection<PackRunSimulationOutput>(Array.Empty<PackRunSimulationOutput>());
}
using System.Collections.ObjectModel;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Core.Execution.Simulation;
public sealed class PackRunSimulationResult
{
public PackRunSimulationResult(
IReadOnlyList<PackRunSimulationNode> steps,
IReadOnlyList<PackRunSimulationOutput> outputs,
TaskPackPlanFailurePolicy failurePolicy)
{
Steps = steps ?? throw new ArgumentNullException(nameof(steps));
Outputs = outputs ?? throw new ArgumentNullException(nameof(outputs));
FailurePolicy = failurePolicy ?? throw new ArgumentNullException(nameof(failurePolicy));
}
public IReadOnlyList<PackRunSimulationNode> Steps { get; }
public IReadOnlyList<PackRunSimulationOutput> Outputs { get; }
public TaskPackPlanFailurePolicy FailurePolicy { get; }
public bool HasPendingApprovals => Steps.Any(ContainsApprovalRequirement);
private static bool ContainsApprovalRequirement(PackRunSimulationNode node)
{
if (node.Status is PackRunSimulationStatus.RequiresApproval or PackRunSimulationStatus.RequiresPolicy)
{
return true;
}
return node.Children.Any(ContainsApprovalRequirement);
}
}
public sealed class PackRunSimulationNode
{
public PackRunSimulationNode(
string id,
string templateId,
PackRunStepKind kind,
bool enabled,
string? uses,
string? approvalId,
string? gateMessage,
IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters,
int? maxParallel,
bool continueOnError,
PackRunSimulationStatus status,
IReadOnlyList<PackRunSimulationNode> children)
{
Id = string.IsNullOrWhiteSpace(id) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(id)) : id;
TemplateId = string.IsNullOrWhiteSpace(templateId) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(templateId)) : templateId;
Kind = kind;
Enabled = enabled;
Uses = uses;
ApprovalId = approvalId;
GateMessage = gateMessage;
Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters));
MaxParallel = maxParallel;
ContinueOnError = continueOnError;
Status = status;
Children = children ?? throw new ArgumentNullException(nameof(children));
}
public string Id { get; }
public string TemplateId { get; }
public PackRunStepKind Kind { get; }
public bool Enabled { get; }
public string? Uses { get; }
public string? ApprovalId { get; }
public string? GateMessage { get; }
public IReadOnlyDictionary<string, TaskPackPlanParameterValue> Parameters { get; }
public int? MaxParallel { get; }
public bool ContinueOnError { get; }
public PackRunSimulationStatus Status { get; }
public IReadOnlyList<PackRunSimulationNode> Children { get; }
public static IReadOnlyList<PackRunSimulationNode> Empty { get; } =
new ReadOnlyCollection<PackRunSimulationNode>(Array.Empty<PackRunSimulationNode>());
}
public enum PackRunSimulationStatus
{
Pending = 0,
Skipped,
RequiresApproval,
RequiresPolicy
}
public sealed class PackRunSimulationOutput
{
public PackRunSimulationOutput(
string name,
string type,
TaskPackPlanParameterValue? path,
TaskPackPlanParameterValue? expression)
{
Name = string.IsNullOrWhiteSpace(name) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)) : name;
Type = string.IsNullOrWhiteSpace(type) ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(type)) : type;
Path = path;
Expression = expression;
}
public string Name { get; }
public string Type { get; }
public TaskPackPlanParameterValue? Path { get; }
public TaskPackPlanParameterValue? Expression { get; }
public bool RequiresRuntimeValue =>
(Path?.RequiresRuntimeValue ?? false) ||
(Expression?.RequiresRuntimeValue ?? false);
public static IReadOnlyList<PackRunSimulationOutput> Empty { get; } =
new ReadOnlyCollection<PackRunSimulationOutput>(Array.Empty<PackRunSimulationOutput>());
}