UI work to fill SBOM sourcing management gap. UI planning remaining functionality exposure. Work on CI/Tests stabilization

Introduces CGS determinism test runs to CI workflows for Windows, macOS, Linux, Alpine, and Debian, fulfilling CGS-008 cross-platform requirements. Updates local-ci scripts to support new smoke steps, test timeouts, progress intervals, and project slicing for improved test isolation and diagnostics.
This commit is contained in:
master
2025-12-29 19:12:38 +02:00
parent 41552d26ec
commit a4badc275e
286 changed files with 50918 additions and 992 deletions

View File

@@ -8,6 +8,7 @@
// ---------------------------------------------------------------------
using System.Collections.Immutable;
using System.Globalization;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Notify.Models;
@@ -1066,7 +1067,7 @@ public sealed class NotificationTemplateRenderer
var ifStart = result.IndexOf("{{#if ", StringComparison.Ordinal);
if (ifStart < 0) break;
var condEnd = result.IndexOf("}}", ifStart, StringComparison.Ordinal);
var condEnd = FindConditionEnd(result, ifStart);
if (condEnd < 0) break;
var condition = result.Substring(ifStart + 6, condEnd - ifStart - 6).Trim();
@@ -1104,6 +1105,11 @@ public sealed class NotificationTemplateRenderer
private static bool EvaluateCondition(string condition, TemplateContext context)
{
if (TryEvaluateComparison(condition, context, out var comparisonResult))
{
return comparisonResult;
}
if (context.Variables.TryGetValue(condition, out var value))
{
return value switch
@@ -1118,6 +1124,184 @@ public sealed class NotificationTemplateRenderer
return false;
}
private static int FindConditionEnd(string template, int ifStart)
{
var index = ifStart + 6;
var depth = 0;
while (index < template.Length - 1)
{
if (template[index] == '{' && template[index + 1] == '{')
{
depth++;
index += 2;
continue;
}
if (template[index] == '}' && template[index + 1] == '}')
{
if (depth == 0)
{
return index;
}
depth--;
index += 2;
continue;
}
index++;
}
return -1;
}
private static bool TryEvaluateComparison(string condition, TemplateContext context, out bool result)
{
result = false;
var normalized = NormalizeCondition(condition);
if (string.IsNullOrWhiteSpace(normalized))
{
return false;
}
if (!TrySplitComparison(normalized, out var leftToken, out var op, out var rightToken))
{
return false;
}
var leftValue = ResolveOperand(leftToken, context);
var rightValue = ResolveOperand(rightToken, context);
if (TryCompareNumbers(leftValue, rightValue, op, out result))
{
return true;
}
if (op is "eq" or "neq")
{
var leftText = leftValue?.ToString() ?? string.Empty;
var rightText = rightValue?.ToString() ?? string.Empty;
result = op == "eq"
? string.Equals(leftText, rightText, StringComparison.Ordinal)
: !string.Equals(leftText, rightText, StringComparison.Ordinal);
return true;
}
return false;
}
private static string NormalizeCondition(string condition)
{
var normalized = condition.Trim();
if (normalized.StartsWith("(", StringComparison.Ordinal) && normalized.EndsWith(")", StringComparison.Ordinal))
{
normalized = normalized[1..^1].Trim();
}
return normalized
.Replace("{{", "", StringComparison.Ordinal)
.Replace("}}", "", StringComparison.Ordinal)
.Trim();
}
private static bool TrySplitComparison(string condition, out string left, out string op, out string right)
{
left = string.Empty;
op = string.Empty;
right = string.Empty;
var parts = condition.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 3)
{
return false;
}
left = parts[0];
op = parts[1];
right = parts[2];
return op is "gt" or "gte" or "lt" or "lte" or "eq" or "neq";
}
private static object? ResolveOperand(string token, TemplateContext context)
{
var trimmed = token.Trim();
if (trimmed.Length >= 2 && ((trimmed.StartsWith("\"", StringComparison.Ordinal) && trimmed.EndsWith("\"", StringComparison.Ordinal))
|| (trimmed.StartsWith("'", StringComparison.Ordinal) && trimmed.EndsWith("'", StringComparison.Ordinal))))
{
return trimmed[1..^1];
}
if (bool.TryParse(trimmed, out var boolValue))
{
return boolValue;
}
if (decimal.TryParse(trimmed, NumberStyles.Number, CultureInfo.InvariantCulture, out var decimalValue))
{
return decimalValue;
}
return ResolvePath(trimmed, context.Variables);
}
private static bool TryCompareNumbers(object? left, object? right, string op, out bool result)
{
result = false;
if (!TryConvertToDecimal(left, out var leftNumber) || !TryConvertToDecimal(right, out var rightNumber))
{
return false;
}
result = op switch
{
"gt" => leftNumber > rightNumber,
"gte" => leftNumber >= rightNumber,
"lt" => leftNumber < rightNumber,
"lte" => leftNumber <= rightNumber,
"eq" => leftNumber == rightNumber,
"neq" => leftNumber != rightNumber,
_ => false
};
return true;
}
private static bool TryConvertToDecimal(object? value, out decimal result)
{
result = 0m;
if (value is null)
{
return false;
}
if (value is decimal decimalValue)
{
result = decimalValue;
return true;
}
if (value is IConvertible convertible)
{
try
{
result = convertible.ToDecimal(CultureInfo.InvariantCulture);
return true;
}
catch (FormatException)
{
return false;
}
catch (InvalidCastException)
{
return false;
}
}
return false;
}
private string ProcessLoops(string template, TemplateContext context)
{
var result = template;