feat: Add Bun language analyzer and related functionality

- Implemented BunPackageNormalizer to deduplicate packages by name and version.
- Created BunProjectDiscoverer to identify Bun project roots in the filesystem.
- Added project files for the Bun analyzer including manifest and project configuration.
- Developed comprehensive tests for Bun language analyzer covering various scenarios.
- Included fixture files for testing standard installs, isolated linker installs, lockfile-only scenarios, and workspaces.
- Established stubs for authentication sessions to facilitate testing in the web application.
This commit is contained in:
StellaOps Bot
2025-12-06 11:20:35 +02:00
parent b978ae399f
commit a7cd10020a
85 changed files with 7414 additions and 42 deletions

View File

@@ -0,0 +1,148 @@
namespace StellaOps.Concelier.WebService.Deprecation;
/// <summary>
/// Standard HTTP deprecation headers per RFC 8594 and Sunset header spec.
/// Per CONCELIER-WEB-OAS-63-001.
/// </summary>
public static class DeprecationHeaders
{
/// <summary>
/// The Deprecation header field (RFC 8594).
/// Value is a date when the API was deprecated.
/// </summary>
public const string Deprecation = "Deprecation";
/// <summary>
/// The Sunset header field.
/// Value is an HTTP-date when the API will be removed.
/// </summary>
public const string Sunset = "Sunset";
/// <summary>
/// Link header with relation type pointing to successor API.
/// </summary>
public const string Link = "Link";
/// <summary>
/// Custom header for deprecation notice message.
/// </summary>
public const string XDeprecationNotice = "X-Deprecation-Notice";
/// <summary>
/// Custom header for migration guide URL.
/// </summary>
public const string XDeprecationGuide = "X-Deprecation-Guide";
}
/// <summary>
/// Deprecation information for an API endpoint.
/// </summary>
public sealed record DeprecationInfo
{
/// <summary>
/// Date when the API was deprecated (RFC 8594 format).
/// </summary>
public required DateTimeOffset DeprecatedAt { get; init; }
/// <summary>
/// Date when the API will be removed (Sunset header).
/// Null if no sunset date is set.
/// </summary>
public DateTimeOffset? SunsetAt { get; init; }
/// <summary>
/// URI of the successor API endpoint.
/// </summary>
public required string SuccessorUri { get; init; }
/// <summary>
/// Human-readable deprecation message.
/// </summary>
public required string Message { get; init; }
/// <summary>
/// URL to migration guide documentation.
/// </summary>
public string? MigrationGuideUrl { get; init; }
}
/// <summary>
/// Registry of deprecated endpoints and their successors.
/// </summary>
public static class DeprecatedEndpoints
{
/// <summary>
/// Date when legacy linkset/observation APIs were deprecated.
/// </summary>
public static readonly DateTimeOffset LegacyApisDeprecatedAt = new(2025, 12, 1, 0, 0, 0, TimeSpan.Zero);
/// <summary>
/// Date when legacy linkset/observation APIs will be removed.
/// </summary>
public static readonly DateTimeOffset LegacyApisSunsetAt = new(2026, 6, 1, 0, 0, 0, TimeSpan.Zero);
/// <summary>
/// Base URL for migration documentation.
/// </summary>
public const string MigrationGuideBaseUrl = "https://docs.stellaops.io/concelier/migration/lnm-v1";
/// <summary>
/// Legacy /linksets endpoint deprecation info.
/// </summary>
public static readonly DeprecationInfo LegacyLinksets = new()
{
DeprecatedAt = LegacyApisDeprecatedAt,
SunsetAt = LegacyApisSunsetAt,
SuccessorUri = "/v1/lnm/linksets",
Message = "This endpoint is deprecated. Use /v1/lnm/linksets instead for Link-Not-Merge linkset retrieval.",
MigrationGuideUrl = $"{MigrationGuideBaseUrl}#linksets"
};
/// <summary>
/// Legacy /advisories/observations endpoint deprecation info.
/// </summary>
public static readonly DeprecationInfo LegacyAdvisoryObservations = new()
{
DeprecatedAt = LegacyApisDeprecatedAt,
SunsetAt = LegacyApisSunsetAt,
SuccessorUri = "/v1/lnm/linksets",
Message = "This endpoint is deprecated. Use /v1/lnm/linksets with includeObservations=true instead.",
MigrationGuideUrl = $"{MigrationGuideBaseUrl}#observations"
};
/// <summary>
/// Legacy /advisories/linksets endpoint deprecation info.
/// </summary>
public static readonly DeprecationInfo LegacyAdvisoryLinksets = new()
{
DeprecatedAt = LegacyApisDeprecatedAt,
SunsetAt = LegacyApisSunsetAt,
SuccessorUri = "/v1/lnm/linksets",
Message = "This endpoint is deprecated. Use /v1/lnm/linksets instead for Link-Not-Merge linkset retrieval.",
MigrationGuideUrl = $"{MigrationGuideBaseUrl}#linksets"
};
/// <summary>
/// Legacy /advisories/linksets/export endpoint deprecation info.
/// </summary>
public static readonly DeprecationInfo LegacyAdvisoryLinksetsExport = new()
{
DeprecatedAt = LegacyApisDeprecatedAt,
SunsetAt = LegacyApisSunsetAt,
SuccessorUri = "/v1/lnm/linksets",
Message = "This endpoint is deprecated. Use /v1/lnm/linksets with appropriate pagination for bulk export.",
MigrationGuideUrl = $"{MigrationGuideBaseUrl}#export"
};
/// <summary>
/// Legacy /concelier/observations endpoint deprecation info.
/// </summary>
public static readonly DeprecationInfo LegacyConcelierObservations = new()
{
DeprecatedAt = LegacyApisDeprecatedAt,
SunsetAt = LegacyApisSunsetAt,
SuccessorUri = "/v1/lnm/linksets",
Message = "This endpoint is deprecated. Use /v1/lnm/linksets with includeObservations=true instead.",
MigrationGuideUrl = $"{MigrationGuideBaseUrl}#observations"
};
}

View File

@@ -0,0 +1,97 @@
using System.Globalization;
namespace StellaOps.Concelier.WebService.Deprecation;
/// <summary>
/// Extension methods for adding deprecation headers to HTTP responses.
/// Per CONCELIER-WEB-OAS-63-001.
/// </summary>
public static class DeprecationMiddlewareExtensions
{
/// <summary>
/// Adds deprecation headers to the HTTP response.
/// </summary>
public static void AddDeprecationHeaders(this HttpContext context, DeprecationInfo deprecation)
{
var headers = context.Response.Headers;
// RFC 8594 Deprecation header (HTTP-date format)
headers[DeprecationHeaders.Deprecation] = FormatHttpDate(deprecation.DeprecatedAt);
// Sunset header if set
if (deprecation.SunsetAt.HasValue)
{
headers[DeprecationHeaders.Sunset] = FormatHttpDate(deprecation.SunsetAt.Value);
}
// Link header pointing to successor
headers[DeprecationHeaders.Link] = $"<{deprecation.SuccessorUri}>; rel=\"successor-version\"";
// Custom deprecation notice
headers[DeprecationHeaders.XDeprecationNotice] = deprecation.Message;
// Migration guide URL if available
if (!string.IsNullOrEmpty(deprecation.MigrationGuideUrl))
{
headers[DeprecationHeaders.XDeprecationGuide] = deprecation.MigrationGuideUrl;
}
}
/// <summary>
/// Formats a DateTimeOffset as an HTTP-date (RFC 7231).
/// </summary>
private static string FormatHttpDate(DateTimeOffset date)
{
// HTTP-date format: "Sun, 06 Nov 1994 08:49:37 GMT"
return date.UtcDateTime.ToString("r", CultureInfo.InvariantCulture);
}
}
/// <summary>
/// Middleware that adds deprecation headers to deprecated endpoints.
/// </summary>
public sealed class DeprecationMiddleware
{
private readonly RequestDelegate _next;
private readonly Dictionary<string, DeprecationInfo> _deprecatedPaths;
public DeprecationMiddleware(RequestDelegate next)
{
_next = next;
_deprecatedPaths = new Dictionary<string, DeprecationInfo>(StringComparer.OrdinalIgnoreCase)
{
["/linksets"] = DeprecatedEndpoints.LegacyLinksets,
["/advisories/observations"] = DeprecatedEndpoints.LegacyAdvisoryObservations,
["/advisories/linksets"] = DeprecatedEndpoints.LegacyAdvisoryLinksets,
["/advisories/linksets/export"] = DeprecatedEndpoints.LegacyAdvisoryLinksetsExport,
["/concelier/observations"] = DeprecatedEndpoints.LegacyConcelierObservations
};
}
public async Task InvokeAsync(HttpContext context)
{
var path = context.Request.Path.Value ?? string.Empty;
// Check if this is a deprecated path
if (_deprecatedPaths.TryGetValue(path, out var deprecation))
{
context.AddDeprecationHeaders(deprecation);
}
await _next(context);
}
}
/// <summary>
/// Extension methods for registering the deprecation middleware.
/// </summary>
public static class DeprecationMiddlewareRegistration
{
/// <summary>
/// Adds the deprecation middleware to the pipeline.
/// </summary>
public static IApplicationBuilder UseDeprecationHeaders(this IApplicationBuilder app)
{
return app.UseMiddleware<DeprecationMiddleware>();
}
}