up the blokcing tasks
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-11 02:32:18 +02:00
parent 92bc4d3a07
commit 49922dff5a
474 changed files with 76071 additions and 12411 deletions

View File

@@ -1,18 +0,0 @@
using System.Text.Json.Nodes;
using StellaOps.Notify.Models;
namespace StellaOps.Notifier.Worker.Processing;
/// <summary>
/// Renders notification templates with event payload data.
/// </summary>
public interface INotifyTemplateRenderer
{
/// <summary>
/// Renders a template body using the provided data context.
/// </summary>
/// <param name="template">The template containing the body pattern.</param>
/// <param name="payload">The event payload data to interpolate.</param>
/// <returns>The rendered string.</returns>
string Render(NotifyTemplate template, JsonNode? payload);
}

View File

@@ -1,60 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace StellaOps.Notifier.Worker.Processing;
internal sealed class MongoInitializationHostedService : IHostedService
{
private const string InitializerTypeName = "StellaOps.Notify.Storage.Mongo.Internal.NotifyMongoInitializer, StellaOps.Notify.Storage.Mongo";
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<MongoInitializationHostedService> _logger;
public MongoInitializationHostedService(IServiceProvider serviceProvider, ILogger<MongoInitializationHostedService> logger)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var initializerType = Type.GetType(InitializerTypeName, throwOnError: false, ignoreCase: false);
if (initializerType is null)
{
_logger.LogWarning("Notify Mongo initializer type {TypeName} was not found; skipping migration run.", InitializerTypeName);
return;
}
using var scope = _serviceProvider.CreateScope();
var initializer = scope.ServiceProvider.GetService(initializerType);
if (initializer is null)
{
_logger.LogWarning("Notify Mongo initializer could not be resolved from the service provider.");
return;
}
var method = initializerType.GetMethod("EnsureIndexesAsync");
if (method is null)
{
_logger.LogWarning("Notify Mongo initializer does not expose EnsureIndexesAsync; skipping migration run.");
return;
}
try
{
var task = method.Invoke(initializer, new object?[] { cancellationToken }) as Task;
if (task is not null)
{
await task.ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to run Notify Mongo migrations.");
throw;
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -1,11 +1,12 @@
using System.Collections.Immutable;
using System.Collections.Immutable;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Notify.Models;
using StellaOps.Notify.Storage.Mongo.Repositories;
using StellaOps.Notifier.Worker.Storage;
using StellaOps.Notifier.Worker.Channels;
using StellaOps.Notifier.Worker.Dispatch;
using StellaOps.Notifier.Worker.Options;
namespace StellaOps.Notifier.Worker.Processing;
@@ -50,8 +51,8 @@ public sealed class NotifierDispatchWorker : BackgroundService
{
_logger.LogInformation("Notifier dispatch worker {WorkerId} started.", _workerId);
var pollInterval = _options.DispatchPollInterval > TimeSpan.Zero
? _options.DispatchPollInterval
var pollInterval = _options.DispatchInterval > TimeSpan.Zero
? _options.DispatchInterval
: TimeSpan.FromSeconds(5);
while (!stoppingToken.IsCancellationRequested)
@@ -149,29 +150,21 @@ public sealed class NotifierDispatchWorker : BackgroundService
NotifyDeliveryRendered rendered;
if (template is not null)
{
// Create a payload from the delivery kind and metadata
var payload = BuildPayloadFromDelivery(delivery);
var renderedBody = _templateRenderer.Render(template, payload);
var notifyEvent = BuildEventFromDelivery(delivery);
var renderedContent = await _templateRenderer
.RenderAsync(template, notifyEvent, cancellationToken)
.ConfigureAwait(false);
var subject = template.Metadata.TryGetValue("subject", out var subj)
? _templateRenderer.Render(
NotifyTemplate.Create(
templateId: "subject-inline",
tenantId: tenantId,
channelType: template.ChannelType,
key: "subject",
locale: locale,
body: subj),
payload)
: $"Notification: {delivery.Kind}";
var subject = renderedContent.Subject ?? $"Notification: {delivery.Kind}";
rendered = NotifyDeliveryRendered.Create(
channelType: channel.Type,
format: template.Format,
format: renderedContent.Format,
target: channel.Config?.Target ?? string.Empty,
title: subject,
body: renderedBody,
locale: locale);
body: renderedContent.Body,
locale: locale,
bodyHash: renderedContent.BodyHash);
}
else
{
@@ -199,12 +192,16 @@ public sealed class NotifierDispatchWorker : BackgroundService
var attempt = new NotifyDeliveryAttempt(
timestamp: _timeProvider.GetUtcNow(),
status: dispatchResult.Success ? NotifyDeliveryAttemptStatus.Succeeded : NotifyDeliveryAttemptStatus.Failed,
statusCode: dispatchResult.StatusCode,
reason: dispatchResult.Reason);
statusCode: dispatchResult.HttpStatusCode,
reason: dispatchResult.Message);
var shouldRetry = !dispatchResult.Success && (dispatchResult.Status == ChannelDispatchStatus.Throttled
|| dispatchResult.Status == ChannelDispatchStatus.Timeout
|| dispatchResult.Status == ChannelDispatchStatus.NetworkError);
var newStatus = dispatchResult.Success
? NotifyDeliveryStatus.Sent
: (dispatchResult.ShouldRetry ? NotifyDeliveryStatus.Pending : NotifyDeliveryStatus.Failed);
? NotifyDeliveryStatus.Delivered
: (shouldRetry ? NotifyDeliveryStatus.Pending : NotifyDeliveryStatus.Failed);
var updatedDelivery = NotifyDelivery.Create(
deliveryId: delivery.DeliveryId,
@@ -214,13 +211,13 @@ public sealed class NotifierDispatchWorker : BackgroundService
eventId: delivery.EventId,
kind: delivery.Kind,
status: newStatus,
statusReason: dispatchResult.Reason,
statusReason: dispatchResult.Message,
rendered: rendered,
attempts: delivery.Attempts.Add(attempt),
metadata: delivery.Metadata,
createdAt: delivery.CreatedAt,
sentAt: dispatchResult.Success ? _timeProvider.GetUtcNow() : delivery.SentAt,
completedAt: newStatus == NotifyDeliveryStatus.Sent || newStatus == NotifyDeliveryStatus.Failed
completedAt: newStatus == NotifyDeliveryStatus.Delivered || newStatus == NotifyDeliveryStatus.Failed
? _timeProvider.GetUtcNow()
: null);
@@ -257,7 +254,7 @@ public sealed class NotifierDispatchWorker : BackgroundService
_logger.LogWarning("Delivery {DeliveryId} marked failed: {Reason}", delivery.DeliveryId, reason);
}
private static JsonObject BuildPayloadFromDelivery(NotifyDelivery delivery)
private static NotifyEvent BuildEventFromDelivery(NotifyDelivery delivery)
{
var payload = new JsonObject
{
@@ -272,7 +269,18 @@ public sealed class NotifierDispatchWorker : BackgroundService
payload[key] = value;
}
return payload;
delivery.Metadata.TryGetValue("version", out var version);
delivery.Metadata.TryGetValue("actor", out var actor);
return NotifyEvent.Create(
eventId: delivery.EventId,
kind: delivery.Kind,
tenant: delivery.TenantId,
ts: delivery.CreatedAt,
payload: payload,
version: version,
actor: actor,
attributes: delivery.Metadata);
}
private static IReadOnlyDictionary<NotifyChannelType, INotifyChannelAdapter> BuildAdapterMap(

View File

@@ -1,10 +1,10 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.AirGap.Policy;
using StellaOps.Notify.Engine;
using StellaOps.Notify.Models;
using StellaOps.Notify.Storage.Mongo.Repositories;
using StellaOps.Notifier.Worker.Storage;
using StellaOps.Notifier.Worker.Options;
namespace StellaOps.Notifier.Worker.Processing;
@@ -331,3 +331,4 @@ internal sealed class NotifierEventProcessor
return metadata;
}
}

View File

@@ -1,100 +0,0 @@
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using StellaOps.Notify.Models;
namespace StellaOps.Notifier.Worker.Processing;
/// <summary>
/// Simple Handlebars-like template renderer supporting {{property}} and {{#each}} blocks.
/// </summary>
public sealed partial class SimpleTemplateRenderer : INotifyTemplateRenderer
{
private static readonly Regex PlaceholderPattern = PlaceholderRegex();
private static readonly Regex EachBlockPattern = EachBlockRegex();
public string Render(NotifyTemplate template, JsonNode? payload)
{
ArgumentNullException.ThrowIfNull(template);
var body = template.Body;
if (string.IsNullOrWhiteSpace(body))
{
return string.Empty;
}
// Process {{#each}} blocks first
body = ProcessEachBlocks(body, payload);
// Then substitute simple placeholders
body = SubstitutePlaceholders(body, payload);
return body;
}
private static string ProcessEachBlocks(string body, JsonNode? payload)
{
return EachBlockPattern.Replace(body, match =>
{
var collectionPath = match.Groups[1].Value.Trim();
var innerTemplate = match.Groups[2].Value;
var collection = ResolvePath(payload, collectionPath);
if (collection is not JsonObject obj)
{
return string.Empty;
}
var results = new List<string>();
foreach (var (key, value) in obj)
{
var itemResult = innerTemplate
.Replace("{{@key}}", key)
.Replace("{{this}}", value?.ToString() ?? string.Empty);
results.Add(itemResult);
}
return string.Join(string.Empty, results);
});
}
private static string SubstitutePlaceholders(string body, JsonNode? payload)
{
return PlaceholderPattern.Replace(body, match =>
{
var path = match.Groups[1].Value.Trim();
var resolved = ResolvePath(payload, path);
return resolved?.ToString() ?? string.Empty;
});
}
private static JsonNode? ResolvePath(JsonNode? root, string path)
{
if (root is null || string.IsNullOrWhiteSpace(path))
{
return null;
}
var segments = path.Split('.');
var current = root;
foreach (var segment in segments)
{
if (current is JsonObject obj && obj.TryGetPropertyValue(segment, out var next))
{
current = next;
}
else
{
return null;
}
}
return current;
}
[GeneratedRegex(@"\{\{([^#/}]+)\}\}", RegexOptions.Compiled)]
private static partial Regex PlaceholderRegex();
[GeneratedRegex(@"\{\{#each\s+([^}]+)\}\}(.*?)\{\{/each\}\}", RegexOptions.Compiled | RegexOptions.Singleline)]
private static partial Regex EachBlockRegex();
}