up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,118 +1,118 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class FilePackRunApprovalStore : IPackRunApprovalStore
|
||||
{
|
||||
private readonly string rootPath;
|
||||
private readonly JsonSerializerOptions serializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
public FilePackRunApprovalStore(string rootPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
|
||||
this.rootPath = rootPath;
|
||||
Directory.CreateDirectory(rootPath);
|
||||
}
|
||||
|
||||
public Task SaveAsync(string runId, IReadOnlyList<PackRunApprovalState> approvals, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetFilePath(runId);
|
||||
var json = SerializeApprovals(approvals);
|
||||
File.WriteAllText(path, json);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<PackRunApprovalState>> GetAsync(string runId, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetFilePath(runId);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return Task.FromResult((IReadOnlyList<PackRunApprovalState>)Array.Empty<PackRunApprovalState>());
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
var approvals = DeserializeApprovals(json);
|
||||
return Task.FromResult((IReadOnlyList<PackRunApprovalState>)approvals);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(string runId, PackRunApprovalState approval, CancellationToken cancellationToken)
|
||||
{
|
||||
var approvals = (await GetAsync(runId, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
var index = approvals.FindIndex(existing => string.Equals(existing.ApprovalId, approval.ApprovalId, StringComparison.Ordinal));
|
||||
if (index < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Approval '{approval.ApprovalId}' not found for run '{runId}'.");
|
||||
}
|
||||
|
||||
approvals[index] = approval;
|
||||
await SaveAsync(runId, approvals, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string GetFilePath(string runId)
|
||||
{
|
||||
var safeFile = $"{runId}.json";
|
||||
return Path.Combine(rootPath, safeFile);
|
||||
}
|
||||
|
||||
private string SerializeApprovals(IReadOnlyList<PackRunApprovalState> approvals)
|
||||
{
|
||||
var array = new JsonArray();
|
||||
foreach (var approval in approvals)
|
||||
{
|
||||
var node = new JsonObject
|
||||
{
|
||||
["approvalId"] = approval.ApprovalId,
|
||||
["status"] = approval.Status.ToString(),
|
||||
["requestedAt"] = approval.RequestedAt,
|
||||
["actorId"] = approval.ActorId,
|
||||
["completedAt"] = approval.CompletedAt,
|
||||
["summary"] = approval.Summary,
|
||||
["requiredGrants"] = new JsonArray(approval.RequiredGrants.Select(grant => (JsonNode)grant).ToArray()),
|
||||
["stepIds"] = new JsonArray(approval.StepIds.Select(step => (JsonNode)step).ToArray()),
|
||||
["messages"] = new JsonArray(approval.Messages.Select(message => (JsonNode)message).ToArray()),
|
||||
["reasonTemplate"] = approval.ReasonTemplate
|
||||
};
|
||||
|
||||
array.Add(node);
|
||||
}
|
||||
|
||||
return array.ToJsonString(serializerOptions);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PackRunApprovalState> DeserializeApprovals(string json)
|
||||
{
|
||||
var array = JsonNode.Parse(json)?.AsArray() ?? new JsonArray();
|
||||
var list = new List<PackRunApprovalState>(array.Count);
|
||||
foreach (var entry in array)
|
||||
{
|
||||
if (entry is not JsonObject obj)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var requiredGrants = obj["requiredGrants"]?.AsArray()?.Select(node => node!.GetValue<string>()).ToList() ?? new List<string>();
|
||||
var stepIds = obj["stepIds"]?.AsArray()?.Select(node => node!.GetValue<string>()).ToList() ?? new List<string>();
|
||||
var messages = obj["messages"]?.AsArray()?.Select(node => node!.GetValue<string>()).ToList() ?? new List<string>();
|
||||
Enum.TryParse(obj["status"]?.GetValue<string>(), ignoreCase: true, out PackRunApprovalStatus status);
|
||||
|
||||
list.Add(new PackRunApprovalState(
|
||||
obj["approvalId"]?.GetValue<string>() ?? string.Empty,
|
||||
requiredGrants,
|
||||
stepIds,
|
||||
messages,
|
||||
obj["reasonTemplate"]?.GetValue<string>(),
|
||||
obj["requestedAt"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.UtcNow,
|
||||
status,
|
||||
obj["actorId"]?.GetValue<string>(),
|
||||
obj["completedAt"]?.GetValue<DateTimeOffset?>(),
|
||||
obj["summary"]?.GetValue<string>()));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class FilePackRunApprovalStore : IPackRunApprovalStore
|
||||
{
|
||||
private readonly string rootPath;
|
||||
private readonly JsonSerializerOptions serializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
public FilePackRunApprovalStore(string rootPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
|
||||
this.rootPath = rootPath;
|
||||
Directory.CreateDirectory(rootPath);
|
||||
}
|
||||
|
||||
public Task SaveAsync(string runId, IReadOnlyList<PackRunApprovalState> approvals, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetFilePath(runId);
|
||||
var json = SerializeApprovals(approvals);
|
||||
File.WriteAllText(path, json);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<PackRunApprovalState>> GetAsync(string runId, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetFilePath(runId);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return Task.FromResult((IReadOnlyList<PackRunApprovalState>)Array.Empty<PackRunApprovalState>());
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
var approvals = DeserializeApprovals(json);
|
||||
return Task.FromResult((IReadOnlyList<PackRunApprovalState>)approvals);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(string runId, PackRunApprovalState approval, CancellationToken cancellationToken)
|
||||
{
|
||||
var approvals = (await GetAsync(runId, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
var index = approvals.FindIndex(existing => string.Equals(existing.ApprovalId, approval.ApprovalId, StringComparison.Ordinal));
|
||||
if (index < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Approval '{approval.ApprovalId}' not found for run '{runId}'.");
|
||||
}
|
||||
|
||||
approvals[index] = approval;
|
||||
await SaveAsync(runId, approvals, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string GetFilePath(string runId)
|
||||
{
|
||||
var safeFile = $"{runId}.json";
|
||||
return Path.Combine(rootPath, safeFile);
|
||||
}
|
||||
|
||||
private string SerializeApprovals(IReadOnlyList<PackRunApprovalState> approvals)
|
||||
{
|
||||
var array = new JsonArray();
|
||||
foreach (var approval in approvals)
|
||||
{
|
||||
var node = new JsonObject
|
||||
{
|
||||
["approvalId"] = approval.ApprovalId,
|
||||
["status"] = approval.Status.ToString(),
|
||||
["requestedAt"] = approval.RequestedAt,
|
||||
["actorId"] = approval.ActorId,
|
||||
["completedAt"] = approval.CompletedAt,
|
||||
["summary"] = approval.Summary,
|
||||
["requiredGrants"] = new JsonArray(approval.RequiredGrants.Select(grant => (JsonNode)grant).ToArray()),
|
||||
["stepIds"] = new JsonArray(approval.StepIds.Select(step => (JsonNode)step).ToArray()),
|
||||
["messages"] = new JsonArray(approval.Messages.Select(message => (JsonNode)message).ToArray()),
|
||||
["reasonTemplate"] = approval.ReasonTemplate
|
||||
};
|
||||
|
||||
array.Add(node);
|
||||
}
|
||||
|
||||
return array.ToJsonString(serializerOptions);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PackRunApprovalState> DeserializeApprovals(string json)
|
||||
{
|
||||
var array = JsonNode.Parse(json)?.AsArray() ?? new JsonArray();
|
||||
var list = new List<PackRunApprovalState>(array.Count);
|
||||
foreach (var entry in array)
|
||||
{
|
||||
if (entry is not JsonObject obj)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var requiredGrants = obj["requiredGrants"]?.AsArray()?.Select(node => node!.GetValue<string>()).ToList() ?? new List<string>();
|
||||
var stepIds = obj["stepIds"]?.AsArray()?.Select(node => node!.GetValue<string>()).ToList() ?? new List<string>();
|
||||
var messages = obj["messages"]?.AsArray()?.Select(node => node!.GetValue<string>()).ToList() ?? new List<string>();
|
||||
Enum.TryParse(obj["status"]?.GetValue<string>(), ignoreCase: true, out PackRunApprovalStatus status);
|
||||
|
||||
list.Add(new PackRunApprovalState(
|
||||
obj["approvalId"]?.GetValue<string>() ?? string.Empty,
|
||||
requiredGrants,
|
||||
stepIds,
|
||||
messages,
|
||||
obj["reasonTemplate"]?.GetValue<string>(),
|
||||
obj["requestedAt"]?.GetValue<DateTimeOffset>() ?? DateTimeOffset.UtcNow,
|
||||
status,
|
||||
obj["actorId"]?.GetValue<string>(),
|
||||
obj["completedAt"]?.GetValue<DateTimeOffset?>(),
|
||||
obj["summary"]?.GetValue<string>()));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
using StellaOps.TaskRunner.Core.Planning;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
/// <summary>
|
||||
/// File-system backed implementation of <see cref="IPackRunStateStore"/> intended for development and air-gapped smoke tests.
|
||||
/// </summary>
|
||||
public sealed class FilePackRunStateStore : IPackRunStateStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private readonly string rootPath;
|
||||
private readonly SemaphoreSlim mutex = new(1, 1);
|
||||
|
||||
public FilePackRunStateStore(string rootPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
|
||||
|
||||
this.rootPath = Path.GetFullPath(rootPath);
|
||||
Directory.CreateDirectory(this.rootPath);
|
||||
}
|
||||
|
||||
public async Task<PackRunState?> GetAsync(string runId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
||||
|
||||
var path = GetPath(runId);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
await using var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var document = await JsonSerializer.DeserializeAsync<StateDocument>(stream, SerializerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return document?.ToDomain();
|
||||
}
|
||||
|
||||
public async Task SaveAsync(PackRunState state, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(state);
|
||||
|
||||
var path = GetPath(state.RunId);
|
||||
var document = StateDocument.FromDomain(state);
|
||||
|
||||
Directory.CreateDirectory(rootPath);
|
||||
|
||||
await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await using var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
await JsonSerializer.SerializeAsync(stream, document, SerializerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mutex.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PackRunState>> ListAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Directory.Exists(rootPath))
|
||||
{
|
||||
return Array.Empty<PackRunState>();
|
||||
}
|
||||
|
||||
var states = new List<PackRunState>();
|
||||
|
||||
var files = Directory.EnumerateFiles(rootPath, "*.json", SearchOption.TopDirectoryOnly)
|
||||
.OrderBy(file => file, StringComparer.Ordinal);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await using var stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var document = await JsonSerializer.DeserializeAsync<StateDocument>(stream, SerializerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (document is not null)
|
||||
{
|
||||
states.Add(document.ToDomain());
|
||||
}
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
private string GetPath(string runId)
|
||||
{
|
||||
var safeName = SanitizeFileName(runId);
|
||||
return Path.Combine(rootPath, $"{safeName}.json");
|
||||
}
|
||||
|
||||
private static string SanitizeFileName(string value)
|
||||
{
|
||||
var result = value.Trim();
|
||||
foreach (var invalid in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
result = result.Replace(invalid, '_');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
using System.Text.Json;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
using StellaOps.TaskRunner.Core.Planning;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
/// <summary>
|
||||
/// File-system backed implementation of <see cref="IPackRunStateStore"/> intended for development and air-gapped smoke tests.
|
||||
/// </summary>
|
||||
public sealed class FilePackRunStateStore : IPackRunStateStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private readonly string rootPath;
|
||||
private readonly SemaphoreSlim mutex = new(1, 1);
|
||||
|
||||
public FilePackRunStateStore(string rootPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
|
||||
|
||||
this.rootPath = Path.GetFullPath(rootPath);
|
||||
Directory.CreateDirectory(this.rootPath);
|
||||
}
|
||||
|
||||
public async Task<PackRunState?> GetAsync(string runId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
||||
|
||||
var path = GetPath(runId);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
await using var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var document = await JsonSerializer.DeserializeAsync<StateDocument>(stream, SerializerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return document?.ToDomain();
|
||||
}
|
||||
|
||||
public async Task SaveAsync(PackRunState state, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(state);
|
||||
|
||||
var path = GetPath(state.RunId);
|
||||
var document = StateDocument.FromDomain(state);
|
||||
|
||||
Directory.CreateDirectory(rootPath);
|
||||
|
||||
await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await using var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
await JsonSerializer.SerializeAsync(stream, document, SerializerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mutex.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PackRunState>> ListAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Directory.Exists(rootPath))
|
||||
{
|
||||
return Array.Empty<PackRunState>();
|
||||
}
|
||||
|
||||
var states = new List<PackRunState>();
|
||||
|
||||
var files = Directory.EnumerateFiles(rootPath, "*.json", SearchOption.TopDirectoryOnly)
|
||||
.OrderBy(file => file, StringComparer.Ordinal);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await using var stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var document = await JsonSerializer.DeserializeAsync<StateDocument>(stream, SerializerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (document is not null)
|
||||
{
|
||||
states.Add(document.ToDomain());
|
||||
}
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
private string GetPath(string runId)
|
||||
{
|
||||
var safeName = SanitizeFileName(runId);
|
||||
return Path.Combine(rootPath, $"{safeName}.json");
|
||||
}
|
||||
|
||||
private static string SanitizeFileName(string value)
|
||||
{
|
||||
var result = value.Trim();
|
||||
foreach (var invalid in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
result = result.Replace(invalid, '_');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private sealed record StateDocument(
|
||||
string RunId,
|
||||
string PlanHash,
|
||||
@@ -125,21 +125,21 @@ public sealed class FilePackRunStateStore : IPackRunStateStore
|
||||
{
|
||||
var steps = state.Steps.Values
|
||||
.OrderBy(step => step.StepId, StringComparer.Ordinal)
|
||||
.Select(step => new StepDocument(
|
||||
step.StepId,
|
||||
step.Kind,
|
||||
step.Enabled,
|
||||
step.ContinueOnError,
|
||||
step.MaxParallel,
|
||||
step.ApprovalId,
|
||||
step.GateMessage,
|
||||
step.Status,
|
||||
step.Attempts,
|
||||
step.LastTransitionAt,
|
||||
step.NextAttemptAt,
|
||||
step.StatusReason))
|
||||
.ToList();
|
||||
|
||||
.Select(step => new StepDocument(
|
||||
step.StepId,
|
||||
step.Kind,
|
||||
step.Enabled,
|
||||
step.ContinueOnError,
|
||||
step.MaxParallel,
|
||||
step.ApprovalId,
|
||||
step.GateMessage,
|
||||
step.Status,
|
||||
step.Attempts,
|
||||
step.LastTransitionAt,
|
||||
step.NextAttemptAt,
|
||||
step.StatusReason))
|
||||
.ToList();
|
||||
|
||||
return new StateDocument(
|
||||
state.RunId,
|
||||
state.PlanHash,
|
||||
@@ -154,23 +154,23 @@ public sealed class FilePackRunStateStore : IPackRunStateStore
|
||||
|
||||
public PackRunState ToDomain()
|
||||
{
|
||||
var steps = Steps.ToDictionary(
|
||||
step => step.StepId,
|
||||
step => new PackRunStepStateRecord(
|
||||
step.StepId,
|
||||
step.Kind,
|
||||
step.Enabled,
|
||||
step.ContinueOnError,
|
||||
step.MaxParallel,
|
||||
step.ApprovalId,
|
||||
step.GateMessage,
|
||||
step.Status,
|
||||
step.Attempts,
|
||||
step.LastTransitionAt,
|
||||
step.NextAttemptAt,
|
||||
step.StatusReason),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
var steps = Steps.ToDictionary(
|
||||
step => step.StepId,
|
||||
step => new PackRunStepStateRecord(
|
||||
step.StepId,
|
||||
step.Kind,
|
||||
step.Enabled,
|
||||
step.ContinueOnError,
|
||||
step.MaxParallel,
|
||||
step.ApprovalId,
|
||||
step.GateMessage,
|
||||
step.Status,
|
||||
step.Attempts,
|
||||
step.LastTransitionAt,
|
||||
step.NextAttemptAt,
|
||||
step.StatusReason),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
return new PackRunState(
|
||||
RunId,
|
||||
PlanHash,
|
||||
@@ -183,18 +183,18 @@ public sealed class FilePackRunStateStore : IPackRunStateStore
|
||||
TenantId);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record StepDocument(
|
||||
string StepId,
|
||||
PackRunStepKind Kind,
|
||||
bool Enabled,
|
||||
bool ContinueOnError,
|
||||
int? MaxParallel,
|
||||
string? ApprovalId,
|
||||
string? GateMessage,
|
||||
PackRunStepExecutionStatus Status,
|
||||
int Attempts,
|
||||
DateTimeOffset? LastTransitionAt,
|
||||
DateTimeOffset? NextAttemptAt,
|
||||
string? StatusReason);
|
||||
}
|
||||
|
||||
private sealed record StepDocument(
|
||||
string StepId,
|
||||
PackRunStepKind Kind,
|
||||
bool Enabled,
|
||||
bool ContinueOnError,
|
||||
int? MaxParallel,
|
||||
string? ApprovalId,
|
||||
string? GateMessage,
|
||||
PackRunStepExecutionStatus Status,
|
||||
int Attempts,
|
||||
DateTimeOffset? LastTransitionAt,
|
||||
DateTimeOffset? NextAttemptAt,
|
||||
string? StatusReason);
|
||||
}
|
||||
|
||||
@@ -25,26 +25,26 @@ public sealed class FilesystemPackRunDispatcher : IPackRunJobDispatcher, IPackRu
|
||||
}
|
||||
|
||||
public string QueuePath => queuePath;
|
||||
|
||||
public async Task<PackRunExecutionContext?> TryDequeueAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var files = Directory.GetFiles(queuePath, "*.json", SearchOption.TopDirectoryOnly)
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
|
||||
public async Task<PackRunExecutionContext?> TryDequeueAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var files = Directory.GetFiles(queuePath, "*.json", SearchOption.TopDirectoryOnly)
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
{
|
||||
var jobJson = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
|
||||
var job = JsonSerializer.Deserialize<JobEnvelope>(jobJson, serializerOptions);
|
||||
if (job is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
TaskPackPlan? plan = job.Plan;
|
||||
if (plan is null)
|
||||
{
|
||||
@@ -76,12 +76,12 @@ public sealed class FilesystemPackRunDispatcher : IPackRunJobDispatcher, IPackRu
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var failedPath = file + ".failed";
|
||||
File.Move(file, failedPath, overwrite: true);
|
||||
Console.Error.WriteLine($"Failed to dequeue job '{file}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var failedPath = file + ".failed";
|
||||
File.Move(file, failedPath, overwrite: true);
|
||||
Console.Error.WriteLine($"Failed to dequeue job '{file}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -108,23 +108,23 @@ public sealed class FilesystemPackRunDispatcher : IPackRunJobDispatcher, IPackRu
|
||||
=> Path.IsPathRooted(relative) ? relative : Path.Combine(root, relative);
|
||||
|
||||
private static async Task<IDictionary<string, JsonNode?>> LoadInputsAsync(string? path, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
{
|
||||
return new Dictionary<string, JsonNode?>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
var node = JsonNode.Parse(json) as JsonObject;
|
||||
if (node is null)
|
||||
{
|
||||
return new Dictionary<string, JsonNode?>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
return node.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => pair.Value,
|
||||
StringComparer.Ordinal);
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
{
|
||||
return new Dictionary<string, JsonNode?>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
var node = JsonNode.Parse(json) as JsonObject;
|
||||
if (node is null)
|
||||
{
|
||||
return new Dictionary<string, JsonNode?>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
return node.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => pair.Value,
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private sealed record JobEnvelope(
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class HttpPackRunNotificationPublisher : IPackRunNotificationPublisher
|
||||
{
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly NotificationOptions options;
|
||||
private readonly ILogger<HttpPackRunNotificationPublisher> logger;
|
||||
|
||||
public HttpPackRunNotificationPublisher(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IOptions<NotificationOptions> options,
|
||||
ILogger<HttpPackRunNotificationPublisher> logger)
|
||||
{
|
||||
this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||
this.options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task PublishApprovalRequestedAsync(string runId, ApprovalNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (options.ApprovalEndpoint is null)
|
||||
{
|
||||
logger.LogWarning("Approval endpoint not configured; skipping approval notification for run {RunId}.", runId);
|
||||
return;
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("taskrunner-notifications");
|
||||
var payload = new
|
||||
{
|
||||
runId,
|
||||
notification.ApprovalId,
|
||||
notification.RequiredGrants,
|
||||
notification.Messages,
|
||||
notification.StepIds,
|
||||
notification.ReasonTemplate
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync(options.ApprovalEndpoint, payload, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task PublishPolicyGatePendingAsync(string runId, PolicyGateNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (options.PolicyEndpoint is null)
|
||||
{
|
||||
logger.LogDebug("Policy endpoint not configured; skipping policy notification for run {RunId} step {StepId}.", runId, notification.StepId);
|
||||
return;
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("taskrunner-notifications");
|
||||
var payload = new
|
||||
{
|
||||
runId,
|
||||
notification.StepId,
|
||||
notification.Message,
|
||||
Parameters = notification.Parameters.Select(parameter => new
|
||||
{
|
||||
parameter.Name,
|
||||
parameter.RequiresRuntimeValue,
|
||||
parameter.Expression,
|
||||
parameter.Error
|
||||
})
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync(options.PolicyEndpoint, payload, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class HttpPackRunNotificationPublisher : IPackRunNotificationPublisher
|
||||
{
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly NotificationOptions options;
|
||||
private readonly ILogger<HttpPackRunNotificationPublisher> logger;
|
||||
|
||||
public HttpPackRunNotificationPublisher(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IOptions<NotificationOptions> options,
|
||||
ILogger<HttpPackRunNotificationPublisher> logger)
|
||||
{
|
||||
this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||
this.options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task PublishApprovalRequestedAsync(string runId, ApprovalNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (options.ApprovalEndpoint is null)
|
||||
{
|
||||
logger.LogWarning("Approval endpoint not configured; skipping approval notification for run {RunId}.", runId);
|
||||
return;
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("taskrunner-notifications");
|
||||
var payload = new
|
||||
{
|
||||
runId,
|
||||
notification.ApprovalId,
|
||||
notification.RequiredGrants,
|
||||
notification.Messages,
|
||||
notification.StepIds,
|
||||
notification.ReasonTemplate
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync(options.ApprovalEndpoint, payload, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task PublishPolicyGatePendingAsync(string runId, PolicyGateNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (options.PolicyEndpoint is null)
|
||||
{
|
||||
logger.LogDebug("Policy endpoint not configured; skipping policy notification for run {RunId} step {StepId}.", runId, notification.StepId);
|
||||
return;
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("taskrunner-notifications");
|
||||
var payload = new
|
||||
{
|
||||
runId,
|
||||
notification.StepId,
|
||||
notification.Message,
|
||||
Parameters = notification.Parameters.Select(parameter => new
|
||||
{
|
||||
parameter.Name,
|
||||
parameter.RequiresRuntimeValue,
|
||||
parameter.Expression,
|
||||
parameter.Error
|
||||
})
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync(options.PolicyEndpoint, payload, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class LoggingPackRunNotificationPublisher : IPackRunNotificationPublisher
|
||||
{
|
||||
private readonly ILogger<LoggingPackRunNotificationPublisher> logger;
|
||||
|
||||
public LoggingPackRunNotificationPublisher(ILogger<LoggingPackRunNotificationPublisher> logger)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public Task PublishApprovalRequestedAsync(string runId, ApprovalNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Run {RunId}: approval {ApprovalId} requires grants {Grants}.",
|
||||
runId,
|
||||
notification.ApprovalId,
|
||||
string.Join(",", notification.RequiredGrants));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task PublishPolicyGatePendingAsync(string runId, PolicyGateNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Run {RunId}: policy gate {StepId} pending (parameters: {Parameters}).",
|
||||
runId,
|
||||
notification.StepId,
|
||||
string.Join(",", notification.Parameters.Select(p => p.Name)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class LoggingPackRunNotificationPublisher : IPackRunNotificationPublisher
|
||||
{
|
||||
private readonly ILogger<LoggingPackRunNotificationPublisher> logger;
|
||||
|
||||
public LoggingPackRunNotificationPublisher(ILogger<LoggingPackRunNotificationPublisher> logger)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public Task PublishApprovalRequestedAsync(string runId, ApprovalNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Run {RunId}: approval {ApprovalId} requires grants {Grants}.",
|
||||
runId,
|
||||
notification.ApprovalId,
|
||||
string.Join(",", notification.RequiredGrants));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task PublishPolicyGatePendingAsync(string runId, PolicyGateNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Run {RunId}: policy gate {StepId} pending (parameters: {Parameters}).",
|
||||
runId,
|
||||
notification.StepId,
|
||||
string.Join(",", notification.Parameters.Select(p => p.Name)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class NoopPackRunJobDispatcher : IPackRunJobDispatcher
|
||||
{
|
||||
public Task<PackRunExecutionContext?> TryDequeueAsync(CancellationToken cancellationToken)
|
||||
=> Task.FromResult<PackRunExecutionContext?>(null);
|
||||
}
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class NoopPackRunJobDispatcher : IPackRunJobDispatcher
|
||||
{
|
||||
public Task<PackRunExecutionContext?> TryDequeueAsync(CancellationToken cancellationToken)
|
||||
=> Task.FromResult<PackRunExecutionContext?>(null);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
using StellaOps.TaskRunner.Core.Planning;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class NoopPackRunStepExecutor : IPackRunStepExecutor
|
||||
{
|
||||
public Task<PackRunStepExecutionResult> ExecuteAsync(
|
||||
PackRunExecutionStep step,
|
||||
IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (parameters.TryGetValue("simulateFailure", out var value) &&
|
||||
value.Value is JsonValue jsonValue &&
|
||||
jsonValue.TryGetValue<bool>(out var failure) &&
|
||||
failure)
|
||||
{
|
||||
return Task.FromResult(new PackRunStepExecutionResult(false, "Simulated failure requested."));
|
||||
}
|
||||
|
||||
return Task.FromResult(new PackRunStepExecutionResult(true));
|
||||
}
|
||||
}
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
using StellaOps.TaskRunner.Core.Planning;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class NoopPackRunStepExecutor : IPackRunStepExecutor
|
||||
{
|
||||
public Task<PackRunStepExecutionResult> ExecuteAsync(
|
||||
PackRunExecutionStep step,
|
||||
IReadOnlyDictionary<string, TaskPackPlanParameterValue> parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (parameters.TryGetValue("simulateFailure", out var value) &&
|
||||
value.Value is JsonValue jsonValue &&
|
||||
jsonValue.TryGetValue<bool>(out var failure) &&
|
||||
failure)
|
||||
{
|
||||
return Task.FromResult(new PackRunStepExecutionResult(false, "Simulated failure requested."));
|
||||
}
|
||||
|
||||
return Task.FromResult(new PackRunStepExecutionResult(true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class NotificationOptions
|
||||
{
|
||||
public Uri? ApprovalEndpoint { get; set; }
|
||||
|
||||
public Uri? PolicyEndpoint { get; set; }
|
||||
}
|
||||
namespace StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
|
||||
public sealed class NotificationOptions
|
||||
{
|
||||
public Uri? ApprovalEndpoint { get; set; }
|
||||
|
||||
public Uri? PolicyEndpoint { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user