Implement MongoDB-based storage for Pack Run approval, artifact, log, and state management
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added MongoPackRunApprovalStore for managing approval states with MongoDB.
- Introduced MongoPackRunArtifactUploader for uploading and storing artifacts.
- Created MongoPackRunLogStore to handle logging of pack run events.
- Developed MongoPackRunStateStore for persisting and retrieving pack run states.
- Implemented unit tests for MongoDB stores to ensure correct functionality.
- Added MongoTaskRunnerTestContext for setting up MongoDB test environment.
- Enhanced PackRunStateFactory to correctly initialize state with gate reasons.
This commit is contained in:
master
2025-11-07 10:01:35 +02:00
parent e5ffcd6535
commit a1ce3f74fa
122 changed files with 8730 additions and 914 deletions

View File

@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
namespace StellaOps.TaskRunner.Core.Configuration;
public static class TaskRunnerStorageModes
{
public const string Filesystem = "filesystem";
public const string Mongo = "mongo";
}
public sealed class TaskRunnerStorageOptions
{
public string Mode { get; set; } = TaskRunnerStorageModes.Filesystem;
public TaskRunnerMongoOptions Mongo { get; set; } = new();
}
public sealed class TaskRunnerMongoOptions
{
public string ConnectionString { get; set; } = "mongodb://127.0.0.1:27017/stellaops-taskrunner";
public string? Database { get; set; }
public string RunsCollection { get; set; } = "pack_runs";
public string LogsCollection { get; set; } = "pack_run_logs";
public string ArtifactsCollection { get; set; } = "pack_artifacts";
public string ApprovalsCollection { get; set; } = "pack_run_approvals";
}

View File

@@ -0,0 +1,33 @@
namespace StellaOps.TaskRunner.Core.Execution;
/// <summary>
/// Persists pack run log entries in a deterministic append-only fashion.
/// </summary>
public interface IPackRunLogStore
{
/// <summary>
/// Appends a single log entry to the run log.
/// </summary>
Task AppendAsync(string runId, PackRunLogEntry entry, CancellationToken cancellationToken);
/// <summary>
/// Returns the log entries for the specified run in chronological order.
/// </summary>
IAsyncEnumerable<PackRunLogEntry> ReadAsync(string runId, CancellationToken cancellationToken);
/// <summary>
/// Determines whether any log entries exist for the specified run.
/// </summary>
Task<bool> ExistsAsync(string runId, CancellationToken cancellationToken);
}
/// <summary>
/// Represents a single structured log entry emitted during a pack run.
/// </summary>
public sealed record PackRunLogEntry(
DateTimeOffset Timestamp,
string Level,
string EventType,
string Message,
string? StepId,
IReadOnlyDictionary<string, string>? Metadata);

View File

@@ -0,0 +1,116 @@
using StellaOps.TaskRunner.Core.Execution.Simulation;
namespace StellaOps.TaskRunner.Core.Execution;
/// <summary>
/// Builds deterministic <see cref="PackRunState"/> snapshots for freshly scheduled runs.
/// </summary>
public static class PackRunStateFactory
{
public static PackRunState CreateInitialState(
PackRunExecutionContext context,
PackRunExecutionGraph graph,
PackRunSimulationEngine simulationEngine,
DateTimeOffset timestamp)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(graph);
ArgumentNullException.ThrowIfNull(simulationEngine);
var simulation = simulationEngine.Simulate(context.Plan);
var simulationIndex = IndexSimulation(simulation.Steps);
var stepRecords = new Dictionary<string, PackRunStepStateRecord>(StringComparer.Ordinal);
foreach (var step in EnumerateSteps(graph.Steps))
{
var simulationStatus = simulationIndex.TryGetValue(step.Id, out var node)
? node.Status
: PackRunSimulationStatus.Pending;
var status = step.Enabled ? PackRunStepExecutionStatus.Pending : PackRunStepExecutionStatus.Skipped;
string? statusReason = null;
if (!step.Enabled)
{
statusReason = "disabled";
}
else if (simulationStatus == PackRunSimulationStatus.RequiresApproval)
{
statusReason = "requires-approval";
}
else if (simulationStatus == PackRunSimulationStatus.RequiresPolicy)
{
statusReason = "requires-policy";
}
else if (simulationStatus == PackRunSimulationStatus.Skipped)
{
status = PackRunStepExecutionStatus.Skipped;
statusReason = "condition-false";
}
var record = new PackRunStepStateRecord(
step.Id,
step.Kind,
step.Enabled,
step.ContinueOnError,
step.MaxParallel,
step.ApprovalId,
step.GateMessage,
status,
Attempts: 0,
LastTransitionAt: null,
NextAttemptAt: null,
StatusReason: statusReason);
stepRecords[step.Id] = record;
}
var failurePolicy = graph.FailurePolicy ?? PackRunExecutionGraph.DefaultFailurePolicy;
return PackRunState.Create(
context.RunId,
context.Plan.Hash,
context.Plan,
failurePolicy,
context.RequestedAt,
stepRecords,
timestamp);
}
private static Dictionary<string, PackRunSimulationNode> IndexSimulation(IReadOnlyList<PackRunSimulationNode> nodes)
{
var result = new Dictionary<string, PackRunSimulationNode>(StringComparer.Ordinal);
foreach (var node in nodes)
{
IndexSimulationNode(node, result);
}
return result;
}
private static void IndexSimulationNode(PackRunSimulationNode node, Dictionary<string, PackRunSimulationNode> accumulator)
{
accumulator[node.Id] = node;
foreach (var child in node.Children)
{
IndexSimulationNode(child, accumulator);
}
}
private static IEnumerable<PackRunExecutionStep> EnumerateSteps(IReadOnlyList<PackRunExecutionStep> steps)
{
foreach (var step in steps)
{
yield return step;
if (step.Children.Count == 0)
{
continue;
}
foreach (var child in EnumerateSteps(step.Children))
{
yield return child;
}
}
}
}