OPC # 0006: OPC Git Trunk-Based management

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
amadzarak
2026-04-26 11:54:24 -04:00
parent 553ea59d39
commit 79c69e1363
10 changed files with 252 additions and 33 deletions
@@ -1,5 +1,6 @@
using ControlPlane.Api.Services;
using ControlPlane.Core.Models;
using ControlPlane.Core.Services;
using System.Text.Json;
namespace ControlPlane.Api.Endpoints;
@@ -178,6 +179,27 @@ public static class PromotionEndpoints
}
});
// GET /api/promotions/build-gate?sha={sha}
// Returns the build-gate status for the given commit SHA.
// If status is "Red", the promote button in the UI should be disabled.
g.MapGet("/build-gate", async (string sha, BuildHistoryService history, CancellationToken ct) =>
{
var builds = await history.GetBuildsByShaAsync(sha);
var latest = builds.MaxBy(b => b.StartedAt);
if (latest is null)
return Results.Ok(new { status = "Unknown", sha, buildId = (string?)null, buildStatus = (string?)null });
var gateStatus = latest.Status switch
{
BuildStatus.Succeeded => "Green",
BuildStatus.Failed => "Red",
BuildStatus.Running => "Running",
_ => "Unknown",
};
return Results.Ok(new { status = gateStatus, sha, buildId = latest.Id, buildStatus = latest.Status.ToString() });
});
return app;
}
}
+6
View File
@@ -163,4 +163,10 @@ await using (var cmd = ds.CreateCommand("""
"""))
await cmd.ExecuteNonQueryAsync();
// Idempotent column additions for schema migrations
await using (var migCmd = ds.CreateCommand("""
ALTER TABLE release_record ADD COLUMN IF NOT EXISTS opc_numbers TEXT[] NOT NULL DEFAULT '{}';
"""))
await migCmd.ExecuteNonQueryAsync();
app.Run();
+46 -2
View File
@@ -138,7 +138,7 @@ public class PromotionService(IConfiguration config, ILogger<PromotionService> l
}
result.Add(new BranchStatus(branchName, true, tip.Sha[..7], summary,
ahead, behind, unreleasedCommits));
ahead, behind, unreleasedCommits, tip.Sha));
}
return result;
@@ -716,6 +716,49 @@ public class PromotionService(IConfiguration config, ILogger<PromotionService> l
try { return JsonSerializer.Deserialize<List<PromotionRequest>>(File.ReadAllText(HistoryPath), JsonOpts) ?? []; }
catch { return []; }
}
// ── OPC number extraction ─────────────────────────────────────────────
private static readonly System.Text.RegularExpressions.Regex OpcTagPattern =
new(@"OPC\s*#\s*(\d+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase
| System.Text.RegularExpressions.RegexOptions.Compiled);
/// <summary>
/// Scans the most recent <paramref name="limit"/> commits on <paramref name="branch"/> and
/// returns a distinct, sorted list of OPC numbers referenced in commit messages (e.g. "OPC # 0042").
/// Safe to call when git is not configured — returns an empty list on any error.
/// </summary>
public Task<List<string>> ExtractOpcNumbersAsync(
string repoName = "Clarity",
string branch = "main",
int limit = 50,
CancellationToken ct = default) =>
Task.Run(() => ExtractOpcNumbersCore(repoName, branch, limit), ct);
private List<string> ExtractOpcNumbersCore(string repoName, string branch, int limit)
{
var repoPath = GetRepoPath(repoName);
if (string.IsNullOrWhiteSpace(repoPath) || !Directory.Exists(repoPath))
return [];
try
{
using var repo = new Repository(repoPath);
var b = repo.Branches[branch] ?? repo.Branches[$"origin/{branch}"];
if (b is null) return [];
var set = new HashSet<string>(StringComparer.Ordinal);
foreach (var commit in b.Commits.Take(limit))
foreach (System.Text.RegularExpressions.Match m in OpcTagPattern.Matches(commit.Message))
set.Add($"OPC # {m.Groups[1].Value.PadLeft(4, '0')}");
return [.. set.OrderBy(x => x)];
}
catch (Exception ex)
{
logger.LogWarning(ex, "ExtractOpcNumbers failed for {Repo}/{Branch}", repoName, branch);
return [];
}
}
}
/// <summary>A single unreleased commit — carries full SHA for cherry-pick operations.</summary>
@@ -729,5 +772,6 @@ public record BranchStatus(
string? LastCommitSummary,
int AheadOfNext, // commits this branch has that the next doesn't
int BehindNext, // commits next has that this branch doesn't (diverged)
CommitInfo[] UnreleasedCommits // rich commit objects for cherry-pick UI
CommitInfo[] UnreleasedCommits, // rich commit objects for cherry-pick UI
string? TipSha = null // full 40-char SHA for build-gate checks
);
@@ -17,6 +17,7 @@ public class ReleaseService(
IConfiguration config,
TenantRegistryService registry,
BuildHistoryService history,
PromotionService promotions,
ILogger<ReleaseService> logger)
{
private static readonly SemaphoreSlim _lock = new(1, 1);
@@ -182,6 +183,11 @@ public class ReleaseService(
}
finally
{
// Stamp OPC ticket numbers from recent commits on the target branch.
var branch = targetEnv switch { "fdev" => "develop", "staging" => "staging", "uat" => "uat", _ => "main" };
try { record.OpcNumbers = await promotions.ExtractOpcNumbersAsync("Clarity", branch, 50, ct); }
catch { /* git not configured — continue without OPC stamp */ }
await history.UpdateReleaseAsync(record);
_lock.Release();
}