OPC # 0006: OPC Git Trunk-Based management

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
amadzarak
2026-04-26 00:26:56 -04:00
parent 885ad47abe
commit db025cce01
7 changed files with 1238 additions and 349 deletions
@@ -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);