new advisories work and features gaps work
This commit is contained in:
@@ -0,0 +1,654 @@
|
||||
// <copyright file="ScmAnnotationContracts.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_006_INTEGRATIONS_scm_annotations (INTEGRATIONS-SCM-001)
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Integrations.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Contract for posting comments to PRs/MRs.
|
||||
/// </summary>
|
||||
public sealed record ScmCommentRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Repository owner (organization or user).
|
||||
/// </summary>
|
||||
[JsonPropertyName("owner")]
|
||||
public required string Owner { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Repository name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("repo")]
|
||||
public required string Repo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// PR/MR number.
|
||||
/// </summary>
|
||||
[JsonPropertyName("prNumber")]
|
||||
public required int PrNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Comment body (Markdown supported).
|
||||
/// </summary>
|
||||
[JsonPropertyName("body")]
|
||||
public required string Body { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional path for file-level comments.
|
||||
/// </summary>
|
||||
[JsonPropertyName("path")]
|
||||
public string? Path { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional line number for inline comments.
|
||||
/// </summary>
|
||||
[JsonPropertyName("line")]
|
||||
public int? Line { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional commit SHA for positioning.
|
||||
/// </summary>
|
||||
[JsonPropertyName("commitSha")]
|
||||
public string? CommitSha { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Comment context (e.g., "stellaops-scan", "stellaops-vex").
|
||||
/// </summary>
|
||||
[JsonPropertyName("context")]
|
||||
public string Context { get; init; } = "stellaops";
|
||||
|
||||
/// <summary>
|
||||
/// Link to evidence pack or detailed report.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidenceUrl")]
|
||||
public string? EvidenceUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Correlation ID for tracing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("traceId")]
|
||||
public string? TraceId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response from posting a comment.
|
||||
/// </summary>
|
||||
public sealed record ScmCommentResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Comment ID in the SCM system.
|
||||
/// </summary>
|
||||
[JsonPropertyName("commentId")]
|
||||
public required string CommentId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to the comment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("url")]
|
||||
public required string Url { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the comment was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAt")]
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the comment was created or updated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("wasUpdated")]
|
||||
public bool WasUpdated { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contract for posting commit/PR status checks.
|
||||
/// </summary>
|
||||
public sealed record ScmStatusRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Repository owner.
|
||||
/// </summary>
|
||||
[JsonPropertyName("owner")]
|
||||
public required string Owner { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Repository name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("repo")]
|
||||
public required string Repo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Commit SHA to post status on.
|
||||
/// </summary>
|
||||
[JsonPropertyName("commitSha")]
|
||||
public required string CommitSha { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Status state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public required ScmStatusState State { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Context name (e.g., "stellaops/security-scan").
|
||||
/// </summary>
|
||||
[JsonPropertyName("context")]
|
||||
public required string Context { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Short description of the status.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public required string Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// URL for more details.
|
||||
/// </summary>
|
||||
[JsonPropertyName("targetUrl")]
|
||||
public string? TargetUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Link to evidence pack.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidenceUrl")]
|
||||
public string? EvidenceUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Correlation ID for tracing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("traceId")]
|
||||
public string? TraceId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status check states.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum ScmStatusState
|
||||
{
|
||||
/// <summary>Status check is pending.</summary>
|
||||
Pending,
|
||||
|
||||
/// <summary>Status check passed.</summary>
|
||||
Success,
|
||||
|
||||
/// <summary>Status check failed.</summary>
|
||||
Failure,
|
||||
|
||||
/// <summary>Status check errored.</summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response from posting a status check.
|
||||
/// </summary>
|
||||
public sealed record ScmStatusResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Status ID in the SCM system.
|
||||
/// </summary>
|
||||
[JsonPropertyName("statusId")]
|
||||
public required string StatusId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// State that was set.
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public required ScmStatusState State { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to the status check.
|
||||
/// </summary>
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the status was created/updated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAt")]
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contract for creating check runs (GitHub-specific, richer than status checks).
|
||||
/// </summary>
|
||||
public sealed record ScmCheckRunRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Repository owner.
|
||||
/// </summary>
|
||||
[JsonPropertyName("owner")]
|
||||
public required string Owner { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Repository name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("repo")]
|
||||
public required string Repo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Check run name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Head SHA to associate with.
|
||||
/// </summary>
|
||||
[JsonPropertyName("headSha")]
|
||||
public required string HeadSha { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Check run status.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public required ScmCheckRunStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Conclusion (required when status is completed).
|
||||
/// </summary>
|
||||
[JsonPropertyName("conclusion")]
|
||||
public ScmCheckRunConclusion? Conclusion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Title for the check run output.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Summary (Markdown).
|
||||
/// </summary>
|
||||
[JsonPropertyName("summary")]
|
||||
public string? Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detailed text (Markdown).
|
||||
/// </summary>
|
||||
[JsonPropertyName("text")]
|
||||
public string? Text { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Annotations to add to the check run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("annotations")]
|
||||
public ImmutableArray<ScmCheckRunAnnotation> Annotations { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Link to evidence pack.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidenceUrl")]
|
||||
public string? EvidenceUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Correlation ID for tracing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("traceId")]
|
||||
public string? TraceId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check run status.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum ScmCheckRunStatus
|
||||
{
|
||||
/// <summary>Check run is queued.</summary>
|
||||
Queued,
|
||||
|
||||
/// <summary>Check run is in progress.</summary>
|
||||
InProgress,
|
||||
|
||||
/// <summary>Check run is completed.</summary>
|
||||
Completed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check run conclusion.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum ScmCheckRunConclusion
|
||||
{
|
||||
/// <summary>Action required.</summary>
|
||||
ActionRequired,
|
||||
|
||||
/// <summary>Cancelled.</summary>
|
||||
Cancelled,
|
||||
|
||||
/// <summary>Failed.</summary>
|
||||
Failure,
|
||||
|
||||
/// <summary>Neutral.</summary>
|
||||
Neutral,
|
||||
|
||||
/// <summary>Success.</summary>
|
||||
Success,
|
||||
|
||||
/// <summary>Skipped.</summary>
|
||||
Skipped,
|
||||
|
||||
/// <summary>Stale.</summary>
|
||||
Stale,
|
||||
|
||||
/// <summary>Timed out.</summary>
|
||||
TimedOut
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Annotation for a check run.
|
||||
/// </summary>
|
||||
public sealed record ScmCheckRunAnnotation
|
||||
{
|
||||
/// <summary>
|
||||
/// File path relative to repository root.
|
||||
/// </summary>
|
||||
[JsonPropertyName("path")]
|
||||
public required string Path { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Start line number.
|
||||
/// </summary>
|
||||
[JsonPropertyName("startLine")]
|
||||
public required int StartLine { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// End line number.
|
||||
/// </summary>
|
||||
[JsonPropertyName("endLine")]
|
||||
public required int EndLine { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Annotation level.
|
||||
/// </summary>
|
||||
[JsonPropertyName("level")]
|
||||
public required ScmAnnotationLevel Level { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Annotation message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public required string Message { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Title for the annotation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw details (not rendered).
|
||||
/// </summary>
|
||||
[JsonPropertyName("rawDetails")]
|
||||
public string? RawDetails { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Annotation severity level.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum ScmAnnotationLevel
|
||||
{
|
||||
/// <summary>Notice level.</summary>
|
||||
Notice,
|
||||
|
||||
/// <summary>Warning level.</summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>Failure level.</summary>
|
||||
Failure
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response from creating a check run.
|
||||
/// </summary>
|
||||
public sealed record ScmCheckRunResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Check run ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("checkRunId")]
|
||||
public required string CheckRunId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to the check run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("url")]
|
||||
public required string Url { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// HTML URL for the check run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("htmlUrl")]
|
||||
public string? HtmlUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Status that was set.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public required ScmCheckRunStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Conclusion if completed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("conclusion")]
|
||||
public ScmCheckRunConclusion? Conclusion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the check run started.
|
||||
/// </summary>
|
||||
[JsonPropertyName("startedAt")]
|
||||
public DateTimeOffset? StartedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the check run completed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("completedAt")]
|
||||
public DateTimeOffset? CompletedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of annotations posted.
|
||||
/// </summary>
|
||||
[JsonPropertyName("annotationCount")]
|
||||
public int AnnotationCount { get; init; }
|
||||
}
|
||||
|
||||
// Sprint: SPRINT_20260112_006_INTEGRATIONS_scm_annotations (INTEGRATIONS-SCM-002)
|
||||
|
||||
/// <summary>
|
||||
/// Contract for updating an existing check run.
|
||||
/// </summary>
|
||||
public sealed record ScmCheckRunUpdateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Repository owner.
|
||||
/// </summary>
|
||||
[JsonPropertyName("owner")]
|
||||
public required string Owner { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Repository name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("repo")]
|
||||
public required string Repo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Check run ID to update.
|
||||
/// </summary>
|
||||
[JsonPropertyName("checkRunId")]
|
||||
public required string CheckRunId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated name (optional).
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated status (optional).
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public ScmCheckRunStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Conclusion (required when status is completed).
|
||||
/// </summary>
|
||||
[JsonPropertyName("conclusion")]
|
||||
public ScmCheckRunConclusion? Conclusion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the check run completed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("completedAt")]
|
||||
public DateTimeOffset? CompletedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated summary.
|
||||
/// </summary>
|
||||
[JsonPropertyName("summary")]
|
||||
public string? Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated text body.
|
||||
/// </summary>
|
||||
[JsonPropertyName("text")]
|
||||
public string? Text { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional annotations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("annotations")]
|
||||
public IReadOnlyList<ScmCheckRunAnnotation>? Annotations { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// URL for more details.
|
||||
/// </summary>
|
||||
[JsonPropertyName("detailsUrl")]
|
||||
public string? DetailsUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Link to evidence pack.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidenceUrl")]
|
||||
public string? EvidenceUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Correlation ID for tracing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("traceId")]
|
||||
public string? TraceId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for SCM annotation clients.
|
||||
/// </summary>
|
||||
public interface IScmAnnotationClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Posts a comment to a PR/MR.
|
||||
/// </summary>
|
||||
Task<ScmOperationResult<ScmCommentResponse>> PostCommentAsync(
|
||||
ScmCommentRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Posts a commit status.
|
||||
/// </summary>
|
||||
Task<ScmOperationResult<ScmStatusResponse>> PostStatusAsync(
|
||||
ScmStatusRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a check run (GitHub Apps only).
|
||||
/// </summary>
|
||||
Task<ScmOperationResult<ScmCheckRunResponse>> CreateCheckRunAsync(
|
||||
ScmCheckRunRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing check run.
|
||||
/// </summary>
|
||||
Task<ScmOperationResult<ScmCheckRunResponse>> UpdateCheckRunAsync(
|
||||
ScmCheckRunUpdateRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of an offline-safe SCM operation.
|
||||
/// </summary>
|
||||
public sealed record ScmOperationResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the operation succeeded.
|
||||
/// </summary>
|
||||
[JsonPropertyName("success")]
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Result data (if successful).
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public T? Data { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message (if failed).
|
||||
/// </summary>
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the error is transient and can be retried.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isTransient")]
|
||||
public bool IsTransient { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the operation was queued for later (offline mode).
|
||||
/// </summary>
|
||||
[JsonPropertyName("queued")]
|
||||
public bool Queued { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Queue ID if queued.
|
||||
/// </summary>
|
||||
[JsonPropertyName("queueId")]
|
||||
public string? QueueId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful result.
|
||||
/// </summary>
|
||||
public static ScmOperationResult<T> Ok(T data) => new()
|
||||
{
|
||||
Success = true,
|
||||
Data = data
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed result.
|
||||
/// </summary>
|
||||
public static ScmOperationResult<T> Fail(string error, bool isTransient = false) => new()
|
||||
{
|
||||
Success = false,
|
||||
Error = error,
|
||||
IsTransient = isTransient
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a queued result (offline mode).
|
||||
/// </summary>
|
||||
public static ScmOperationResult<T> QueuedForLater(string queueId) => new()
|
||||
{
|
||||
Success = false,
|
||||
Queued = true,
|
||||
QueueId = queueId
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user