206 lines
7.4 KiB
C#
206 lines
7.4 KiB
C#
|
|
using StellaOps.Integrations.Contracts;
|
|
using StellaOps.Integrations.Core;
|
|
using System.Net.Http.Headers;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.Integrations.Plugin.GitHubApp;
|
|
|
|
/// <summary>
|
|
/// GitHub App connector plugin for SCM integration.
|
|
/// Supports GitHub.com and GitHub Enterprise Server.
|
|
/// </summary>
|
|
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<TestConnectionResult> 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<GitHubAppResponse>(content, JsonOptions);
|
|
|
|
return new TestConnectionResult(
|
|
Success: true,
|
|
Message: $"Connected as GitHub App: {app?.Name}",
|
|
Details: new Dictionary<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["endpoint"] = config.Endpoint,
|
|
["error"] = ex.GetType().Name
|
|
},
|
|
Duration: duration);
|
|
}
|
|
}
|
|
|
|
public async Task<HealthCheckResult> 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<GitHubRateLimitResponse>(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<string, string>
|
|
{
|
|
["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<string, string> { ["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<string, string> { ["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; }
|
|
}
|
|
}
|