discord-bot/Clients/CirrusCiClient/CirrusCi.cs
2023-04-21 02:05:59 +05:00

150 lines
6.7 KiB
C#

using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CirrusCiClient.Generated;
using CirrusCiClient.POCOs;
using CompatApiClient;
using CompatApiClient.Utils;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using StrawberryShake;
namespace CirrusCiClient;
public static class CirrusCi
{
private static readonly MemoryCache BuildInfoCache = new(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromHours(1) });
private static readonly IServiceProvider ServiceProvider;
private static IClient Client => ServiceProvider.GetRequiredService<IClient>();
static CirrusCi()
{
var collection = new ServiceCollection();
collection.AddClient(ExecutionStrategy.CacheAndNetwork).ConfigureHttpClient(c => c.BaseAddress = new("https://api.cirrus-ci.com/graphql"));
ServiceProvider = collection.BuildServiceProvider();
}
public static async Task<BuildInfo?> GetPrBuildInfoAsync(string? commit, DateTime? oldestTimestamp, int pr, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(commit))
return null;
commit = commit.ToLower();
var queryResult = await Client.GetPrBuilds.ExecuteAsync("pull/" + pr, oldestTimestamp.ToTimestamp(), cancellationToken);
queryResult.EnsureNoErrors();
if (queryResult.Data?.OwnerRepository?.Builds?.Edges is {Count: > 0} edgeList)
{
var node = edgeList.LastOrDefault(e => e?.Node?.ChangeIdInRepo == commit)?.Node;
if (node is null)
return null;
var winTask = node.Tasks?.FirstOrDefault(t => t?.Name.Contains("Windows") ?? false);
var winArtifact = winTask?.Artifacts?
.Where(a => a?.Files is {Count: >0})
.SelectMany(a => a!.Files!)
.FirstOrDefault(f => f?.Path.EndsWith(".7z") ?? false);
var linTask = node.Tasks?.FirstOrDefault(t => t is {} lt && lt.Name.Contains("Linux") && lt.Name.Contains("GCC"));
var linArtifact = linTask?.Artifacts?
.Where(a => a?.Files is {Count: >0})
.SelectMany(a => a!.Files!)
.FirstOrDefault(a => a?.Path.EndsWith(".AppImage") ?? false);
var macTask = node.Tasks?.FirstOrDefault(t => t?.Name.Contains("macOS") ?? false);
var macArtifact = macTask?.Artifacts?
.Where(a => a?.Files is { Count: > 0 })
.SelectMany(a => a!.Files!)
.FirstOrDefault(a => a?.Path.EndsWith(".dmg") ?? false);
var startTime = FromTimestamp(node.BuildCreatedTimestamp);
var finishTime = GetFinishTime(node);
return new()
{
Commit = node.ChangeIdInRepo,
StartTime = startTime,
FinishTime = finishTime,
WindowsBuild = new()
{
Filename = winArtifact?.Path is string wp ? Path.GetFileName(wp) : null,
DownloadLink = winTask?.Id is string wtid && winArtifact?.Path is string wtap ? $"https://api.cirrus-ci.com/v1/artifact/task/{wtid}/Artifact/{wtap}" : null,
Status = winTask?.Status,
},
LinuxBuild = new()
{
Filename = linArtifact?.Path is string lp ? Path.GetFileName(lp) : null,
DownloadLink = linTask?.Id is string ltid && linArtifact?.Path is string ltap ? $"https://api.cirrus-ci.com/v1/artifact/task/{ltid}/Artifact/{ltap}" : null,
Status = linTask?.Status,
},
MacBuild = new()
{
Filename = macArtifact?.Path is string mp ? Path.GetFileName(mp) : null,
DownloadLink = macTask?.Id is string mtid && macArtifact?.Path is string mtap ? $"https://api.cirrus-ci.com/v1/artifact/task/{mtid}/Artifact/{mtap}" : null,
Status= macTask?.Status,
}
};
}
return null;
}
public static async Task<ProjectBuildStats> GetPipelineDurationAsync(CancellationToken cancellationToken)
{
const string cacheKey = "project-build-stats";
if (BuildInfoCache.TryGetValue(cacheKey, out ProjectBuildStats? result) && result is not null)
return result;
try
{
var queryResult = await Client.GetLastFewBuilds.ExecuteAsync(200, cancellationToken).ConfigureAwait(false);
queryResult.EnsureNoErrors();
var times = (
from edge in queryResult.Data?.OwnerRepository?.Builds?.Edges
let node = edge?.Node
where node?.Status == BuildStatus.Completed
let p = new {start = FromTimestamp(node.BuildCreatedTimestamp), finish = GetFinishTime(node)}
where p.finish.HasValue
let ts = p.finish!.Value - p.start
orderby ts descending
select ts
).ToList();
if (times.Count <= 10)
return ProjectBuildStats.Defaults;
result = new()
{
Percentile95 = times[(int)(times.Count * 0.05)],
Percentile90 = times[(int)(times.Count * 0.10)],
Percentile85 = times[(int)(times.Count * 0.16)],
Percentile80 = times[(int)(times.Count * 0.20)],
Mean = TimeSpan.FromTicks(times.Select(t => t.Ticks).Mean()),
StdDev = TimeSpan.FromTicks((long)times.Select(t => t.Ticks).StdDev()),
BuildCount = times.Count,
};
BuildInfoCache.Set(cacheKey, result, TimeSpan.FromDays(1));
return result;
}
catch (Exception e)
{
ApiConfig.Log.Error(e, "Failed to get Cirrus build stats");
}
return ProjectBuildStats.Defaults;
}
private static DateTime? GetFinishTime(IBaseNodeInfo node)
=> node.LatestGroupTasks?
.Select(t => t?.FinalStatusTimestamp)
.Where(ts => ts > 0)
.ToList() is {Count: >0} finalTimes
? FromTimestamp(finalTimes.Max()!.Value)
: node.ClockDurationInSeconds > 0
? FromTimestamp(node.BuildCreatedTimestamp).AddSeconds(node.ClockDurationInSeconds.Value)
: (DateTime?)null;
[return: NotNullIfNotNull(nameof(DateTime))]
private static string? ToTimestamp(this DateTime? dateTime) => dateTime.HasValue ? (dateTime.Value.ToUniversalTime() - DateTime.UnixEpoch).TotalMilliseconds.ToString("0") : null;
private static DateTime FromTimestamp(long timestamp) => DateTime.UnixEpoch.AddMilliseconds(timestamp);
}