feat: Implement approvals workflow and notifications integration
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added approvals orchestration with persistence and workflow scaffolding. - Integrated notifications insights and staged resume hooks. - Introduced approval coordinator and policy notification bridge with unit tests. - Added approval decision API with resume requeue and persisted plan snapshots. - Documented the Excitor consensus API beta and provided JSON sample payload. - Created analyzers to flag usage of deprecated merge service APIs. - Implemented logging for artifact uploads and approval decision service. - Added tests for PackRunApprovalDecisionService and related components.
This commit is contained in:
@@ -12,15 +12,27 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.Configure<TaskRunnerServiceOptions>(builder.Configuration.GetSection("TaskRunner"));
|
||||
builder.Services.AddSingleton<TaskPackManifestLoader>();
|
||||
builder.Services.AddSingleton<TaskPackPlanner>();
|
||||
builder.Services.AddSingleton<PackRunSimulationEngine>();
|
||||
builder.Services.AddSingleton<PackRunExecutionGraphBuilder>();
|
||||
builder.Services.AddSingleton<IPackRunStateStore>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<TaskRunnerServiceOptions>>().Value;
|
||||
return new FilePackRunStateStore(options.RunStatePath);
|
||||
});
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddSingleton<TaskPackPlanner>();
|
||||
builder.Services.AddSingleton<PackRunSimulationEngine>();
|
||||
builder.Services.AddSingleton<PackRunExecutionGraphBuilder>();
|
||||
builder.Services.AddSingleton<IPackRunApprovalStore>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<TaskRunnerServiceOptions>>().Value;
|
||||
return new FilePackRunApprovalStore(options.ApprovalStorePath);
|
||||
});
|
||||
builder.Services.AddSingleton<IPackRunStateStore>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<TaskRunnerServiceOptions>>().Value;
|
||||
return new FilePackRunStateStore(options.RunStatePath);
|
||||
});
|
||||
builder.Services.AddSingleton(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<TaskRunnerServiceOptions>>().Value;
|
||||
return new FilesystemPackRunDispatcher(options.QueuePath, options.ArchivePath);
|
||||
});
|
||||
builder.Services.AddSingleton<IPackRunJobScheduler>(sp => sp.GetRequiredService<FilesystemPackRunDispatcher>());
|
||||
builder.Services.AddSingleton<PackRunApprovalDecisionService>();
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -67,11 +79,11 @@ app.MapPost("/v1/task-runner/simulations", async (
|
||||
return Results.Ok(response);
|
||||
}).WithName("SimulateTaskPack");
|
||||
|
||||
app.MapGet("/v1/task-runner/runs/{runId}", async (
|
||||
string runId,
|
||||
IPackRunStateStore stateStore,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
app.MapGet("/v1/task-runner/runs/{runId}", async (
|
||||
string runId,
|
||||
IPackRunStateStore stateStore,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(runId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "runId is required." });
|
||||
@@ -83,10 +95,43 @@ app.MapGet("/v1/task-runner/runs/{runId}", async (
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(RunStateMapper.ToResponse(state));
|
||||
}).WithName("GetRunState");
|
||||
|
||||
app.MapGet("/", () => Results.Redirect("/openapi"));
|
||||
return Results.Ok(RunStateMapper.ToResponse(state));
|
||||
}).WithName("GetRunState");
|
||||
|
||||
app.MapPost("/v1/task-runner/runs/{runId}/approvals/{approvalId}", async (
|
||||
string runId,
|
||||
string approvalId,
|
||||
[FromBody] ApprovalDecisionDto request,
|
||||
PackRunApprovalDecisionService decisionService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
return Results.BadRequest(new { error = "Request body is required." });
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<PackRunApprovalDecisionType>(request.Decision, ignoreCase: true, out var decisionType))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Invalid decision. Expected approved, rejected, or expired." });
|
||||
}
|
||||
|
||||
var result = await decisionService.ApplyAsync(
|
||||
new PackRunApprovalDecisionRequest(runId, approvalId, decisionType, request.ActorId, request.Summary),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (ReferenceEquals(result, PackRunApprovalDecisionResult.NotFound))
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
status = result.Status,
|
||||
resumed = result.ShouldResume
|
||||
});
|
||||
}).WithName("ApplyApprovalDecision");
|
||||
|
||||
app.MapGet("/", () => Results.Redirect("/openapi"));
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -146,19 +191,21 @@ internal sealed record RunStateResponse(
|
||||
DateTimeOffset UpdatedAt,
|
||||
IReadOnlyList<RunStateStepResponse> Steps);
|
||||
|
||||
internal sealed record RunStateStepResponse(
|
||||
string StepId,
|
||||
string Kind,
|
||||
bool Enabled,
|
||||
bool ContinueOnError,
|
||||
int? MaxParallel,
|
||||
string? ApprovalId,
|
||||
string? GateMessage,
|
||||
string Status,
|
||||
int Attempts,
|
||||
DateTimeOffset? LastTransitionAt,
|
||||
DateTimeOffset? NextAttemptAt,
|
||||
string? StatusReason);
|
||||
internal sealed record RunStateStepResponse(
|
||||
string StepId,
|
||||
string Kind,
|
||||
bool Enabled,
|
||||
bool ContinueOnError,
|
||||
int? MaxParallel,
|
||||
string? ApprovalId,
|
||||
string? GateMessage,
|
||||
string Status,
|
||||
int Attempts,
|
||||
DateTimeOffset? LastTransitionAt,
|
||||
DateTimeOffset? NextAttemptAt,
|
||||
string? StatusReason);
|
||||
|
||||
internal sealed record ApprovalDecisionDto(string Decision, string? ActorId, string? Summary);
|
||||
|
||||
internal static class SimulationMapper
|
||||
{
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
namespace StellaOps.TaskRunner.WebService;
|
||||
|
||||
public sealed class TaskRunnerServiceOptions
|
||||
{
|
||||
public string RunStatePath { get; set; } = Path.Combine(AppContext.BaseDirectory, "state", "runs");
|
||||
}
|
||||
public sealed class TaskRunnerServiceOptions
|
||||
{
|
||||
public string RunStatePath { get; set; } = Path.Combine(AppContext.BaseDirectory, "state", "runs");
|
||||
public string ApprovalStorePath { get; set; } = Path.Combine(AppContext.BaseDirectory, "approvals");
|
||||
public string QueuePath { get; set; } = Path.Combine(AppContext.BaseDirectory, "queue");
|
||||
public string ArchivePath { get; set; } = Path.Combine(AppContext.BaseDirectory, "queue", "archive");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user