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
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:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user