251 lines
8.9 KiB
C#
251 lines
8.9 KiB
C#
using System.Globalization;
|
|
using StellaOps.JobEngine.WebService.Endpoints;
|
|
|
|
namespace StellaOps.JobEngine.WebService.Services;
|
|
|
|
/// <summary>
|
|
/// Builds deterministic release dashboard snapshots from in-memory seed data.
|
|
/// </summary>
|
|
public static class ReleaseDashboardSnapshotBuilder
|
|
{
|
|
private static readonly PipelineDefinition[] PipelineDefinitions =
|
|
{
|
|
new("dev", "development", "Development", 1),
|
|
new("staging", "staging", "Staging", 2),
|
|
new("uat", "uat", "UAT", 3),
|
|
new("production", "production", "Production", 4),
|
|
};
|
|
|
|
private static readonly HashSet<string> AllowedReleaseStatuses = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"draft",
|
|
"ready",
|
|
"promoting",
|
|
"deployed",
|
|
"failed",
|
|
"deprecated",
|
|
"rolled_back",
|
|
};
|
|
|
|
public static ReleaseDashboardSnapshot Build(
|
|
IReadOnlyList<ApprovalEndpoints.ApprovalDto>? approvals = null,
|
|
IReadOnlyList<ReleaseEndpoints.ManagedReleaseDto>? releases = null)
|
|
{
|
|
var releaseItems = (releases ?? ReleaseEndpoints.SeedData.Releases)
|
|
.OrderByDescending(release => release.CreatedAt)
|
|
.ThenBy(release => release.Id, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
var approvalItems = (approvals ?? ApprovalEndpoints.SeedData.Approvals)
|
|
.OrderBy(approval => ParseTimestamp(approval.RequestedAt))
|
|
.ThenBy(approval => approval.Id, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
var pendingApprovals = approvalItems
|
|
.Where(approval => string.Equals(approval.Status, "pending", StringComparison.OrdinalIgnoreCase))
|
|
.Select(approval => new PendingApprovalItem(
|
|
approval.Id,
|
|
approval.ReleaseId,
|
|
approval.ReleaseName,
|
|
approval.ReleaseVersion,
|
|
ToDisplayEnvironment(approval.SourceEnvironment),
|
|
ToDisplayEnvironment(approval.TargetEnvironment),
|
|
approval.RequestedBy,
|
|
approval.RequestedAt,
|
|
NormalizeUrgency(approval.Urgency)))
|
|
.ToArray();
|
|
|
|
var activeDeployments = releaseItems
|
|
.Where(release => string.Equals(release.Status, "deploying", StringComparison.OrdinalIgnoreCase))
|
|
.OrderByDescending(release => release.UpdatedAt)
|
|
.ThenBy(release => release.Id, StringComparer.Ordinal)
|
|
.Select((release, index) =>
|
|
{
|
|
var progress = Math.Min(90, 45 + (index * 15));
|
|
var totalTargets = Math.Max(1, release.ComponentCount);
|
|
var completedTargets = Math.Clamp(
|
|
(int)Math.Round(totalTargets * (progress / 100d), MidpointRounding.AwayFromZero),
|
|
1,
|
|
totalTargets);
|
|
|
|
return new ActiveDeploymentItem(
|
|
Id: $"dep-{release.Id}",
|
|
ReleaseId: release.Id,
|
|
ReleaseName: release.Name,
|
|
ReleaseVersion: release.Version,
|
|
Environment: ToDisplayEnvironment(release.TargetEnvironment ?? release.CurrentEnvironment ?? "staging"),
|
|
Progress: progress,
|
|
Status: "running",
|
|
StartedAt: release.UpdatedAt.ToString("O"),
|
|
CompletedTargets: completedTargets,
|
|
TotalTargets: totalTargets);
|
|
})
|
|
.ToArray();
|
|
|
|
var pipelineEnvironments = PipelineDefinitions
|
|
.Select(definition =>
|
|
{
|
|
var releaseCount = releaseItems.Count(release =>
|
|
string.Equals(NormalizeEnvironment(release.CurrentEnvironment), definition.NormalizedName, StringComparison.OrdinalIgnoreCase));
|
|
var pendingCount = pendingApprovals.Count(approval =>
|
|
string.Equals(NormalizeEnvironment(approval.TargetEnvironment), definition.NormalizedName, StringComparison.OrdinalIgnoreCase));
|
|
var hasActiveDeployment = activeDeployments.Any(deployment =>
|
|
string.Equals(NormalizeEnvironment(deployment.Environment), definition.NormalizedName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
var healthStatus = hasActiveDeployment || pendingCount > 0
|
|
? "degraded"
|
|
: releaseCount > 0
|
|
? "healthy"
|
|
: "unknown";
|
|
|
|
return new PipelineEnvironmentItem(
|
|
definition.Id,
|
|
definition.NormalizedName,
|
|
definition.DisplayName,
|
|
definition.Order,
|
|
releaseCount,
|
|
pendingCount,
|
|
healthStatus);
|
|
})
|
|
.ToArray();
|
|
|
|
var pipelineConnections = PipelineDefinitions
|
|
.Skip(1)
|
|
.Select((definition, index) => new PipelineConnectionItem(
|
|
PipelineDefinitions[index].Id,
|
|
definition.Id))
|
|
.ToArray();
|
|
|
|
var recentReleases = releaseItems
|
|
.Take(10)
|
|
.Select(release => new RecentReleaseItem(
|
|
release.Id,
|
|
release.Name,
|
|
release.Version,
|
|
NormalizeReleaseStatus(release.Status),
|
|
release.CurrentEnvironment is null ? null : ToDisplayEnvironment(release.CurrentEnvironment),
|
|
release.CreatedAt.ToString("O"),
|
|
string.IsNullOrWhiteSpace(release.CreatedBy) ? "system" : release.CreatedBy,
|
|
release.ComponentCount))
|
|
.ToArray();
|
|
|
|
return new ReleaseDashboardSnapshot(
|
|
new PipelineData(pipelineEnvironments, pipelineConnections),
|
|
pendingApprovals,
|
|
activeDeployments,
|
|
recentReleases);
|
|
}
|
|
|
|
private static DateTimeOffset ParseTimestamp(string value)
|
|
{
|
|
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed))
|
|
{
|
|
return parsed;
|
|
}
|
|
|
|
return DateTimeOffset.MinValue;
|
|
}
|
|
|
|
private static string NormalizeEnvironment(string? value)
|
|
{
|
|
var normalized = value?.Trim().ToLowerInvariant() ?? string.Empty;
|
|
return normalized switch
|
|
{
|
|
"dev" => "development",
|
|
"stage" => "staging",
|
|
"prod" => "production",
|
|
_ => normalized,
|
|
};
|
|
}
|
|
|
|
private static string ToDisplayEnvironment(string? value)
|
|
{
|
|
return NormalizeEnvironment(value) switch
|
|
{
|
|
"development" => "Development",
|
|
"staging" => "Staging",
|
|
"uat" => "UAT",
|
|
"production" => "Production",
|
|
var other when string.IsNullOrWhiteSpace(other) => "Unknown",
|
|
var other => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(other),
|
|
};
|
|
}
|
|
|
|
private static string NormalizeReleaseStatus(string value)
|
|
{
|
|
var normalized = value.Trim().ToLowerInvariant();
|
|
if (string.Equals(normalized, "deploying", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "promoting";
|
|
}
|
|
|
|
return AllowedReleaseStatuses.Contains(normalized) ? normalized : "draft";
|
|
}
|
|
|
|
private static string NormalizeUrgency(string value)
|
|
{
|
|
var normalized = value.Trim().ToLowerInvariant();
|
|
return normalized switch
|
|
{
|
|
"low" or "normal" or "high" or "critical" => normalized,
|
|
_ => "normal",
|
|
};
|
|
}
|
|
|
|
private sealed record PipelineDefinition(string Id, string NormalizedName, string DisplayName, int Order);
|
|
}
|
|
|
|
public sealed record ReleaseDashboardSnapshot(
|
|
PipelineData PipelineData,
|
|
IReadOnlyList<PendingApprovalItem> PendingApprovals,
|
|
IReadOnlyList<ActiveDeploymentItem> ActiveDeployments,
|
|
IReadOnlyList<RecentReleaseItem> RecentReleases);
|
|
|
|
public sealed record PipelineData(
|
|
IReadOnlyList<PipelineEnvironmentItem> Environments,
|
|
IReadOnlyList<PipelineConnectionItem> Connections);
|
|
|
|
public sealed record PipelineEnvironmentItem(
|
|
string Id,
|
|
string Name,
|
|
string DisplayName,
|
|
int Order,
|
|
int ReleaseCount,
|
|
int PendingCount,
|
|
string HealthStatus);
|
|
|
|
public sealed record PipelineConnectionItem(string From, string To);
|
|
|
|
public sealed record PendingApprovalItem(
|
|
string Id,
|
|
string ReleaseId,
|
|
string ReleaseName,
|
|
string ReleaseVersion,
|
|
string SourceEnvironment,
|
|
string TargetEnvironment,
|
|
string RequestedBy,
|
|
string RequestedAt,
|
|
string Urgency);
|
|
|
|
public sealed record ActiveDeploymentItem(
|
|
string Id,
|
|
string ReleaseId,
|
|
string ReleaseName,
|
|
string ReleaseVersion,
|
|
string Environment,
|
|
int Progress,
|
|
string Status,
|
|
string StartedAt,
|
|
int CompletedTargets,
|
|
int TotalTargets);
|
|
|
|
public sealed record RecentReleaseItem(
|
|
string Id,
|
|
string Name,
|
|
string Version,
|
|
string Status,
|
|
string? CurrentEnvironment,
|
|
string CreatedAt,
|
|
string CreatedBy,
|
|
int ComponentCount);
|