541 lines
21 KiB
C#
541 lines
21 KiB
C#
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using StellaOps.Platform.WebService.Constants;
|
|
using StellaOps.Platform.WebService.Contracts;
|
|
using StellaOps.Platform.WebService.Services;
|
|
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace StellaOps.Platform.WebService.Endpoints;
|
|
|
|
public static class ReleaseReadModelEndpoints
|
|
{
|
|
public static IEndpointRouteBuilder MapReleaseReadModelEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var releases = app.MapGroup("/api/v2/releases")
|
|
.WithTags("Releases V2")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
releases.MapGet(string.Empty, async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
[AsParameters] ReleaseListQuery query,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var page = await service.ListReleasesAsync(
|
|
requestContext!,
|
|
query.Region,
|
|
query.Environment,
|
|
query.Type,
|
|
query.Status,
|
|
query.Limit,
|
|
query.Offset,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return Results.Ok(new PlatformListResponse<ReleaseProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
page.Items,
|
|
page.Total,
|
|
page.Limit,
|
|
page.Offset));
|
|
})
|
|
.WithName("ListReleasesV2")
|
|
.WithSummary("List Pack-22 release projections")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
releases.MapGet("/{releaseId:guid}", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid releaseId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var detail = await service.GetReleaseDetailAsync(
|
|
requestContext!,
|
|
releaseId,
|
|
cancellationToken).ConfigureAwait(false);
|
|
if (detail is null)
|
|
{
|
|
return Results.NotFound(new { error = "release_not_found", releaseId });
|
|
}
|
|
|
|
return Results.Ok(new PlatformItemResponse<ReleaseDetailProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
detail));
|
|
})
|
|
.WithName("GetReleaseDetailV2")
|
|
.WithSummary("Get Pack-22 release detail projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
releases.MapGet("/activity", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
[AsParameters] ReleaseActivityQuery query,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var page = await service.ListActivityAsync(
|
|
requestContext!,
|
|
query.Region,
|
|
query.Environment,
|
|
query.Limit,
|
|
query.Offset,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return Results.Ok(new PlatformListResponse<ReleaseActivityProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
page.Items,
|
|
page.Total,
|
|
page.Limit,
|
|
page.Offset));
|
|
})
|
|
.WithName("ListReleaseActivityV2")
|
|
.WithSummary("List cross-release activity timeline")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
releases.MapGet("/approvals", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
[AsParameters] ReleaseApprovalsQuery query,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var page = await service.ListApprovalsAsync(
|
|
requestContext!,
|
|
query.Status,
|
|
query.Region,
|
|
query.Environment,
|
|
query.Limit,
|
|
query.Offset,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return Results.Ok(new PlatformListResponse<ReleaseApprovalProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
page.Items,
|
|
page.Total,
|
|
page.Limit,
|
|
page.Offset));
|
|
})
|
|
.WithName("ListReleaseApprovalsV2")
|
|
.WithSummary("List cross-release approvals queue projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
var runs = releases.MapGroup("/runs");
|
|
|
|
runs.MapGet(string.Empty, async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
[AsParameters] ReleaseRunListQuery query,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var page = await service.ListRunsAsync(
|
|
requestContext!,
|
|
query.Status,
|
|
query.Lane,
|
|
query.Environment,
|
|
query.Region,
|
|
query.Outcome,
|
|
query.NeedsApproval,
|
|
query.BlockedByDataIntegrity,
|
|
query.Limit,
|
|
query.Offset,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return Results.Ok(new PlatformListResponse<ReleaseRunProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
page.Items,
|
|
page.Total,
|
|
page.Limit,
|
|
page.Offset));
|
|
})
|
|
.WithName("ListReleaseRunsV2")
|
|
.WithSummary("List run-centric release projections for Pack-22 contracts")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
runs.MapGet("/{runId:guid}", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid runId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var item = await service.GetRunDetailAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
|
return item is null
|
|
? Results.NotFound(new { error = "run_not_found", runId })
|
|
: Results.Ok(new PlatformItemResponse<ReleaseRunDetailProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
item));
|
|
})
|
|
.WithName("GetReleaseRunDetailV2")
|
|
.WithSummary("Get canonical release run detail projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
runs.MapGet("/{runId:guid}/timeline", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid runId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var item = await service.GetRunTimelineAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
|
return item is null
|
|
? Results.NotFound(new { error = "run_not_found", runId })
|
|
: Results.Ok(new PlatformItemResponse<ReleaseRunTimelineProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
item));
|
|
})
|
|
.WithName("GetReleaseRunTimelineV2")
|
|
.WithSummary("Get release run timeline projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
runs.MapGet("/{runId:guid}/gate-decision", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid runId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var item = await service.GetRunGateDecisionAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
|
return item is null
|
|
? Results.NotFound(new { error = "run_not_found", runId })
|
|
: Results.Ok(new PlatformItemResponse<ReleaseRunGateDecisionProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
item));
|
|
})
|
|
.WithName("GetReleaseRunGateDecisionV2")
|
|
.WithSummary("Get release run gate decision projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
runs.MapGet("/{runId:guid}/approvals", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid runId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var item = await service.GetRunApprovalsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
|
return item is null
|
|
? Results.NotFound(new { error = "run_not_found", runId })
|
|
: Results.Ok(new PlatformItemResponse<ReleaseRunApprovalsProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
item));
|
|
})
|
|
.WithName("GetReleaseRunApprovalsV2")
|
|
.WithSummary("Get release run approvals checkpoints projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
runs.MapGet("/{runId:guid}/deployments", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid runId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var item = await service.GetRunDeploymentsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
|
return item is null
|
|
? Results.NotFound(new { error = "run_not_found", runId })
|
|
: Results.Ok(new PlatformItemResponse<ReleaseRunDeploymentsProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
item));
|
|
})
|
|
.WithName("GetReleaseRunDeploymentsV2")
|
|
.WithSummary("Get release run deployments projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
runs.MapGet("/{runId:guid}/security-inputs", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid runId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var item = await service.GetRunSecurityInputsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
|
return item is null
|
|
? Results.NotFound(new { error = "run_not_found", runId })
|
|
: Results.Ok(new PlatformItemResponse<ReleaseRunSecurityInputsProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
item));
|
|
})
|
|
.WithName("GetReleaseRunSecurityInputsV2")
|
|
.WithSummary("Get release run security inputs projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
runs.MapGet("/{runId:guid}/evidence", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid runId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var item = await service.GetRunEvidenceAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
|
return item is null
|
|
? Results.NotFound(new { error = "run_not_found", runId })
|
|
: Results.Ok(new PlatformItemResponse<ReleaseRunEvidenceProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
item));
|
|
})
|
|
.WithName("GetReleaseRunEvidenceV2")
|
|
.WithSummary("Get release run evidence capsule projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
runs.MapGet("/{runId:guid}/rollback", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid runId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var item = await service.GetRunRollbackAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
|
return item is null
|
|
? Results.NotFound(new { error = "run_not_found", runId })
|
|
: Results.Ok(new PlatformItemResponse<ReleaseRunRollbackProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
item));
|
|
})
|
|
.WithName("GetReleaseRunRollbackV2")
|
|
.WithSummary("Get release run rollback projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
runs.MapGet("/{runId:guid}/replay", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid runId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var item = await service.GetRunReplayAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
|
return item is null
|
|
? Results.NotFound(new { error = "run_not_found", runId })
|
|
: Results.Ok(new PlatformItemResponse<ReleaseRunReplayProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
item));
|
|
})
|
|
.WithName("GetReleaseRunReplayV2")
|
|
.WithSummary("Get release run replay projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
runs.MapGet("/{runId:guid}/audit", async Task<IResult>(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
ReleaseReadModelService service,
|
|
TimeProvider timeProvider,
|
|
Guid runId,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var item = await service.GetRunAuditAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
|
|
return item is null
|
|
? Results.NotFound(new { error = "run_not_found", runId })
|
|
: Results.Ok(new PlatformItemResponse<ReleaseRunAuditProjection>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
timeProvider.GetUtcNow(),
|
|
Cached: false,
|
|
CacheTtlSeconds: 0,
|
|
item));
|
|
})
|
|
.WithName("GetReleaseRunAuditV2")
|
|
.WithSummary("Get release run audit projection")
|
|
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
|
|
|
return app;
|
|
}
|
|
|
|
private static bool TryResolveContext(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
out PlatformRequestContext? requestContext,
|
|
out IResult? failure)
|
|
{
|
|
if (resolver.TryResolve(context, out requestContext, out var error))
|
|
{
|
|
failure = null;
|
|
return true;
|
|
}
|
|
|
|
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
|
return false;
|
|
}
|
|
|
|
public sealed record ReleaseListQuery(
|
|
string? Region,
|
|
string? Environment,
|
|
string? Type,
|
|
string? Status,
|
|
int? Limit,
|
|
int? Offset);
|
|
|
|
public sealed record ReleaseActivityQuery(
|
|
string? Region,
|
|
string? Environment,
|
|
int? Limit,
|
|
int? Offset);
|
|
|
|
public sealed record ReleaseApprovalsQuery(
|
|
string? Status,
|
|
string? Region,
|
|
string? Environment,
|
|
int? Limit,
|
|
int? Offset);
|
|
|
|
public sealed record ReleaseRunListQuery(
|
|
string? Status,
|
|
string? Lane,
|
|
string? Environment,
|
|
string? Region,
|
|
string? Outcome,
|
|
bool? NeedsApproval,
|
|
bool? BlockedByDataIntegrity,
|
|
int? Limit,
|
|
int? Offset);
|
|
}
|