feat: Implement approvals workflow and notifications integration
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added approvals orchestration with persistence and workflow scaffolding. - Integrated notifications insights and staged resume hooks. - Introduced approval coordinator and policy notification bridge with unit tests. - Added approval decision API with resume requeue and persisted plan snapshots. - Documented the Excitor consensus API beta and provided JSON sample payload. - Created analyzers to flag usage of deprecated merge service APIs. - Implemented logging for artifact uploads and approval decision service. - Added tests for PackRunApprovalDecisionService and related components.
This commit is contained in:
@@ -52,9 +52,11 @@ internal static class JobRegistrationExtensions
|
||||
new("source:vndr-oracle:parse", "StellaOps.Concelier.Connector.Vndr.Oracle.OracleParseJob", "StellaOps.Concelier.Connector.Vndr.Oracle", TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(5)),
|
||||
new("source:vndr-oracle:map", "StellaOps.Concelier.Connector.Vndr.Oracle.OracleMapJob", "StellaOps.Concelier.Connector.Vndr.Oracle", TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(5)),
|
||||
|
||||
new("export:json", "StellaOps.Concelier.Exporter.Json.JsonExportJob", "StellaOps.Concelier.Exporter.Json", TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(5)),
|
||||
new("export:json", "StellaOps.Concelier.Exporter.Json.JsonExportJob", "StellaOps.Concelier.Exporter.Json", TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(5)),
|
||||
new("export:trivy-db", "StellaOps.Concelier.Exporter.TrivyDb.TrivyDbExportJob", "StellaOps.Concelier.Exporter.TrivyDb", TimeSpan.FromMinutes(20), TimeSpan.FromMinutes(10)),
|
||||
#pragma warning disable CS0618, CONCELIER0001 // Legacy merge job remains available until MERGE-LNM-21-002 completes.
|
||||
new("merge:reconcile", "StellaOps.Concelier.Merge.Jobs.MergeReconcileJob", "StellaOps.Concelier.Merge", TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(5))
|
||||
#pragma warning restore CS0618, CONCELIER0001
|
||||
};
|
||||
|
||||
public static IServiceCollection AddBuiltInConcelierJobs(this IServiceCollection services)
|
||||
|
||||
@@ -15,6 +15,8 @@ public sealed class ConcelierOptions
|
||||
public AuthorityOptions Authority { get; set; } = new();
|
||||
|
||||
public MirrorOptions Mirror { get; set; } = new();
|
||||
|
||||
public FeaturesOptions Features { get; set; } = new();
|
||||
|
||||
public sealed class StorageOptions
|
||||
{
|
||||
@@ -135,4 +137,13 @@ public sealed class ConcelierOptions
|
||||
|
||||
public int MaxDownloadRequestsPerHour { get; set; } = 1200;
|
||||
}
|
||||
|
||||
public sealed class FeaturesOptions
|
||||
{
|
||||
public bool NoMergeEnabled { get; set; }
|
||||
|
||||
public bool LnmShadowWrites { get; set; } = true;
|
||||
|
||||
public IList<string> MergeJobAllowlist { get; } = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ public static class ConcelierOptionsPostConfigure
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
options.Authority ??= new ConcelierOptions.AuthorityOptions();
|
||||
options.Authority ??= new ConcelierOptions.AuthorityOptions();
|
||||
options.Features ??= new ConcelierOptions.FeaturesOptions();
|
||||
|
||||
var authority = options.Authority;
|
||||
if (string.IsNullOrWhiteSpace(authority.ClientSecret)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
@@ -98,9 +99,36 @@ builder.Services.AddConcelierLinksetMappers();
|
||||
builder.Services.AddAdvisoryRawServices();
|
||||
builder.Services.AddSingleton<IAdvisoryObservationQueryService, AdvisoryObservationQueryService>();
|
||||
|
||||
builder.Services.AddMergeModule(builder.Configuration);
|
||||
var features = concelierOptions.Features ?? new ConcelierOptions.FeaturesOptions();
|
||||
|
||||
if (!features.NoMergeEnabled)
|
||||
{
|
||||
#pragma warning disable CS0618, CONCELIER0001, CONCELIER0002 // Legacy merge service is intentionally supported behind a feature toggle.
|
||||
builder.Services.AddMergeModule(builder.Configuration);
|
||||
#pragma warning restore CS0618, CONCELIER0001, CONCELIER0002
|
||||
}
|
||||
|
||||
builder.Services.AddJobScheduler();
|
||||
builder.Services.AddBuiltInConcelierJobs();
|
||||
builder.Services.PostConfigure<JobSchedulerOptions>(options =>
|
||||
{
|
||||
if (features.NoMergeEnabled)
|
||||
{
|
||||
options.Definitions.Remove("merge:reconcile");
|
||||
return;
|
||||
}
|
||||
|
||||
if (features.MergeJobAllowlist is { Count: > 0 })
|
||||
{
|
||||
var allowMergeJob = features.MergeJobAllowlist.Any(value =>
|
||||
string.Equals(value, "merge:reconcile", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!allowMergeJob)
|
||||
{
|
||||
options.Definitions.Remove("merge:reconcile");
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.Services.AddSingleton<OpenApiDiscoveryDocumentProvider>();
|
||||
|
||||
builder.Services.AddSingleton<ServiceStatus>(sp => new ServiceStatus(sp.GetRequiredService<TimeProvider>()));
|
||||
@@ -183,7 +211,7 @@ if (authorityConfigured)
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddStellaOpsScopePolicy(JobsPolicyName, concelierOptions.Authority.RequiredScopes.ToArray());
|
||||
options.AddStellaOpsScopePolicy(ObservationsPolicyName, StellaOpsScopes.VulnRead);
|
||||
options.AddStellaOpsScopePolicy(ObservationsPolicyName, StellaOpsScopes.VulnView);
|
||||
options.AddStellaOpsScopePolicy(AdvisoryIngestPolicyName, StellaOpsScopes.AdvisoryIngest);
|
||||
options.AddStellaOpsScopePolicy(AdvisoryReadPolicyName, StellaOpsScopes.AdvisoryRead);
|
||||
options.AddStellaOpsScopePolicy(AocVerifyPolicyName, StellaOpsScopes.AdvisoryRead, StellaOpsScopes.AocVerify);
|
||||
@@ -197,6 +225,11 @@ builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (features.NoMergeEnabled)
|
||||
{
|
||||
app.Logger.LogWarning("Legacy merge module disabled via concelier:features:noMergeEnabled; Link-Not-Merge mode active.");
|
||||
}
|
||||
|
||||
var resolvedConcelierOptions = app.Services.GetRequiredService<IOptions<ConcelierOptions>>().Value;
|
||||
var resolvedAuthority = resolvedConcelierOptions.Authority ?? new ConcelierOptions.AuthorityOptions();
|
||||
authorityConfigured = resolvedAuthority.Enabled;
|
||||
|
||||
@@ -35,5 +35,8 @@
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
||||
<ProjectReference Include="../../Aoc/__Libraries/StellaOps.Aoc/StellaOps.Aoc.csproj" />
|
||||
<ProjectReference Include="../../Aoc/__Libraries/StellaOps.Aoc.AspNetCore/StellaOps.Aoc.AspNetCore.csproj" />
|
||||
<ProjectReference Include="../__Analyzers/StellaOps.Concelier.Analyzers/StellaOps.Concelier.Analyzers.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
; Shipped analyzer releases
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
## Release History
|
||||
|
||||
### Unreleased
|
||||
|
||||
#### New Rules
|
||||
|
||||
Rule ID | Title | Notes
|
||||
--------|-------|------
|
||||
CONCELIER0002 | Legacy merge pipeline is disabled | Flags usage of `AddMergeModule` and `AdvisoryMergeService`.
|
||||
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
|
||||
namespace StellaOps.Concelier.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzer that flags usages of the legacy merge service APIs.
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class NoMergeUsageAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Diagnostic identifier for legacy merge usage violations.
|
||||
/// </summary>
|
||||
public const string DiagnosticId = "CONCELIER0002";
|
||||
|
||||
private const string Category = "Usage";
|
||||
private const string MergeExtensionType = "StellaOps.Concelier.Merge.MergeServiceCollectionExtensions";
|
||||
private const string MergeServiceType = "StellaOps.Concelier.Merge.Services.AdvisoryMergeService";
|
||||
|
||||
private static readonly LocalizableString Title = "Legacy merge pipeline is disabled";
|
||||
private static readonly LocalizableString MessageFormat = "Do not reference the legacy Concelier merge pipeline (type '{0}')";
|
||||
private static readonly LocalizableString Description =
|
||||
"The legacy Concelier merge service is deprecated under MERGE-LNM-21-002. "
|
||||
+ "Switch to observation/linkset APIs or guard calls behind the concelier:features:noMergeEnabled toggle.";
|
||||
|
||||
private static readonly DiagnosticDescriptor Rule = new(
|
||||
DiagnosticId,
|
||||
Title,
|
||||
MessageFormat,
|
||||
Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: Description,
|
||||
helpLinkUri: "https://stella-ops.org/docs/migration/no-merge");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
|
||||
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
|
||||
}
|
||||
|
||||
private static void AnalyzeInvocation(OperationAnalysisContext context)
|
||||
{
|
||||
if (context.Operation is not IInvocationOperation invocation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targetMethod = invocation.TargetMethod;
|
||||
if (targetMethod is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SymbolEquals(targetMethod.ContainingType, MergeExtensionType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(targetMethod.Name, "AddMergeModule", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsAllowedAssembly(context.ContainingSymbol.ContainingAssembly))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReportDiagnostic(context, invocation.Syntax, $"{MergeExtensionType}.{targetMethod.Name}");
|
||||
}
|
||||
|
||||
private static void AnalyzeObjectCreation(OperationAnalysisContext context)
|
||||
{
|
||||
if (context.Operation is not IObjectCreationOperation creation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var createdType = creation.Type;
|
||||
if (createdType is null || !SymbolEquals(createdType, MergeServiceType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsAllowedAssembly(context.ContainingSymbol.ContainingAssembly))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReportDiagnostic(context, creation.Syntax, MergeServiceType);
|
||||
}
|
||||
|
||||
private static bool SymbolEquals(ITypeSymbol? symbol, string fullName)
|
||||
{
|
||||
if (symbol is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var display = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
if (display.StartsWith("global::", StringComparison.Ordinal))
|
||||
{
|
||||
display = display.Substring("global::".Length);
|
||||
}
|
||||
|
||||
return string.Equals(display, fullName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsAllowedAssembly(IAssemblySymbol? assemblySymbol)
|
||||
{
|
||||
if (assemblySymbol is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var assemblyName = assemblySymbol.Name;
|
||||
if (string.IsNullOrWhiteSpace(assemblyName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (assemblyName.StartsWith("StellaOps.Concelier.Merge", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (assemblyName.EndsWith(".Analyzers", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void ReportDiagnostic(OperationAnalysisContext context, SyntaxNode syntax, string targetName)
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(Rule, syntax.GetLocation(), targetName);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<AssemblyName>StellaOps.Concelier.Analyzers</AssemblyName>
|
||||
<RootNamespace>StellaOps.Concelier.Analyzers</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -114,10 +114,10 @@ internal sealed class AdvisoryObservationFactory : IAdvisoryObservationFactory
|
||||
|
||||
private static AdvisoryObservationLinkset CreateLinkset(RawIdentifiers identifiers, RawLinkset linkset)
|
||||
{
|
||||
var aliases = NormalizeAliases(identifiers, linkset);
|
||||
var purls = NormalizePackageUrls(linkset.PackageUrls);
|
||||
var cpes = NormalizeCpes(linkset.Cpes);
|
||||
var references = NormalizeReferences(linkset.References);
|
||||
var aliases = CollectAliases(identifiers, linkset);
|
||||
var purls = CollectValues(linkset.PackageUrls);
|
||||
var cpes = CollectValues(linkset.Cpes);
|
||||
var references = CollectReferences(linkset.References);
|
||||
|
||||
return new AdvisoryObservationLinkset(aliases, purls, cpes, references);
|
||||
}
|
||||
@@ -170,124 +170,91 @@ internal sealed class AdvisoryObservationFactory : IAdvisoryObservationFactory
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<string> NormalizeAliases(RawIdentifiers identifiers, RawLinkset linkset)
|
||||
{
|
||||
var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (LinksetNormalization.TryNormalizeAlias(identifiers.PrimaryId, out var primary))
|
||||
{
|
||||
aliases.Add(primary);
|
||||
}
|
||||
|
||||
foreach (var alias in identifiers.Aliases)
|
||||
{
|
||||
if (LinksetNormalization.TryNormalizeAlias(alias, out var normalized))
|
||||
{
|
||||
aliases.Add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var alias in linkset.Aliases)
|
||||
{
|
||||
if (LinksetNormalization.TryNormalizeAlias(alias, out var normalized))
|
||||
{
|
||||
aliases.Add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var note in linkset.Notes)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(note.Value)
|
||||
&& LinksetNormalization.TryNormalizeAlias(note.Value, out var normalized))
|
||||
{
|
||||
aliases.Add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
return aliases
|
||||
.OrderBy(static value => value, StringComparer.OrdinalIgnoreCase)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> NormalizePackageUrls(ImmutableArray<string> packageUrls)
|
||||
{
|
||||
if (packageUrls.IsDefaultOrEmpty)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var candidate in packageUrls)
|
||||
{
|
||||
if (!LinksetNormalization.TryNormalizePackageUrl(candidate, out var normalized) || string.IsNullOrEmpty(normalized))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
set.Add(normalized);
|
||||
}
|
||||
|
||||
return set
|
||||
.OrderBy(static value => value, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> NormalizeCpes(ImmutableArray<string> cpes)
|
||||
{
|
||||
if (cpes.IsDefaultOrEmpty)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var cpe in cpes)
|
||||
{
|
||||
if (!LinksetNormalization.TryNormalizeCpe(cpe, out var normalized) || string.IsNullOrEmpty(normalized))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
set.Add(normalized);
|
||||
}
|
||||
|
||||
return set
|
||||
.OrderBy(static value => value, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<AdvisoryObservationReference> NormalizeReferences(ImmutableArray<RawReference> references)
|
||||
{
|
||||
if (references.IsDefaultOrEmpty)
|
||||
{
|
||||
return ImmutableArray<AdvisoryObservationReference>.Empty;
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var list = new List<AdvisoryObservationReference>();
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
var normalized = LinksetNormalization.TryCreateReference(reference.Type, reference.Url);
|
||||
if (normalized is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!seen.Add(normalized.Url))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list.Add(normalized);
|
||||
}
|
||||
|
||||
return list
|
||||
.OrderBy(static reference => reference.Type, StringComparer.Ordinal)
|
||||
.ThenBy(static reference => reference.Url, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
private static IEnumerable<string> CollectAliases(RawIdentifiers identifiers, RawLinkset linkset)
|
||||
{
|
||||
var results = new List<string>();
|
||||
|
||||
AddAlias(results, identifiers.PrimaryId);
|
||||
AddRange(results, identifiers.Aliases);
|
||||
AddRange(results, linkset.Aliases);
|
||||
|
||||
foreach (var note in linkset.Notes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(note.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
results.Add(note.Value.Trim());
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
static void AddAlias(ICollection<string> target, string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
target.Add(value.Trim());
|
||||
}
|
||||
|
||||
static void AddRange(ICollection<string> target, ImmutableArray<string> values)
|
||||
{
|
||||
if (values.IsDefaultOrEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
AddAlias(target, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> CollectValues(ImmutableArray<string> values)
|
||||
{
|
||||
if (values.IsDefaultOrEmpty)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var list = new List<string>(values.Length);
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list.Add(value.Trim());
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static IEnumerable<AdvisoryObservationReference> CollectReferences(ImmutableArray<RawReference> references)
|
||||
{
|
||||
if (references.IsDefaultOrEmpty)
|
||||
{
|
||||
return ImmutableArray<AdvisoryObservationReference>.Empty;
|
||||
}
|
||||
|
||||
var list = new List<AdvisoryObservationReference>(references.Length);
|
||||
foreach (var reference in references)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reference.Type) || string.IsNullOrWhiteSpace(reference.Url))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list.Add(new AdvisoryObservationReference(reference.Type.Trim(), reference.Url.Trim()));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<string, string> CreateAttributes(AdvisoryRawDocument rawDocument)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
> Docs alignment (2025-10-26): Linkset expectations detailed in AOC reference §4 and policy-engine architecture §2.1.
|
||||
> 2025-10-28: Advisory raw ingestion now strips client-supplied supersedes hints, logs ignored pointers, and surfaces repository-supplied supersedes identifiers; service tests cover duplicate handling and append-only semantics.
|
||||
> Docs alignment (2025-10-26): Deployment guide + observability guide describe supersedes metrics; ensure implementation emits `aoc_violation_total` on failure.
|
||||
| CONCELIER-CORE-AOC-19-004 `Remove ingestion normalization` | DOING (2025-10-28) | Concelier Core Guild | CONCELIER-CORE-AOC-19-002, POLICY-AOC-19-003 | Strip normalization/dedup/severity logic from ingestion pipelines, delegate derived computations to Policy Engine, and update exporters/tests to consume raw documents only.<br>2025-10-29 19:05Z: Audit completed for `AdvisoryRawService`/Mongo repo to confirm alias order/dedup removal persists; identified remaining normalization in observation/linkset factory that will be revised to surface raw duplicates for Policy ingestion. Change sketch + regression matrix drafted under `docs/dev/aoc-normalization-removal-notes.md` (pending commit).<br>2025-10-31 20:45Z: Added raw linkset projection to observations/storage, exposing canonical+raw views, refreshed fixtures/tests, and documented behaviour in models/doc factory.<br>2025-10-31 21:10Z: Coordinated with Policy Engine (POLICY-ENGINE-20-003) on adoption timeline; backfill + consumer readiness tracked in `docs/dev/raw-linkset-backfill-plan.md`. |
|
||||
| CONCELIER-CORE-AOC-19-004 `Remove ingestion normalization` | DOING (2025-10-28) | Concelier Core Guild | CONCELIER-CORE-AOC-19-002, POLICY-AOC-19-003 | Strip normalization/dedup/severity logic from ingestion pipelines, delegate derived computations to Policy Engine, and update exporters/tests to consume raw documents only.<br>2025-10-29 19:05Z: Audit completed for `AdvisoryRawService`/Mongo repo to confirm alias order/dedup removal persists; identified remaining normalization in observation/linkset factory that will be revised to surface raw duplicates for Policy ingestion. Change sketch + regression matrix drafted under `docs/dev/aoc-normalization-removal-notes.md` (pending commit).<br>2025-10-31 20:45Z: Added raw linkset projection to observations/storage, exposing canonical+raw views, refreshed fixtures/tests, and documented behaviour in models/doc factory.<br>2025-10-31 21:10Z: Coordinated with Policy Engine (POLICY-ENGINE-20-003) on adoption timeline; backfill + consumer readiness tracked in `docs/dev/raw-linkset-backfill-plan.md`.<br>2025-11-05 14:25Z: Resuming to document merge-dependent normalization paths and prepare implementation notes for `noMergeEnabled` gating before code changes land.<br>2025-11-05 19:20Z: Observation factory/linkset now preserve upstream ordering + duplicates; canonicalisation responsibility shifts to downstream consumers with refreshed unit coverage.<br>2025-11-06 16:10Z: Updated AOC reference/backfill docs with raw vs canonical guidance and cross-linked analyzer guardrails. |
|
||||
> Docs alignment (2025-10-26): Architecture overview emphasises policy-only derivation; coordinate with Policy Engine guild for rollout.
|
||||
> 2025-10-29: `AdvisoryRawService` now preserves upstream alias/linkset ordering (trim-only) and updated AOC documentation reflects the behaviour; follow-up to ensure policy consumers handle duplicates remains open.
|
||||
| CONCELIER-CORE-AOC-19-013 `Authority tenant scope smoke coverage` | TODO | Concelier Core Guild | AUTH-AOC-19-002 | Extend Concelier smoke/e2e fixtures to configure `requiredTenants` and assert cross-tenant rejection with updated Authority tokens. | Coordinate deliverable so Authority docs (`AUTH-AOC-19-003`) can close once tests are in place. |
|
||||
|
||||
@@ -5,9 +5,10 @@ using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Jobs;
|
||||
|
||||
public sealed class MergeReconcileJob : IJob
|
||||
namespace StellaOps.Concelier.Merge.Jobs;
|
||||
|
||||
[Obsolete("MergeReconcileJob is deprecated; Link-Not-Merge supersedes merge scheduling. Disable via concelier:features:noMergeEnabled. Tracking MERGE-LNM-21-002.", DiagnosticId = "CONCELIER0001", UrlFormat = "https://stella-ops.org/docs/migration/no-merge")]
|
||||
public sealed class MergeReconcileJob : IJob
|
||||
{
|
||||
private readonly AdvisoryMergeService _mergeService;
|
||||
private readonly ILogger<MergeReconcileJob> _logger;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Concelier.Core;
|
||||
using StellaOps.Concelier.Merge.Jobs;
|
||||
using StellaOps.Concelier.Merge.Jobs;
|
||||
using StellaOps.Concelier.Merge.Options;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
|
||||
namespace StellaOps.Concelier.Merge;
|
||||
|
||||
public static class MergeServiceCollectionExtensions
|
||||
namespace StellaOps.Concelier.Merge;
|
||||
|
||||
[Obsolete("Legacy merge module is deprecated; prefer Link-Not-Merge linkset pipelines. Track MERGE-LNM-21-002 and set concelier:features:noMergeEnabled=true to disable registration.", DiagnosticId = "CONCELIER0001", UrlFormat = "https://stella-ops.org/docs/migration/no-merge")]
|
||||
public static class MergeServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddMergeModule(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
@@ -34,10 +36,12 @@ public static class MergeServiceCollectionExtensions
|
||||
return new AdvisoryPrecedenceMerger(resolver, options, timeProvider, logger);
|
||||
});
|
||||
|
||||
services.TryAddSingleton<MergeEventWriter>();
|
||||
services.TryAddSingleton<AdvisoryMergeService>();
|
||||
services.AddTransient<MergeReconcileJob>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
#pragma warning disable CS0618 // Legacy merge services are marked obsolete.
|
||||
services.TryAddSingleton<MergeEventWriter>();
|
||||
services.TryAddSingleton<AdvisoryMergeService>();
|
||||
services.AddTransient<MergeReconcileJob>();
|
||||
#pragma warning restore CS0618
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ using StellaOps.Concelier.Storage.Mongo.Aliases;
|
||||
using StellaOps.Concelier.Storage.Mongo.MergeEvents;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Services;
|
||||
|
||||
public sealed class AdvisoryMergeService
|
||||
namespace StellaOps.Concelier.Merge.Services;
|
||||
|
||||
[Obsolete("AdvisoryMergeService is deprecated. Transition callers to Link-Not-Merge observation/linkset APIs (MERGE-LNM-21-002) and enable concelier:features:noMergeEnabled when ready.", DiagnosticId = "CONCELIER0001", UrlFormat = "https://stella-ops.org/docs/migration/no-merge")]
|
||||
public sealed class AdvisoryMergeService
|
||||
{
|
||||
private static readonly Meter MergeMeter = new("StellaOps.Concelier.Merge");
|
||||
private static readonly Counter<long> AliasCollisionCounter = MergeMeter.CreateCounter<long>(
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
| Task | Owner(s) | Depends on | Notes |
|
||||
|---|---|---|---|
|
||||
|MERGE-LNM-21-001 Migration plan authoring|BE-Merge, Architecture Guild|CONCELIER-LNM-21-101|**DONE (2025-11-03)** – Authored `docs/migration/no-merge.md` with rollout phases, backfill/validation checklists, rollback guidance, and ownership matrix for the Link-Not-Merge cutover.|
|
||||
|MERGE-LNM-21-002 Merge service deprecation|BE-Merge|MERGE-LNM-21-001|**DOING (2025-11-03)** – Auditing service registrations, DI bindings, and tests consuming `AdvisoryMergeService`; drafting deprecation plan and analyzer scope prior to code removal.|
|
||||
|MERGE-LNM-21-002 Merge service deprecation|BE-Merge|MERGE-LNM-21-001|**DOING (2025-11-03)** – Auditing service registrations, DI bindings, and tests consuming `AdvisoryMergeService`; drafting deprecation plan and analyzer scope prior to code removal.<br>2025-11-05 14:42Z: Implementing `concelier:features:noMergeEnabled` gate, merge job allowlist checks, `[Obsolete]` markings, and analyzer scaffolding to steer consumers toward linkset APIs.<br>2025-11-06 16:10Z: Introduced Roslyn analyzer (`CONCELIER0002`) referenced by Concelier WebService + tests, documented suppression guidance, and updated migration playbook.|
|
||||
> 2025-11-03: Catalogued call sites (WebService Program `AddMergeModule`, built-in job registration `merge:reconcile`, `MergeReconcileJob`) and confirmed unit tests are the only direct `MergeAsync` callers; next step is to define analyzer + replacement observability coverage.
|
||||
|MERGE-LNM-21-003 Determinism/test updates|QA Guild, BE-Merge|MERGE-LNM-21-002|Replace merge determinism suites with observation/linkset regression tests verifying no data mutation and conflicts remain visible.|
|
||||
|
||||
@@ -280,57 +280,60 @@ public sealed record AdvisoryObservationLinkset
|
||||
IEnumerable<string>? cpes,
|
||||
IEnumerable<AdvisoryObservationReference>? references)
|
||||
{
|
||||
Aliases = NormalizeStringSet(aliases, toLower: true);
|
||||
Purls = NormalizeStringSet(purls);
|
||||
Cpes = NormalizeStringSet(cpes);
|
||||
References = NormalizeReferences(references);
|
||||
}
|
||||
Aliases = ToImmutableArray(aliases);
|
||||
Purls = ToImmutableArray(purls);
|
||||
Cpes = ToImmutableArray(cpes);
|
||||
References = ToImmutableReferences(references);
|
||||
}
|
||||
|
||||
public ImmutableArray<string> Aliases { get; }
|
||||
|
||||
public ImmutableArray<string> Purls { get; }
|
||||
|
||||
public ImmutableArray<string> Aliases { get; }
|
||||
|
||||
public ImmutableArray<string> Purls { get; }
|
||||
|
||||
public ImmutableArray<string> Cpes { get; }
|
||||
|
||||
public ImmutableArray<AdvisoryObservationReference> References { get; }
|
||||
|
||||
private static ImmutableArray<string> NormalizeStringSet(IEnumerable<string>? values, bool toLower = false)
|
||||
{
|
||||
if (values is null)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var list = new List<string>();
|
||||
foreach (var value in values)
|
||||
{
|
||||
var trimmed = Validation.TrimToNull(value);
|
||||
if (trimmed is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list.Add(toLower ? trimmed.ToLowerInvariant() : trimmed);
|
||||
}
|
||||
|
||||
return list
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(static v => v, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static ImmutableArray<AdvisoryObservationReference> NormalizeReferences(IEnumerable<AdvisoryObservationReference>? references)
|
||||
{
|
||||
if (references is null)
|
||||
{
|
||||
return ImmutableArray<AdvisoryObservationReference>.Empty;
|
||||
}
|
||||
|
||||
return references
|
||||
.Where(static reference => reference is not null)
|
||||
.Distinct()
|
||||
.OrderBy(static reference => reference.Type, StringComparer.Ordinal)
|
||||
.ThenBy(static reference => reference.Url, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
public ImmutableArray<string> Cpes { get; }
|
||||
|
||||
public ImmutableArray<AdvisoryObservationReference> References { get; }
|
||||
|
||||
private static ImmutableArray<string> ToImmutableArray(IEnumerable<string>? values)
|
||||
{
|
||||
if (values is null)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<string>();
|
||||
foreach (var value in values)
|
||||
{
|
||||
var trimmed = Validation.TrimToNull(value);
|
||||
if (trimmed is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Add(trimmed);
|
||||
}
|
||||
|
||||
return builder.Count == 0 ? ImmutableArray<string>.Empty : builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static ImmutableArray<AdvisoryObservationReference> ToImmutableReferences(IEnumerable<AdvisoryObservationReference>? references)
|
||||
{
|
||||
if (references is null)
|
||||
{
|
||||
return ImmutableArray<AdvisoryObservationReference>.Empty;
|
||||
}
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<AdvisoryObservationReference>();
|
||||
foreach (var reference in references)
|
||||
{
|
||||
if (reference is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Add(reference);
|
||||
}
|
||||
|
||||
return builder.Count == 0 ? ImmutableArray<AdvisoryObservationReference>.Empty : builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ public sealed class AdvisoryObservationFactoryTests
|
||||
private static readonly DateTimeOffset SampleTimestamp = DateTimeOffset.Parse("2025-10-26T12:34:56Z");
|
||||
|
||||
[Fact]
|
||||
public void Create_NormalizesLinksetIdentifiersAndReferences()
|
||||
{
|
||||
var factory = new AdvisoryObservationFactory();
|
||||
var rawDocument = BuildRawDocument(
|
||||
identifiers: new RawIdentifiers(
|
||||
public void Create_PreservesLinksetOrderAndDuplicates()
|
||||
{
|
||||
var factory = new AdvisoryObservationFactory();
|
||||
var rawDocument = BuildRawDocument(
|
||||
identifiers: new RawIdentifiers(
|
||||
Aliases: ImmutableArray.Create(" CVE-2025-0001 ", "ghsa-XXXX-YYYY"),
|
||||
PrimaryId: "GHSA-XXXX-YYYY"),
|
||||
linkset: new RawLinkset
|
||||
@@ -29,16 +29,27 @@ public sealed class AdvisoryObservationFactoryTests
|
||||
new RawReference("Advisory", " https://example.test/advisory "),
|
||||
new RawReference("ADVISORY", "https://example.test/advisory"))
|
||||
});
|
||||
|
||||
var observation = factory.Create(rawDocument, SampleTimestamp);
|
||||
|
||||
Assert.Equal(SampleTimestamp, observation.CreatedAt);
|
||||
Assert.Equal(new[] { "cve-2025-0001", "ghsa-xxxx-yyyy" }, observation.Linkset.Aliases);
|
||||
Assert.Equal(new[] { "pkg:npm/left-pad@1.0.0" }, observation.Linkset.Purls);
|
||||
Assert.Equal(new[] { "cpe:2.3:a:example:product:1.0:*:*:*:*:*:*:*" }, observation.Linkset.Cpes);
|
||||
var reference = Assert.Single(observation.Linkset.References);
|
||||
Assert.Equal("advisory", reference.Type);
|
||||
Assert.Equal("https://example.test/advisory", reference.Url);
|
||||
|
||||
var observation = factory.Create(rawDocument, SampleTimestamp);
|
||||
|
||||
Assert.Equal(SampleTimestamp, observation.CreatedAt);
|
||||
Assert.Equal(
|
||||
new[] { "GHSA-XXXX-YYYY", "CVE-2025-0001", "ghsa-XXXX-YYYY", "CVE-2025-0001" },
|
||||
observation.Linkset.Aliases);
|
||||
Assert.Equal(
|
||||
new[] { "pkg:NPM/left-pad@1.0.0", "pkg:npm/left-pad@1.0.0?foo=bar" },
|
||||
observation.Linkset.Purls);
|
||||
Assert.Equal(
|
||||
new[] { "cpe:/a:Example:Product:1.0", "cpe:/a:example:product:1.0" },
|
||||
observation.Linkset.Cpes);
|
||||
Assert.Equal(2, observation.Linkset.References.Length);
|
||||
Assert.All(
|
||||
observation.Linkset.References,
|
||||
reference =>
|
||||
{
|
||||
Assert.Equal("advisory", reference.Type);
|
||||
Assert.Equal("https://example.test/advisory", reference.Url);
|
||||
});
|
||||
|
||||
Assert.Equal(
|
||||
new[] { "GHSA-XXXX-YYYY", " CVE-2025-0001 ", "ghsa-XXXX-YYYY", " CVE-2025-0001 " },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
@@ -52,9 +53,9 @@ public sealed class AdvisoryObservationQueryServiceTests
|
||||
Assert.Equal("tenant-a:osv:beta:1", result.Observations[0].ObservationId);
|
||||
Assert.Equal("tenant-a:ghsa:alpha:1", result.Observations[1].ObservationId);
|
||||
|
||||
Assert.Equal(
|
||||
new[] { "cve-2025-0001", "cve-2025-0002", "ghsa-xyzz" },
|
||||
result.Linkset.Aliases);
|
||||
Assert.Equal(
|
||||
new[] { "CVE-2025-0001", "CVE-2025-0002", "GHSA-xyzz" },
|
||||
result.Linkset.Aliases);
|
||||
|
||||
Assert.Equal(
|
||||
new[] { "pkg:npm/package-a@1.0.0", "pkg:pypi/package-b@2.0.0" },
|
||||
@@ -103,8 +104,11 @@ public sealed class AdvisoryObservationQueryServiceTests
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, result.Observations.Length);
|
||||
Assert.All(result.Observations, observation =>
|
||||
Assert.Contains(observation.Linkset.Aliases, alias => alias is "cve-2025-0001" or "cve-2025-9999"));
|
||||
Assert.All(result.Observations, observation =>
|
||||
Assert.Contains(
|
||||
observation.Linkset.Aliases,
|
||||
alias => alias.Equals("CVE-2025-0001", StringComparison.OrdinalIgnoreCase)
|
||||
|| alias.Equals("CVE-2025-9999", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
Assert.False(result.HasMore);
|
||||
Assert.Null(result.NextCursor);
|
||||
|
||||
@@ -10,5 +10,8 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
|
||||
<ProjectReference Include="../../StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
||||
<ProjectReference Include="../../__Analyzers/StellaOps.Concelier.Analyzers/StellaOps.Concelier.Analyzers.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -221,7 +221,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
Assert.NotNull(ingestResponse.Headers.Location);
|
||||
var locationValue = ingestResponse.Headers.Location!.ToString();
|
||||
Assert.False(string.IsNullOrWhiteSpace(locationValue));
|
||||
var lastSlashIndex = locationValue.LastIndexOf('/', StringComparison.Ordinal);
|
||||
var lastSlashIndex = locationValue.LastIndexOf('/');
|
||||
var idSegment = lastSlashIndex >= 0
|
||||
? locationValue[(lastSlashIndex + 1)..]
|
||||
: locationValue;
|
||||
@@ -886,15 +886,61 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
var limitedResponse = await client.GetAsync("/concelier/exports/index.json");
|
||||
Assert.Equal((HttpStatusCode)429, limitedResponse.StatusCode);
|
||||
Assert.NotNull(limitedResponse.Headers.RetryAfter);
|
||||
Assert.True(limitedResponse.Headers.RetryAfter!.Delta.HasValue);
|
||||
Assert.True(limitedResponse.Headers.RetryAfter!.Delta!.Value.TotalSeconds > 0);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task JobsEndpointsAllowBypassWhenAuthorityEnabled()
|
||||
{
|
||||
var environment = new Dictionary<string, string?>
|
||||
Assert.True(limitedResponse.Headers.RetryAfter!.Delta.HasValue);
|
||||
Assert.True(limitedResponse.Headers.RetryAfter!.Delta!.Value.TotalSeconds > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeModuleDisabledWhenFeatureFlagEnabled()
|
||||
{
|
||||
var environment = new Dictionary<string, string?>
|
||||
{
|
||||
["CONCELIER_FEATURES__NOMERGEENABLED"] = "true"
|
||||
};
|
||||
|
||||
using var factory = new ConcelierApplicationFactory(
|
||||
_runner.ConnectionString,
|
||||
authorityConfigure: null,
|
||||
environmentOverrides: environment);
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var provider = scope.ServiceProvider;
|
||||
|
||||
#pragma warning disable CS0618, CONCELIER0001, CONCELIER0002 // Checking deprecated service registration state.
|
||||
Assert.Null(provider.GetService<AdvisoryMergeService>());
|
||||
#pragma warning restore CS0618, CONCELIER0001, CONCELIER0002
|
||||
|
||||
var schedulerOptions = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
|
||||
Assert.DoesNotContain("merge:reconcile", schedulerOptions.Definitions.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeJobRemainsWhenAllowlisted()
|
||||
{
|
||||
var environment = new Dictionary<string, string?>
|
||||
{
|
||||
["CONCELIER_FEATURES__MERGEJOBALLOWLIST__0"] = "merge:reconcile"
|
||||
};
|
||||
|
||||
using var factory = new ConcelierApplicationFactory(
|
||||
_runner.ConnectionString,
|
||||
authorityConfigure: null,
|
||||
environmentOverrides: environment);
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var provider = scope.ServiceProvider;
|
||||
|
||||
#pragma warning disable CS0618, CONCELIER0001, CONCELIER0002 // Checking deprecated service registration state.
|
||||
Assert.NotNull(provider.GetService<AdvisoryMergeService>());
|
||||
#pragma warning restore CS0618, CONCELIER0001, CONCELIER0002
|
||||
|
||||
var schedulerOptions = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
|
||||
Assert.Contains("merge:reconcile", schedulerOptions.Definitions.Keys);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task JobsEndpointsAllowBypassWhenAuthorityEnabled()
|
||||
{
|
||||
var environment = new Dictionary<string, string?>
|
||||
{
|
||||
["CONCELIER_AUTHORITY__ENABLED"] = "true",
|
||||
["CONCELIER_AUTHORITY__ALLOWANONYMOUSFALLBACK"] = "false",
|
||||
|
||||
Reference in New Issue
Block a user