OPC # 0006: OPC Git Trunk-Based management
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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