233 lines
9 KiB
C#
233 lines
9 KiB
C#
|
|
using Dapper;
|
||
|
|
using Npgsql;
|
||
|
|
using PlanTempusAdmin.Models;
|
||
|
|
|
||
|
|
namespace PlanTempusAdmin.Services;
|
||
|
|
|
||
|
|
public class ForgejoService
|
||
|
|
{
|
||
|
|
private readonly string _connectionString;
|
||
|
|
private readonly ILogger<ForgejoService> _logger;
|
||
|
|
|
||
|
|
static ForgejoService()
|
||
|
|
{
|
||
|
|
DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
public ForgejoService(IConfiguration configuration, ILogger<ForgejoService> logger)
|
||
|
|
{
|
||
|
|
_connectionString = configuration.GetConnectionString("ForgejoDb")
|
||
|
|
?? throw new InvalidOperationException("ForgejoDb connection string not configured");
|
||
|
|
_logger = logger;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<bool> TestConnectionAsync()
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await using var connection = new NpgsqlConnection(_connectionString);
|
||
|
|
await connection.OpenAsync();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogWarning(ex, "Could not connect to Forgejo database");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<ForgejoDashboard> GetDashboardAsync()
|
||
|
|
{
|
||
|
|
var dashboard = new ForgejoDashboard();
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await using var connection = new NpgsqlConnection(_connectionString);
|
||
|
|
|
||
|
|
// Repository stats
|
||
|
|
var repoStats = await connection.QuerySingleOrDefaultAsync<dynamic>(@"
|
||
|
|
SELECT
|
||
|
|
COUNT(*)::int as total,
|
||
|
|
COUNT(*) FILTER (WHERE NOT is_private)::int as public_repos,
|
||
|
|
COUNT(*) FILTER (WHERE is_private)::int as private_repos,
|
||
|
|
COUNT(*) FILTER (WHERE is_fork)::int as forked,
|
||
|
|
COUNT(*) FILTER (WHERE is_archived)::int as archived,
|
||
|
|
COUNT(*) FILTER (WHERE is_mirror)::int as mirrors,
|
||
|
|
COALESCE(SUM(size), 0) as total_size,
|
||
|
|
COALESCE(SUM(num_stars), 0)::int as total_stars,
|
||
|
|
COALESCE(SUM(num_forks), 0)::int as total_forks,
|
||
|
|
COALESCE(SUM(num_issues - num_closed_issues), 0)::int as open_issues,
|
||
|
|
COALESCE(SUM(num_pulls - num_closed_pulls), 0)::int as open_prs
|
||
|
|
FROM repository");
|
||
|
|
|
||
|
|
if (repoStats != null)
|
||
|
|
{
|
||
|
|
dashboard.TotalRepos = (int)repoStats.total;
|
||
|
|
dashboard.PublicRepos = (int)repoStats.public_repos;
|
||
|
|
dashboard.PrivateRepos = (int)repoStats.private_repos;
|
||
|
|
dashboard.ForkedRepos = (int)repoStats.forked;
|
||
|
|
dashboard.ArchivedRepos = (int)repoStats.archived;
|
||
|
|
dashboard.MirrorRepos = (int)repoStats.mirrors;
|
||
|
|
dashboard.TotalSize = (long)repoStats.total_size;
|
||
|
|
dashboard.TotalStars = (int)repoStats.total_stars;
|
||
|
|
dashboard.TotalForks = (int)repoStats.total_forks;
|
||
|
|
dashboard.TotalOpenIssues = (int)repoStats.open_issues;
|
||
|
|
dashboard.TotalOpenPRs = (int)repoStats.open_prs;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Actions stats
|
||
|
|
var actionStats = await connection.QuerySingleOrDefaultAsync<dynamic>(@"
|
||
|
|
SELECT
|
||
|
|
COUNT(*)::int as total,
|
||
|
|
COUNT(*) FILTER (WHERE TO_TIMESTAMP(created) >= NOW() - INTERVAL '1 day')::int as today,
|
||
|
|
COUNT(*) FILTER (WHERE TO_TIMESTAMP(created) >= NOW() - INTERVAL '7 days')::int as this_week,
|
||
|
|
COUNT(*) FILTER (WHERE status = 3)::int as successful,
|
||
|
|
COUNT(*) FILTER (WHERE status = 4)::int as failed,
|
||
|
|
COUNT(*) FILTER (WHERE status = 2)::int as running
|
||
|
|
FROM action_run");
|
||
|
|
|
||
|
|
if (actionStats != null)
|
||
|
|
{
|
||
|
|
dashboard.TotalRuns = (int)actionStats.total;
|
||
|
|
dashboard.RunsToday = (int)actionStats.today;
|
||
|
|
dashboard.RunsThisWeek = (int)actionStats.this_week;
|
||
|
|
dashboard.SuccessfulRuns = (int)actionStats.successful;
|
||
|
|
dashboard.FailedRunsCount = (int)actionStats.failed;
|
||
|
|
dashboard.RunningNow = (int)actionStats.running;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Recently updated repos
|
||
|
|
dashboard.RecentlyUpdated = await GetRepositoriesAsync(connection, "ORDER BY r.updated_unix DESC LIMIT 5");
|
||
|
|
|
||
|
|
// Largest repos
|
||
|
|
dashboard.LargestRepos = await GetRepositoriesAsync(connection, "ORDER BY r.size DESC LIMIT 5");
|
||
|
|
|
||
|
|
// Recent action runs
|
||
|
|
dashboard.RecentRuns = await GetActionRunsAsync(connection, "ORDER BY ar.created DESC LIMIT 10");
|
||
|
|
|
||
|
|
// Failed runs
|
||
|
|
dashboard.FailedRuns = await GetActionRunsAsync(connection, "WHERE ar.status = 4 ORDER BY ar.created DESC LIMIT 5");
|
||
|
|
|
||
|
|
// Running now
|
||
|
|
dashboard.RunningRuns = await GetActionRunsAsync(connection, "WHERE ar.status = 2 ORDER BY ar.started LIMIT 10");
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogError(ex, "Error fetching Forgejo dashboard");
|
||
|
|
}
|
||
|
|
|
||
|
|
return dashboard;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<List<ForgejoRepository>> GetAllRepositoriesAsync()
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await using var connection = new NpgsqlConnection(_connectionString);
|
||
|
|
return await GetRepositoriesAsync(connection, "ORDER BY LOWER(u.name), LOWER(r.name)");
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogError(ex, "Error fetching repositories");
|
||
|
|
return new List<ForgejoRepository>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<List<ForgejoActionRun>> GetAllActionRunsAsync(int limit = 100)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await using var connection = new NpgsqlConnection(_connectionString);
|
||
|
|
return await GetActionRunsAsync(connection, $"ORDER BY ar.created DESC LIMIT {limit}");
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogError(ex, "Error fetching action runs");
|
||
|
|
return new List<ForgejoActionRun>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<List<ForgejoActionStats>> GetActionStatsAsync()
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await using var connection = new NpgsqlConnection(_connectionString);
|
||
|
|
var stats = await connection.QueryAsync<ForgejoActionStats>(@"
|
||
|
|
SELECT
|
||
|
|
ar.workflow_id,
|
||
|
|
r.name as repo_name,
|
||
|
|
COUNT(*)::int as total_runs,
|
||
|
|
COUNT(*) FILTER (WHERE ar.status = 3)::int as successful,
|
||
|
|
COUNT(*) FILTER (WHERE ar.status = 4)::int as failed,
|
||
|
|
TO_TIMESTAMP(MAX(ar.created)) as last_run,
|
||
|
|
AVG(ar.stopped - ar.started) FILTER (WHERE ar.stopped > 0 AND ar.started > 0) as avg_duration_seconds
|
||
|
|
FROM action_run ar
|
||
|
|
JOIN repository r ON ar.repo_id = r.id
|
||
|
|
GROUP BY ar.workflow_id, r.name
|
||
|
|
ORDER BY total_runs DESC");
|
||
|
|
return stats.ToList();
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogError(ex, "Error fetching action stats");
|
||
|
|
return new List<ForgejoActionStats>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task<List<ForgejoRepository>> GetRepositoriesAsync(NpgsqlConnection connection, string orderClause)
|
||
|
|
{
|
||
|
|
var repos = await connection.QueryAsync<ForgejoRepository>($@"
|
||
|
|
SELECT
|
||
|
|
r.id,
|
||
|
|
u.name as owner_name,
|
||
|
|
r.name,
|
||
|
|
r.description,
|
||
|
|
r.is_private,
|
||
|
|
r.is_fork,
|
||
|
|
r.is_archived,
|
||
|
|
r.is_mirror,
|
||
|
|
r.num_stars,
|
||
|
|
r.num_forks,
|
||
|
|
r.num_watches,
|
||
|
|
r.num_issues,
|
||
|
|
r.num_closed_issues,
|
||
|
|
r.num_pulls,
|
||
|
|
r.num_closed_pulls,
|
||
|
|
r.size,
|
||
|
|
TO_TIMESTAMP(r.created_unix) as created_at,
|
||
|
|
TO_TIMESTAMP(r.updated_unix) as updated_at
|
||
|
|
FROM repository r
|
||
|
|
JOIN ""user"" u ON r.owner_id = u.id
|
||
|
|
{orderClause}");
|
||
|
|
return repos.ToList();
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task<List<ForgejoActionRun>> GetActionRunsAsync(NpgsqlConnection connection, string whereOrderClause)
|
||
|
|
{
|
||
|
|
var runs = await connection.QueryAsync<ForgejoActionRun>($@"
|
||
|
|
SELECT
|
||
|
|
ar.id,
|
||
|
|
ar.repo_id,
|
||
|
|
r.name as repo_name,
|
||
|
|
u.name as owner_name,
|
||
|
|
ar.workflow_id,
|
||
|
|
ar.""index"",
|
||
|
|
COALESCE(tu.name, '') as trigger_user,
|
||
|
|
ar.ref,
|
||
|
|
ar.commit_sha,
|
||
|
|
ar.event,
|
||
|
|
ar.title,
|
||
|
|
ar.status,
|
||
|
|
CASE WHEN ar.started > 0 THEN TO_TIMESTAMP(ar.started) ELSE NULL END as started,
|
||
|
|
CASE WHEN ar.stopped > 0 THEN TO_TIMESTAMP(ar.stopped) ELSE NULL END as stopped,
|
||
|
|
TO_TIMESTAMP(ar.created) as created,
|
||
|
|
TO_TIMESTAMP(ar.updated) as updated
|
||
|
|
FROM action_run ar
|
||
|
|
JOIN repository r ON ar.repo_id = r.id
|
||
|
|
JOIN ""user"" u ON r.owner_id = u.id
|
||
|
|
LEFT JOIN ""user"" tu ON ar.trigger_user_id = tu.id
|
||
|
|
{whereOrderClause}");
|
||
|
|
return runs.ToList();
|
||
|
|
}
|
||
|
|
}
|