using ControlPlane.Api.Services; using ControlPlane.Core.Services; using System.Text.Json; namespace ControlPlane.Api.Endpoints; public static class ReleaseEndpoints { private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web) { Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }, }; public static IEndpointRouteBuilder MapReleaseEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/release").WithTags("Release"); group.MapGet("/history", GetHistory); group.MapPost("/{env}", TriggerRelease); return app; } private static async Task GetHistory(BuildHistoryService history) => Results.Ok(await history.GetReleasesAsync()); /// /// Triggers a rolling redeploy of all managed containers in the target env. /// Streams SSE lines until release is complete. /// Valid env values: fdev | uat | prod | all /// private static async Task TriggerRelease( string env, HttpContext ctx, ReleaseService releases, CancellationToken ct) { var valid = new[] { "fdev", "uat", "prod", "all" }; if (!valid.Contains(env, StringComparer.OrdinalIgnoreCase)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync( new { error = $"Invalid environment '{env}'. Valid: fdev, uat, prod, all." }, ct); return; } ctx.Response.Headers.ContentType = "text/event-stream"; ctx.Response.Headers.CacheControl = "no-cache"; ctx.Response.Headers.Connection = "keep-alive"; async Task Send(object payload) { var json = JsonSerializer.Serialize(payload, JsonOpts); await ctx.Response.WriteAsync($"data: {json}\n\n", ct); await ctx.Response.Body.FlushAsync(ct); } void OnLine(string line) => Send(new { line }).GetAwaiter().GetResult(); var record = await releases.ReleaseAsync(env, OnLine, ct); await Send(new { done = true, release = record }); } }