using StellaOps.Integrations.Contracts; using StellaOps.Integrations.Core; using System.Net.Http.Headers; using System.Text.Json; namespace StellaOps.Integrations.Plugin.GitHubApp; /// /// GitHub App connector plugin for SCM integration. /// Supports GitHub.com and GitHub Enterprise Server. /// public sealed class GitHubAppConnectorPlugin : IIntegrationConnectorPlugin { private readonly TimeProvider _timeProvider; public GitHubAppConnectorPlugin() : this(TimeProvider.System) { } public GitHubAppConnectorPlugin(TimeProvider? timeProvider = null) { _timeProvider = timeProvider ?? TimeProvider.System; } public string Name => "github-app"; public IntegrationType Type => IntegrationType.Scm; public IntegrationProvider Provider => IntegrationProvider.GitHubApp; public bool IsAvailable(IServiceProvider services) => true; public async Task TestConnectionAsync(IntegrationConfig config, CancellationToken cancellationToken = default) { var startTime = _timeProvider.GetUtcNow(); using var client = CreateHttpClient(config); try { // Call GitHub API to verify authentication var response = await client.GetAsync("/app", cancellationToken); var duration = _timeProvider.GetUtcNow() - startTime; if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(cancellationToken); var app = JsonSerializer.Deserialize(content, JsonOptions); return new TestConnectionResult( Success: true, Message: $"Connected as GitHub App: {app?.Name}", Details: new Dictionary { ["endpoint"] = config.Endpoint, ["appName"] = app?.Name ?? "unknown", ["appId"] = app?.Id.ToString() ?? "unknown", ["slug"] = app?.Slug ?? "unknown" }, Duration: duration); } var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); return new TestConnectionResult( Success: false, Message: $"GitHub returned {response.StatusCode}", Details: new Dictionary { ["endpoint"] = config.Endpoint, ["statusCode"] = ((int)response.StatusCode).ToString(), ["error"] = errorContent.Length > 200 ? errorContent[..200] : errorContent }, Duration: duration); } catch (Exception ex) { var duration = _timeProvider.GetUtcNow() - startTime; return new TestConnectionResult( Success: false, Message: $"Connection failed: {ex.Message}", Details: new Dictionary { ["endpoint"] = config.Endpoint, ["error"] = ex.GetType().Name }, Duration: duration); } } public async Task CheckHealthAsync(IntegrationConfig config, CancellationToken cancellationToken = default) { var startTime = _timeProvider.GetUtcNow(); using var client = CreateHttpClient(config); try { // Check GitHub API status var response = await client.GetAsync("/rate_limit", cancellationToken); var duration = _timeProvider.GetUtcNow() - startTime; if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(cancellationToken); var rateLimit = JsonSerializer.Deserialize(content, JsonOptions); var remaining = rateLimit?.Resources?.Core?.Remaining ?? 0; var limit = rateLimit?.Resources?.Core?.Limit ?? 1; var percentUsed = (int)((1 - (double)remaining / limit) * 100); var status = percentUsed switch { < 80 => HealthStatus.Healthy, < 95 => HealthStatus.Degraded, _ => HealthStatus.Unhealthy }; return new HealthCheckResult( Status: status, Message: $"Rate limit: {remaining}/{limit} remaining ({percentUsed}% used)", Details: new Dictionary { ["remaining"] = remaining.ToString(), ["limit"] = limit.ToString(), ["percentUsed"] = percentUsed.ToString() }, CheckedAt: _timeProvider.GetUtcNow(), Duration: duration); } return new HealthCheckResult( Status: HealthStatus.Unhealthy, Message: $"GitHub returned {response.StatusCode}", Details: new Dictionary { ["statusCode"] = ((int)response.StatusCode).ToString() }, CheckedAt: _timeProvider.GetUtcNow(), Duration: duration); } catch (Exception ex) { var duration = _timeProvider.GetUtcNow() - startTime; return new HealthCheckResult( Status: HealthStatus.Unhealthy, Message: $"Health check failed: {ex.Message}", Details: new Dictionary { ["error"] = ex.GetType().Name }, CheckedAt: _timeProvider.GetUtcNow(), Duration: duration); } } private static HttpClient CreateHttpClient(IntegrationConfig config) { var baseUrl = string.IsNullOrEmpty(config.Endpoint) || config.Endpoint == "https://github.com" ? "https://api.github.com" : config.Endpoint.TrimEnd('/') + "/api/v3"; var client = new HttpClient { BaseAddress = new Uri(baseUrl), Timeout = TimeSpan.FromSeconds(30) }; client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StellaOps", "1.0")); // Add JWT token if provided (GitHub App authentication) if (!string.IsNullOrEmpty(config.ResolvedSecret)) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", config.ResolvedSecret); } return client; } private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; private sealed class GitHubAppResponse { public long Id { get; set; } public string? Name { get; set; } public string? Slug { get; set; } } private sealed class GitHubRateLimitResponse { public GitHubResources? Resources { get; set; } } private sealed class GitHubResources { public GitHubRateLimit? Core { get; set; } } private sealed class GitHubRateLimit { public int Limit { get; set; } public int Remaining { get; set; } public int Reset { get; set; } } }