Compare commits
2 Commits
5e969a2b3e
...
uat
| Author | SHA1 | Date | |
|---|---|---|---|
| 2badb5264b | |||
| bb0c6e08c7 |
@@ -166,6 +166,7 @@ await using (var cmd = ds.CreateCommand("""
|
|||||||
// Idempotent column additions for schema migrations
|
// Idempotent column additions for schema migrations
|
||||||
await using (var migCmd = ds.CreateCommand("""
|
await using (var migCmd = ds.CreateCommand("""
|
||||||
ALTER TABLE release_record ADD COLUMN IF NOT EXISTS opc_numbers TEXT[] NOT NULL DEFAULT '{}';
|
ALTER TABLE release_record ADD COLUMN IF NOT EXISTS opc_numbers TEXT[] NOT NULL DEFAULT '{}';
|
||||||
|
ALTER TABLE release_record ADD COLUMN IF NOT EXISTS commit_sha VARCHAR(40);
|
||||||
"""))
|
"""))
|
||||||
await migCmd.ExecuteNonQueryAsync();
|
await migCmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -787,6 +787,64 @@ public class PromotionService(IConfiguration config, ILogger<PromotionService> l
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns distinct, sorted OPC numbers for commits reachable from <paramref name="toSha"/>
|
||||||
|
/// that are NOT reachable from <paramref name="fromSha"/> — i.e. the exact delta for this release.
|
||||||
|
/// Falls back to <see cref="ExtractOpcNumbersAsync"/> (last 50 commits) when <paramref name="fromSha"/>
|
||||||
|
/// is null (first-ever release for this environment).
|
||||||
|
/// </summary>
|
||||||
|
public Task<List<string>> ExtractOpcNumbersDeltaAsync(
|
||||||
|
string repoName,
|
||||||
|
string toSha,
|
||||||
|
string? fromSha,
|
||||||
|
CancellationToken ct = default) =>
|
||||||
|
fromSha is null
|
||||||
|
? ExtractOpcNumbersAsync(repoName, ct: ct)
|
||||||
|
: Task.Run(() => ExtractOpcNumbersDeltaCore(repoName, toSha, fromSha), ct);
|
||||||
|
|
||||||
|
private List<string> ExtractOpcNumbersDeltaCore(string repoName, string toSha, string fromSha)
|
||||||
|
{
|
||||||
|
var repoPath = GetRepoPath(repoName);
|
||||||
|
if (string.IsNullOrWhiteSpace(repoPath) || !Directory.Exists(repoPath))
|
||||||
|
return [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var repo = new Repository(repoPath);
|
||||||
|
var toCommit = repo.Lookup<Commit>(toSha);
|
||||||
|
var fromCommit = repo.Lookup<Commit>(fromSha);
|
||||||
|
if (toCommit is null) return [];
|
||||||
|
|
||||||
|
var filter = fromCommit is null
|
||||||
|
? new CommitFilter { IncludeReachableFrom = toCommit }
|
||||||
|
: new CommitFilter { IncludeReachableFrom = toCommit, ExcludeReachableFrom = fromCommit };
|
||||||
|
|
||||||
|
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (var commit in repo.Commits.QueryBy(filter))
|
||||||
|
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, "ExtractOpcNumbersDelta failed for {Repo} {From}..{To}", repoName, fromSha[..7], toSha[..7]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the full HEAD SHA of <paramref name="branch"/> in <paramref name="repoName"/>, or null.</summary>
|
||||||
|
public string? GetBranchTipSha(string repoName, string branch)
|
||||||
|
{
|
||||||
|
var repoPath = GetRepoPath(repoName);
|
||||||
|
if (string.IsNullOrWhiteSpace(repoPath) || !Directory.Exists(repoPath)) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var repo = new Repository(repoPath);
|
||||||
|
return (repo.Branches[$"origin/{branch}"] ?? repo.Branches[branch])?.Tip?.Sha;
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A single unreleased commit — carries full SHA for cherry-pick operations.</summary>
|
/// <summary>A single unreleased commit — carries full SHA for cherry-pick operations.</summary>
|
||||||
|
|||||||
@@ -51,7 +51,12 @@ public class ReleaseService(
|
|||||||
return blocked;
|
return blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
var record = await history.CreateReleaseAsync(targetEnv, ImageName);
|
// Resolve the Clarity branch for this environment and stamp the HEAD SHA
|
||||||
|
// before creating the record so we capture "what was deployed" accurately.
|
||||||
|
var branch = targetEnv switch { "fdev" => "develop", "staging" => "staging", "uat" => "uat", _ => "main" };
|
||||||
|
var currentSha = promotions.GetBranchTipSha("Clarity", branch);
|
||||||
|
|
||||||
|
var record = await history.CreateReleaseAsync(targetEnv, ImageName, currentSha);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -183,9 +188,16 @@ public class ReleaseService(
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// Stamp OPC ticket numbers from recent commits on the target branch.
|
// Stamp the exact OPC ticket numbers introduced by this release:
|
||||||
var branch = targetEnv switch { "fdev" => "develop", "staging" => "staging", "uat" => "uat", _ => "main" };
|
// diff from previous release's SHA to this release's SHA on the Clarity branch.
|
||||||
try { record.OpcNumbers = await promotions.ExtractOpcNumbersAsync("Clarity", branch, 50, ct); }
|
try
|
||||||
|
{
|
||||||
|
var prev = await history.GetLastSuccessfulReleaseForEnvAsync(targetEnv);
|
||||||
|
// Exclude the current (in-flight) record — it's not succeeded yet
|
||||||
|
var prevSha = prev?.Id == record.Id ? null : prev?.CommitSha;
|
||||||
|
if (currentSha is not null)
|
||||||
|
record.OpcNumbers = await promotions.ExtractOpcNumbersDeltaAsync("Clarity", currentSha, prevSha, ct);
|
||||||
|
}
|
||||||
catch { /* git not configured — continue without OPC stamp */ }
|
catch { /* git not configured — continue without OPC stamp */ }
|
||||||
|
|
||||||
await history.UpdateReleaseAsync(record);
|
await history.UpdateReleaseAsync(record);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class ReleaseRecord
|
|||||||
public DateTimeOffset? FinishedAt { get; set; }
|
public DateTimeOffset? FinishedAt { get; set; }
|
||||||
public List<TenantReleaseResult> Tenants { get; set; } = [];
|
public List<TenantReleaseResult> Tenants { get; set; } = [];
|
||||||
public List<string> OpcNumbers { get; set; } = [];
|
public List<string> OpcNumbers { get; set; } = [];
|
||||||
|
public string? CommitSha { get; set; } // Clarity branch HEAD SHA at release time
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TenantReleaseResult
|
public class TenantReleaseResult
|
||||||
|
|||||||
@@ -142,13 +142,13 @@ public class BuildHistoryService(NpgsqlDataSource db, ILogger<BuildHistoryServic
|
|||||||
|
|
||||||
// ── Releases ────────────────────────────────────────────────────────────
|
// ── Releases ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public async Task<ReleaseRecord> CreateReleaseAsync(string environment, string imageName)
|
public async Task<ReleaseRecord> CreateReleaseAsync(string environment, string imageName, string? commitSha = null)
|
||||||
{
|
{
|
||||||
var record = new ReleaseRecord { Environment = environment, ImageName = imageName };
|
var record = new ReleaseRecord { Environment = environment, ImageName = imageName, CommitSha = commitSha };
|
||||||
|
|
||||||
await using var cmd = db.CreateCommand("""
|
await using var cmd = db.CreateCommand("""
|
||||||
INSERT INTO release_record (id, environment, image_name, status, started_at, opc_numbers)
|
INSERT INTO release_record (id, environment, image_name, status, started_at, opc_numbers, commit_sha)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
""");
|
""");
|
||||||
cmd.Parameters.AddWithValue(record.Id);
|
cmd.Parameters.AddWithValue(record.Id);
|
||||||
cmd.Parameters.AddWithValue(record.Environment);
|
cmd.Parameters.AddWithValue(record.Environment);
|
||||||
@@ -156,6 +156,7 @@ public class BuildHistoryService(NpgsqlDataSource db, ILogger<BuildHistoryServic
|
|||||||
cmd.Parameters.AddWithValue(record.Status.ToString());
|
cmd.Parameters.AddWithValue(record.Status.ToString());
|
||||||
cmd.Parameters.AddWithValue(record.StartedAt);
|
cmd.Parameters.AddWithValue(record.StartedAt);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter<string[]> { TypedValue = [.. record.OpcNumbers] });
|
cmd.Parameters.Add(new NpgsqlParameter<string[]> { TypedValue = [.. record.OpcNumbers] });
|
||||||
|
cmd.Parameters.AddWithValue((object?)record.CommitSha ?? DBNull.Value);
|
||||||
await cmd.ExecuteNonQueryAsync();
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
return record;
|
return record;
|
||||||
@@ -169,12 +170,13 @@ public class BuildHistoryService(NpgsqlDataSource db, ILogger<BuildHistoryServic
|
|||||||
await using var tx = await conn.BeginTransactionAsync();
|
await using var tx = await conn.BeginTransactionAsync();
|
||||||
|
|
||||||
await using var upd = new NpgsqlCommand("""
|
await using var upd = new NpgsqlCommand("""
|
||||||
UPDATE release_record SET status = $2, finished_at = $3, opc_numbers = $4 WHERE id = $1
|
UPDATE release_record SET status = $2, finished_at = $3, opc_numbers = $4, commit_sha = $5 WHERE id = $1
|
||||||
""", conn, tx);
|
""", conn, tx);
|
||||||
upd.Parameters.AddWithValue(record.Id);
|
upd.Parameters.AddWithValue(record.Id);
|
||||||
upd.Parameters.AddWithValue(record.Status.ToString());
|
upd.Parameters.AddWithValue(record.Status.ToString());
|
||||||
upd.Parameters.AddWithValue(record.FinishedAt!.Value);
|
upd.Parameters.AddWithValue(record.FinishedAt!.Value);
|
||||||
upd.Parameters.Add(new NpgsqlParameter<string[]> { TypedValue = [.. record.OpcNumbers] });
|
upd.Parameters.Add(new NpgsqlParameter<string[]> { TypedValue = [.. record.OpcNumbers] });
|
||||||
|
upd.Parameters.AddWithValue((object?)record.CommitSha ?? DBNull.Value);
|
||||||
await upd.ExecuteNonQueryAsync();
|
await upd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
// Replace tenant results wholesale on each update
|
// Replace tenant results wholesale on each update
|
||||||
@@ -206,7 +208,7 @@ public class BuildHistoryService(NpgsqlDataSource db, ILogger<BuildHistoryServic
|
|||||||
var lookup = new Dictionary<string, ReleaseRecord>();
|
var lookup = new Dictionary<string, ReleaseRecord>();
|
||||||
|
|
||||||
await using var cmd = db.CreateCommand("""
|
await using var cmd = db.CreateCommand("""
|
||||||
SELECT id, environment, image_name, status, started_at, finished_at, opc_numbers
|
SELECT id, environment, image_name, status, started_at, finished_at, opc_numbers, commit_sha
|
||||||
FROM release_record
|
FROM release_record
|
||||||
ORDER BY started_at DESC
|
ORDER BY started_at DESC
|
||||||
LIMIT 50
|
LIMIT 50
|
||||||
@@ -224,6 +226,7 @@ public class BuildHistoryService(NpgsqlDataSource db, ILogger<BuildHistoryServic
|
|||||||
StartedAt = reader.GetFieldValue<DateTimeOffset>(4),
|
StartedAt = reader.GetFieldValue<DateTimeOffset>(4),
|
||||||
FinishedAt = reader.IsDBNull(5) ? null : reader.GetFieldValue<DateTimeOffset>(5),
|
FinishedAt = reader.IsDBNull(5) ? null : reader.GetFieldValue<DateTimeOffset>(5),
|
||||||
OpcNumbers = reader.IsDBNull(6) ? [] : [.. reader.GetFieldValue<string[]>(6)],
|
OpcNumbers = reader.IsDBNull(6) ? [] : [.. reader.GetFieldValue<string[]>(6)],
|
||||||
|
CommitSha = reader.IsDBNull(7) ? null : reader.GetString(7),
|
||||||
};
|
};
|
||||||
ordered.Add(r);
|
ordered.Add(r);
|
||||||
lookup[r.Id] = r;
|
lookup[r.Id] = r;
|
||||||
@@ -254,5 +257,34 @@ public class BuildHistoryService(NpgsqlDataSource db, ILogger<BuildHistoryServic
|
|||||||
|
|
||||||
return ordered;
|
return ordered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the most recent succeeded release for <paramref name="environment"/>, or null if none exists.
|
||||||
|
/// Used to calculate the OPC ticket delta between releases (previousSha..currentSha).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ReleaseRecord?> GetLastSuccessfulReleaseForEnvAsync(string environment)
|
||||||
|
{
|
||||||
|
await using var cmd = db.CreateCommand("""
|
||||||
|
SELECT id, environment, image_name, status, started_at, finished_at, opc_numbers, commit_sha
|
||||||
|
FROM release_record
|
||||||
|
WHERE environment = $1 AND status = 'Succeeded'
|
||||||
|
ORDER BY started_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
""");
|
||||||
|
cmd.Parameters.AddWithValue(environment);
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
if (!await reader.ReadAsync()) return null;
|
||||||
|
return new ReleaseRecord
|
||||||
|
{
|
||||||
|
Id = reader.GetString(0),
|
||||||
|
Environment = reader.GetString(1),
|
||||||
|
ImageName = reader.GetString(2),
|
||||||
|
Status = Enum.Parse<ReleaseStatus>(reader.GetString(3)),
|
||||||
|
StartedAt = reader.GetFieldValue<DateTimeOffset>(4),
|
||||||
|
FinishedAt = reader.IsDBNull(5) ? null : reader.GetFieldValue<DateTimeOffset>(5),
|
||||||
|
OpcNumbers = reader.IsDBNull(6) ? [] : [.. reader.GetFieldValue<string[]>(6)],
|
||||||
|
CommitSha = reader.IsDBNull(7) ? null : reader.GetString(7),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -804,30 +804,116 @@ body {
|
|||||||
|
|
||||||
.opc-sdlc-pipeline {
|
.opc-sdlc-pipeline {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
gap: 0.2rem;
|
gap: 0;
|
||||||
margin-bottom: 0.35rem;
|
overflow-x: auto;
|
||||||
}
|
padding-bottom: 0.25rem;
|
||||||
|
|
||||||
.opc-sdlc-stage-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.opc-sdlc-arrow {
|
.opc-sdlc-arrow {
|
||||||
color: #8f99a8;
|
color: #8f99a8;
|
||||||
font-size: 0.8rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0 0.1rem;
|
flex-shrink: 0;
|
||||||
|
align-self: center;
|
||||||
|
margin: 0 0.4rem;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opc-sdlc-furthest {
|
/* Individual branch box */
|
||||||
font-size: 0.75rem;
|
.opc-sdlc-box {
|
||||||
|
flex: 1 1 140px;
|
||||||
|
min-width: 130px;
|
||||||
|
max-width: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #dce0e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opc-sdlc-box--reached {
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opc-sdlc-box-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
background: #f6f7f9;
|
||||||
|
border-bottom: 1px solid #e5e8eb;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opc-sdlc-box-count {
|
||||||
|
font-size: 0.68rem;
|
||||||
color: #738091;
|
color: #738091;
|
||||||
margin-top: 0.3rem;
|
background: #e5e8eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 6px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollable body */
|
||||||
|
.opc-sdlc-box-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 140px;
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 0.3rem 0.4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opc-sdlc-sha-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.1rem 0.2rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opc-sdlc-sha-row--reached {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opc-sdlc-sha {
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #2d72d2;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opc-sdlc-sha-msg {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: #4a5568;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opc-sdlc-box-empty {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #a3acb6;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opc-sdlc-box-pending {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: #a3acb6;
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
border-top: 1px dashed #e5e8eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Commits section labels */
|
/* Commits section labels */
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useEffect, useCallback } from 'react';
|
import { useState, useMemo, useEffect, useCallback, Fragment } from 'react';
|
||||||
import { GitCommitDrawer } from '../components/GitCommitDrawer';
|
import { GitCommitDrawer } from '../components/GitCommitDrawer';
|
||||||
import {
|
import {
|
||||||
Button, Callout, Divider, Drawer, FormGroup,
|
Button, Callout, Divider, Drawer, FormGroup,
|
||||||
@@ -79,15 +79,6 @@ const SDLC_STAGES: { branch: string; label: string; intent: Intent }[] = [
|
|||||||
{ branch: 'main', label: 'Production', intent: Intent.SUCCESS },
|
{ branch: 'main', label: 'Production', intent: Intent.SUCCESS },
|
||||||
];
|
];
|
||||||
|
|
||||||
function deriveSdlcSummary(coverage: BranchCoverage[]): { label: string; intent: Intent } | null {
|
|
||||||
for (let i = SDLC_STAGES.length - 1; i >= 0; i--) {
|
|
||||||
const stage = SDLC_STAGES[i];
|
|
||||||
const hit = coverage.find(c => c.branch === stage.branch);
|
|
||||||
if (hit?.contains) return { label: stage.label, intent: stage.intent };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aggregate per-repo branch coverage into a single view.
|
// Aggregate per-repo branch coverage into a single view.
|
||||||
// A stage is "reached" only when every repo that recognised at least one hash
|
// A stage is "reached" only when every repo that recognised at least one hash
|
||||||
// reports contains=true for that branch. Repos that recognised no hashes are
|
// reports contains=true for that branch. Repos that recognised no hashes are
|
||||||
@@ -487,42 +478,48 @@ function CommitsTab({ opc, isActive }: { opc: Opc; isActive: boolean }) {
|
|||||||
|
|
||||||
{/* SDLC Delivery Chain */}
|
{/* SDLC Delivery Chain */}
|
||||||
{coverage.length > 0 && (() => {
|
{coverage.length > 0 && (() => {
|
||||||
const summary = deriveSdlcSummary(coverage);
|
const allCommits = [
|
||||||
|
...autoCommits,
|
||||||
|
...pinned.map(p => ({ repoKey: 'pinned', hash: p.hash, shortHash: p.shortHash, author: p.pinnedBy, date: p.pinnedAt, subject: p.subject, files: [] })),
|
||||||
|
].filter((c, i, a) => a.findIndex(x => x.hash === c.hash) === i);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="opc-delivery-chain">
|
<div className="opc-delivery-chain">
|
||||||
<div className="opc-field-label" style={{ marginBottom: '0.6rem' }}>Delivery Chain</div>
|
<div className="opc-field-label" style={{ marginBottom: '0.75rem' }}>Delivery Chain</div>
|
||||||
<div className="opc-sdlc-pipeline">
|
<div className="opc-sdlc-pipeline">
|
||||||
{SDLC_STAGES.map((stage, i) => {
|
{SDLC_STAGES.map((stage, i) => {
|
||||||
const hit = coverage.find(c => c.branch === stage.branch);
|
const hit = coverage.find(c => c.branch === stage.branch);
|
||||||
const reached = hit?.contains ?? false;
|
const reached = hit?.contains ?? false;
|
||||||
return (
|
return (
|
||||||
<div key={stage.branch} className="opc-sdlc-stage-item">
|
<Fragment key={stage.branch}>
|
||||||
{i > 0 && <span className="opc-sdlc-arrow">→</span>}
|
{i > 0 && <span className="opc-sdlc-arrow">→</span>}
|
||||||
<Tooltip content={
|
<div className={`opc-sdlc-box${reached ? ' opc-sdlc-box--reached' : ''}`} style={{ borderColor: reached ? SDLC_STAGES[i].intent === 'primary' ? '#2d72d2' : SDLC_STAGES[i].intent === 'warning' ? '#c87619' : SDLC_STAGES[i].intent === 'danger' ? '#ac2f33' : '#1c6e42' : '#dce0e6' }}>
|
||||||
reached
|
{/* Box header */}
|
||||||
? `All linked commits have reached ${stage.label}`
|
<div className="opc-sdlc-box-header">
|
||||||
: hit
|
<Tag intent={reached ? stage.intent : Intent.NONE} minimal={!reached} round style={{ fontWeight: 600, fontSize: '0.72rem' }}>
|
||||||
? `Not all linked commits have reached ${stage.label} yet`
|
{stage.label}
|
||||||
: `${stage.label} branch not found locally`
|
</Tag>
|
||||||
}>
|
{reached && <span className="opc-sdlc-box-count">{allCommits.length}</span>}
|
||||||
<Tag
|
</div>
|
||||||
intent={reached ? stage.intent : Intent.NONE}
|
{/* Scrollable SHA list */}
|
||||||
icon={reached ? 'tick-circle' : 'circle'}
|
<div className="opc-sdlc-box-body">
|
||||||
minimal={!reached}
|
{allCommits.length === 0 ? (
|
||||||
round
|
<span className="opc-sdlc-box-empty">No linked commits</span>
|
||||||
>
|
) : allCommits.map(c => (
|
||||||
{stage.label}
|
<div key={c.hash} className={`opc-sdlc-sha-row${reached ? ' opc-sdlc-sha-row--reached' : ''}`} title={c.subject}>
|
||||||
</Tag>
|
<code className="opc-sdlc-sha">{c.shortHash}</code>
|
||||||
</Tooltip>
|
<span className="opc-sdlc-sha-msg">{c.subject}</span>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
{!reached && allCommits.length > 0 && (
|
||||||
|
<div className="opc-sdlc-box-pending">Not yet promoted</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{summary && (
|
|
||||||
<div className="opc-sdlc-furthest">
|
|
||||||
Furthest: <strong>{summary.label}</strong>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
Reference in New Issue
Block a user