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

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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(

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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));
}
}

View File

@@ -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; }
}