OPC # 0006: OPC Git Trunk-Based management
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using ControlPlane.Api.Services;
|
||||
using ControlPlane.Core.Services;
|
||||
|
||||
namespace ControlPlane.Api.Endpoints;
|
||||
|
||||
@@ -14,6 +15,7 @@ public static class ImageBuildEndpoints
|
||||
var group = app.MapGroup("/api/image").WithTags("Image");
|
||||
|
||||
group.MapGet("/status", GetStatus);
|
||||
group.MapGet("/history", GetHistory);
|
||||
group.MapPost("/build", TriggerBuild);
|
||||
|
||||
// Post-provisioning verification helpers
|
||||
@@ -28,6 +30,26 @@ public static class ImageBuildEndpoints
|
||||
private static async Task<IResult> GetStatus(ImageBuildService svc) =>
|
||||
Results.Ok(await svc.GetStatusAsync());
|
||||
|
||||
/// <summary>Returns recent DockerImage build records for the sparkline chart.</summary>
|
||||
private static async Task<IResult> GetHistory(BuildHistoryService history, int limit = 30)
|
||||
{
|
||||
var all = await history.GetBuildsAsync();
|
||||
var records = all
|
||||
.Where(b => b.Kind == ControlPlane.Core.Models.BuildKind.DockerImage)
|
||||
.Take(Math.Clamp(limit, 1, 100))
|
||||
.Select(b => new
|
||||
{
|
||||
b.Id,
|
||||
b.Status,
|
||||
b.StartedAt,
|
||||
b.DurationMs,
|
||||
b.CommitSha,
|
||||
b.ImageDigest,
|
||||
})
|
||||
.ToList();
|
||||
return Results.Ok(records);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a docker build and streams the output line-by-line as SSE.
|
||||
/// The build context is the repo root, which must be configured via
|
||||
|
||||
@@ -12,9 +12,9 @@ public static class PromotionEndpoints
|
||||
{
|
||||
var g = app.MapGroup("/api/promotions").WithTags("Promotions");
|
||||
|
||||
// GET /api/promotions/ladder — branch status for all 4 ladder branches
|
||||
g.MapGet("/ladder", async (PromotionService svc, CancellationToken ct) =>
|
||||
Results.Ok(await svc.GetLadderStatusAsync(ct)));
|
||||
// GET /api/promotions/ladder?repo=Clarity — branch status for all 4 ladder branches
|
||||
g.MapGet("/ladder", async (PromotionService svc, CancellationToken ct, string repo = "Clarity") =>
|
||||
Results.Ok(await svc.GetLadderStatusAsync(repo, ct)));
|
||||
|
||||
// GET /api/promotions/history
|
||||
g.MapGet("/history", async (PromotionService svc) =>
|
||||
@@ -50,7 +50,7 @@ public static class PromotionEndpoints
|
||||
void OnLine(string line) => channel.Writer.TryWrite(line);
|
||||
|
||||
var promoteTask = Task.Run(() =>
|
||||
svc.PromoteAsync(req.From, req.To, req.RequestedBy ?? "system", req.Note, OnLine, ct), ct)
|
||||
svc.PromoteAsync(req.From, req.To, req.RequestedBy ?? "system", req.Note, OnLine, ct, req.Repo ?? "Clarity"), ct)
|
||||
.ContinueWith(t => channel.Writer.TryComplete(t.Exception), TaskScheduler.Default);
|
||||
|
||||
await foreach (var line in channel.Reader.ReadAllAsync(ct))
|
||||
@@ -66,8 +66,84 @@ public static class PromotionEndpoints
|
||||
await ctx.Response.Body.FlushAsync(ct);
|
||||
});
|
||||
|
||||
// POST /api/promotions/reset — body: { branch, toSha, repo }
|
||||
// Force-resets a downstream branch to a specific SHA (e.g. to recover from a GitFlow merge commit).
|
||||
// Only allowed for staging/uat — never develop or main.
|
||||
g.MapPost("/reset", async (PromotionService svc, ResetBranchRequest req, CancellationToken ct) =>
|
||||
{
|
||||
var allowed = new[] { "staging", "uat" };
|
||||
if (!allowed.Contains(req.Branch))
|
||||
return Results.BadRequest(new { error = $"Reset is only allowed for: {string.Join(", ", allowed)}." });
|
||||
|
||||
try
|
||||
{
|
||||
await svc.ResetBranchAsync(req.Branch, req.ToSha, req.Repo ?? "Clarity", ct);
|
||||
return Results.Ok(new { reset = req.Branch, toSha = req.ToSha });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/promotions/cherry-pick — body: { shas, from, to, requestedBy, note, repo }
|
||||
// Streams SSE log lines then sends {done, promotion} when complete.
|
||||
// Unlike a full promote, cherry-pick applies selected commits as copies — branches will diverge.
|
||||
g.MapPost("/cherry-pick", async (
|
||||
HttpContext ctx,
|
||||
PromotionService svc,
|
||||
CherryPickRequest req,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var ladder = PromotionService.Ladder;
|
||||
var fi = Array.IndexOf(ladder, req.From);
|
||||
var ti = Array.IndexOf(ladder, req.To);
|
||||
if (fi < 0 || ti < 0 || ti != fi + 1)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(
|
||||
new { error = $"Invalid cherry-pick target: {req.From} → {req.To}. Must be adjacent in ladder." }, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.Shas is null || req.Shas.Length == 0)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(
|
||||
new { error = "No commits specified for cherry-pick." }, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Response.Headers.ContentType = "text/event-stream";
|
||||
ctx.Response.Headers.CacheControl = "no-cache";
|
||||
ctx.Response.Headers.Connection = "keep-alive";
|
||||
|
||||
var channel = System.Threading.Channels.Channel.CreateUnbounded<string?>(
|
||||
new System.Threading.Channels.UnboundedChannelOptions { SingleWriter = true, SingleReader = true });
|
||||
|
||||
void OnLine(string line) => channel.Writer.TryWrite(line);
|
||||
|
||||
var cpTask = Task.Run(() =>
|
||||
svc.CherryPickAsync(req.Shas, req.From, req.To, req.RequestedBy ?? "system", req.Note, OnLine, ct, req.Repo ?? "Clarity"), ct)
|
||||
.ContinueWith(t => channel.Writer.TryComplete(t.Exception), TaskScheduler.Default);
|
||||
|
||||
await foreach (var line in channel.Reader.ReadAllAsync(ct))
|
||||
{
|
||||
var json = JsonSerializer.Serialize(new { line }, JsonOpts);
|
||||
await ctx.Response.WriteAsync($"data: {json}\n\n", ct);
|
||||
await ctx.Response.Body.FlushAsync(ct);
|
||||
}
|
||||
|
||||
var promotion = await cpTask;
|
||||
var doneJson = JsonSerializer.Serialize(new { done = true, promotion }, JsonOpts);
|
||||
await ctx.Response.WriteAsync($"data: {doneJson}\n\n", ct);
|
||||
await ctx.Response.Body.FlushAsync(ct);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
public record PromoteRequest(string From, string To, string? RequestedBy, string? Note);
|
||||
public record PromoteRequest(string From, string To, string? RequestedBy, string? Note, string? Repo);
|
||||
public record ResetBranchRequest(string Branch, string ToSha, string? Repo);
|
||||
public record CherryPickRequest(string[] Shas, string From, string To, string? RequestedBy, string? Note, string? Repo);
|
||||
|
||||
Reference in New Issue
Block a user