Frontend gaps fill work. Testing fixes work. Auditing in progress.
This commit is contained in:
323
src/Signals/StellaOps.Signals/Scm/Webhooks/GitHubEventMapper.cs
Normal file
323
src/Signals/StellaOps.Signals/Scm/Webhooks/GitHubEventMapper.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Signals.Scm.Models;
|
||||
|
||||
namespace StellaOps.Signals.Scm.Webhooks;
|
||||
|
||||
/// <summary>
|
||||
/// Maps GitHub webhook events to normalized SCM events.
|
||||
/// </summary>
|
||||
public sealed class GitHubEventMapper : IScmEventMapper
|
||||
{
|
||||
public ScmProvider Provider => ScmProvider.GitHub;
|
||||
|
||||
public NormalizedScmEvent? Map(string eventType, string deliveryId, JsonElement payload)
|
||||
{
|
||||
var (scmEventType, extractor) = eventType.ToLowerInvariant() switch
|
||||
{
|
||||
"push" => (ScmEventType.Push, (Func<JsonElement, (ScmEventType, string?, string?)>)ExtractPushDetails),
|
||||
"pull_request" => (ScmEventType.Unknown, ExtractPullRequestDetails),
|
||||
"release" => (ScmEventType.ReleasePublished, ExtractReleaseDetails),
|
||||
"create" => (ScmEventType.Unknown, ExtractCreateDetails),
|
||||
"workflow_run" => (ScmEventType.Unknown, ExtractWorkflowRunDetails),
|
||||
"check_run" => (ScmEventType.Unknown, ExtractCheckRunDetails),
|
||||
_ => (ScmEventType.Unknown, (Func<JsonElement, (ScmEventType, string?, string?)>?)null)
|
||||
};
|
||||
|
||||
if (extractor is null && scmEventType == ScmEventType.Unknown)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var repository = ExtractRepository(payload);
|
||||
if (repository is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string? commitSha = null;
|
||||
string? refName = null;
|
||||
|
||||
if (extractor is not null)
|
||||
{
|
||||
var (extractedType, sha, @ref) = extractor(payload);
|
||||
if (extractedType != ScmEventType.Unknown)
|
||||
{
|
||||
scmEventType = extractedType;
|
||||
}
|
||||
commitSha = sha;
|
||||
refName = @ref;
|
||||
}
|
||||
|
||||
return new NormalizedScmEvent
|
||||
{
|
||||
EventId = deliveryId,
|
||||
Provider = ScmProvider.GitHub,
|
||||
EventType = scmEventType,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Repository = repository,
|
||||
Actor = ExtractActor(payload),
|
||||
Ref = refName ?? GetString(payload, "ref"),
|
||||
CommitSha = commitSha ?? GetNestedString(payload, "head_commit", "id"),
|
||||
PullRequest = ExtractPullRequest(payload),
|
||||
Release = ExtractRelease(payload),
|
||||
Pipeline = ExtractWorkflow(payload)
|
||||
};
|
||||
}
|
||||
|
||||
private static (ScmEventType, string?, string?) ExtractPushDetails(JsonElement payload)
|
||||
{
|
||||
var sha = GetNestedString(payload, "head_commit", "id") ?? GetString(payload, "after");
|
||||
var refName = GetString(payload, "ref");
|
||||
return (ScmEventType.Push, sha, refName);
|
||||
}
|
||||
|
||||
private static (ScmEventType, string?, string?) ExtractPullRequestDetails(JsonElement payload)
|
||||
{
|
||||
var action = GetString(payload, "action");
|
||||
var eventType = action switch
|
||||
{
|
||||
"opened" or "reopened" => ScmEventType.PullRequestOpened,
|
||||
"closed" when GetNestedBool(payload, "pull_request", "merged") => ScmEventType.PullRequestMerged,
|
||||
"closed" => ScmEventType.PullRequestClosed,
|
||||
_ => ScmEventType.Unknown
|
||||
};
|
||||
|
||||
var sha = GetNestedString(payload, "pull_request", "head", "sha");
|
||||
var refName = GetNestedString(payload, "pull_request", "head", "ref");
|
||||
return (eventType, sha, refName);
|
||||
}
|
||||
|
||||
private static (ScmEventType, string?, string?) ExtractReleaseDetails(JsonElement payload)
|
||||
{
|
||||
var action = GetString(payload, "action");
|
||||
if (action != "published")
|
||||
{
|
||||
return (ScmEventType.Unknown, null, null);
|
||||
}
|
||||
|
||||
var tagName = GetNestedString(payload, "release", "tag_name");
|
||||
return (ScmEventType.ReleasePublished, null, tagName);
|
||||
}
|
||||
|
||||
private static (ScmEventType, string?, string?) ExtractCreateDetails(JsonElement payload)
|
||||
{
|
||||
var refType = GetString(payload, "ref_type");
|
||||
if (refType != "tag")
|
||||
{
|
||||
return (ScmEventType.Unknown, null, null);
|
||||
}
|
||||
|
||||
var refName = GetString(payload, "ref");
|
||||
return (ScmEventType.TagCreated, null, refName);
|
||||
}
|
||||
|
||||
private static (ScmEventType, string?, string?) ExtractWorkflowRunDetails(JsonElement payload)
|
||||
{
|
||||
var action = GetString(payload, "action");
|
||||
var conclusion = GetNestedString(payload, "workflow_run", "conclusion");
|
||||
|
||||
var eventType = action switch
|
||||
{
|
||||
"requested" or "in_progress" => ScmEventType.PipelineStarted,
|
||||
"completed" when conclusion == "success" => ScmEventType.PipelineSucceeded,
|
||||
"completed" => ScmEventType.PipelineFailed,
|
||||
_ => ScmEventType.Unknown
|
||||
};
|
||||
|
||||
var sha = GetNestedString(payload, "workflow_run", "head_sha");
|
||||
var refName = GetNestedString(payload, "workflow_run", "head_branch");
|
||||
return (eventType, sha, refName);
|
||||
}
|
||||
|
||||
private static (ScmEventType, string?, string?) ExtractCheckRunDetails(JsonElement payload)
|
||||
{
|
||||
var action = GetString(payload, "action");
|
||||
var status = GetNestedString(payload, "check_run", "status");
|
||||
var conclusion = GetNestedString(payload, "check_run", "conclusion");
|
||||
|
||||
var eventType = (action, status, conclusion) switch
|
||||
{
|
||||
("created", _, _) => ScmEventType.PipelineStarted,
|
||||
("completed", _, "success") => ScmEventType.PipelineSucceeded,
|
||||
("completed", _, _) => ScmEventType.PipelineFailed,
|
||||
_ => ScmEventType.Unknown
|
||||
};
|
||||
|
||||
var sha = GetNestedString(payload, "check_run", "head_sha");
|
||||
return (eventType, sha, null);
|
||||
}
|
||||
|
||||
private static ScmRepository? ExtractRepository(JsonElement payload)
|
||||
{
|
||||
if (!payload.TryGetProperty("repository", out var repo))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScmRepository
|
||||
{
|
||||
Id = GetString(repo, "id")?.ToString(),
|
||||
FullName = GetString(repo, "full_name") ?? string.Empty,
|
||||
Owner = GetNestedString(repo, "owner", "login"),
|
||||
Name = GetString(repo, "name"),
|
||||
CloneUrl = GetString(repo, "clone_url"),
|
||||
DefaultBranch = GetString(repo, "default_branch"),
|
||||
IsPrivate = GetBool(repo, "private")
|
||||
};
|
||||
}
|
||||
|
||||
private static ScmActor? ExtractActor(JsonElement payload)
|
||||
{
|
||||
if (!payload.TryGetProperty("sender", out var sender))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var actorType = GetString(sender, "type") switch
|
||||
{
|
||||
"User" => ScmActorType.User,
|
||||
"Bot" => ScmActorType.Bot,
|
||||
_ => ScmActorType.Unknown
|
||||
};
|
||||
|
||||
return new ScmActor
|
||||
{
|
||||
Id = GetString(sender, "id")?.ToString(),
|
||||
Username = GetString(sender, "login"),
|
||||
Type = actorType
|
||||
};
|
||||
}
|
||||
|
||||
private static ScmPullRequest? ExtractPullRequest(JsonElement payload)
|
||||
{
|
||||
if (!payload.TryGetProperty("pull_request", out var pr))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScmPullRequest
|
||||
{
|
||||
Number = GetInt(pr, "number"),
|
||||
Title = GetString(pr, "title"),
|
||||
SourceBranch = GetNestedString(pr, "head", "ref"),
|
||||
TargetBranch = GetNestedString(pr, "base", "ref"),
|
||||
State = GetString(pr, "state"),
|
||||
Url = GetString(pr, "html_url")
|
||||
};
|
||||
}
|
||||
|
||||
private static ScmRelease? ExtractRelease(JsonElement payload)
|
||||
{
|
||||
if (!payload.TryGetProperty("release", out var release))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScmRelease
|
||||
{
|
||||
Id = GetString(release, "id")?.ToString(),
|
||||
TagName = GetString(release, "tag_name"),
|
||||
Name = GetString(release, "name"),
|
||||
IsPrerelease = GetBool(release, "prerelease"),
|
||||
IsDraft = GetBool(release, "draft"),
|
||||
Url = GetString(release, "html_url")
|
||||
};
|
||||
}
|
||||
|
||||
private static ScmPipeline? ExtractWorkflow(JsonElement payload)
|
||||
{
|
||||
if (!payload.TryGetProperty("workflow_run", out var run))
|
||||
{
|
||||
if (!payload.TryGetProperty("check_run", out var checkRun))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScmPipeline
|
||||
{
|
||||
Id = GetString(checkRun, "id")?.ToString(),
|
||||
Name = GetString(checkRun, "name"),
|
||||
Status = GetString(checkRun, "status") switch
|
||||
{
|
||||
"queued" => ScmPipelineStatus.Queued,
|
||||
"in_progress" => ScmPipelineStatus.InProgress,
|
||||
"completed" => ScmPipelineStatus.Completed,
|
||||
_ => ScmPipelineStatus.Unknown
|
||||
},
|
||||
Conclusion = GetString(checkRun, "conclusion"),
|
||||
Url = GetString(checkRun, "html_url")
|
||||
};
|
||||
}
|
||||
|
||||
return new ScmPipeline
|
||||
{
|
||||
Id = GetString(run, "id")?.ToString(),
|
||||
Name = GetString(run, "name"),
|
||||
RunNumber = GetLong(run, "run_number"),
|
||||
Status = GetString(run, "status") switch
|
||||
{
|
||||
"queued" => ScmPipelineStatus.Queued,
|
||||
"in_progress" => ScmPipelineStatus.InProgress,
|
||||
"completed" => ScmPipelineStatus.Completed,
|
||||
_ => ScmPipelineStatus.Unknown
|
||||
},
|
||||
Conclusion = GetString(run, "conclusion"),
|
||||
Url = GetString(run, "html_url")
|
||||
};
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement element, string property)
|
||||
{
|
||||
return element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: null;
|
||||
}
|
||||
|
||||
private static bool GetBool(JsonElement element, string property)
|
||||
{
|
||||
return element.TryGetProperty(property, out var value) &&
|
||||
value.ValueKind == JsonValueKind.True;
|
||||
}
|
||||
|
||||
private static int GetInt(JsonElement element, string property)
|
||||
{
|
||||
return element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.Number
|
||||
? value.GetInt32()
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static long GetLong(JsonElement element, string property)
|
||||
{
|
||||
return element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.Number
|
||||
? value.GetInt64()
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static string? GetNestedString(JsonElement element, params string[] path)
|
||||
{
|
||||
var current = element;
|
||||
foreach (var prop in path)
|
||||
{
|
||||
if (!current.TryGetProperty(prop, out current))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return current.ValueKind == JsonValueKind.String ? current.GetString() : null;
|
||||
}
|
||||
|
||||
private static bool GetNestedBool(JsonElement element, params string[] path)
|
||||
{
|
||||
var current = element;
|
||||
foreach (var prop in path)
|
||||
{
|
||||
if (!current.TryGetProperty(prop, out current))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return current.ValueKind == JsonValueKind.True;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user