Files
git.stella-ops.org/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Services/ReleaseDashboardSnapshotBuilder.cs
2026-03-31 23:53:45 +03:00

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);