mirror of
https://github.com/RPCS3/discord-bot.git
synced 2026-01-31 01:25:22 +01:00
326 lines
15 KiB
C#
326 lines
15 KiB
C#
using System.IO;
|
|
using System.Net.Http;
|
|
using CompatApiClient.Utils;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.TeamFoundation.Build.WebApi;
|
|
using SharpCompress.Readers;
|
|
|
|
namespace CompatBot.Utils.Extensions;
|
|
|
|
internal static class AzureDevOpsClientExtensions
|
|
{
|
|
private static readonly MemoryCache BuildInfoCache = new(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromHours(1) });
|
|
|
|
private const string RepoId = "RPCS3/rpcs3";
|
|
private const string RepoType = "GitHub";
|
|
|
|
public record BuildInfo
|
|
{
|
|
public string? Commit { get; init; }
|
|
|
|
public string? WindowsFilename { get; init; }
|
|
public string? LinuxFilename { get; init; }
|
|
public string? MacFilename { get; init; }
|
|
public string? LinuxArmFilename { get; init; }
|
|
public string? MacArmFilename { get; init; }
|
|
|
|
public string? WindowsBuildDownloadLink { get; init; }
|
|
public string? LinuxBuildDownloadLink { get; init; }
|
|
public string? MacBuildDownloadLink { get; init; }
|
|
public string? LinuxArmBuildDownloadLink { get; init; }
|
|
public string? MacArmBuildDownloadLink { get; init; }
|
|
|
|
public DateTime? StartTime { get; init; }
|
|
public DateTime? FinishTime { get; init; }
|
|
public BuildStatus? Status { get; init; }
|
|
public BuildResult? Result { get; init; }
|
|
}
|
|
|
|
public record PipelineStats
|
|
{
|
|
public TimeSpan Percentile95 { get; init; }
|
|
public TimeSpan Percentile90 { get; init; }
|
|
public TimeSpan Percentile85 { get; init; }
|
|
public TimeSpan Percentile80 { get; init; }
|
|
public TimeSpan Mean { get; init; }
|
|
public TimeSpan StdDev { get; init; }
|
|
public int BuildCount { get; init; }
|
|
|
|
public static readonly PipelineStats Defaults = new()
|
|
{
|
|
Percentile95 = TimeSpan.FromMinutes(60.82984169),
|
|
Percentile90 = TimeSpan.FromMinutes(59.145449178333337),
|
|
Percentile85 = TimeSpan.FromMinutes(57.569420649999998),
|
|
Percentile80 = TimeSpan.FromMinutes(57.033788216666665),
|
|
Mean = TimeSpan.FromMinutes(50.7788),
|
|
StdDev = TimeSpan.FromMinutes(7.376),
|
|
};
|
|
}
|
|
|
|
public static async Task<PipelineStats> GetPipelineDurationAsync(this BuildHttpClient? azureDevOpsClient, CancellationToken cancellationToken)
|
|
{
|
|
const string cacheKey = "pipeline-duration";
|
|
if (BuildInfoCache.TryGetValue(cacheKey, out PipelineStats? result) && result is not null)
|
|
return result;
|
|
|
|
if (azureDevOpsClient is null)
|
|
return PipelineStats.Defaults;
|
|
|
|
var builds = await azureDevOpsClient.GetBuildsAsync(
|
|
Config.AzureDevOpsProjectId,
|
|
repositoryId: RepoId,
|
|
repositoryType: RepoType,
|
|
statusFilter: BuildStatus.Completed,
|
|
resultFilter: BuildResult.Succeeded,
|
|
minFinishTime: DateTime.UtcNow.AddDays(-7),
|
|
cancellationToken: cancellationToken
|
|
).ConfigureAwait(false);
|
|
|
|
var times = builds
|
|
.Where(b => b is {StartTime: not null, FinishTime: not null})
|
|
.Select(b => (b.FinishTime - b.StartTime)!.Value)
|
|
.OrderByDescending(t => t)
|
|
.ToList();
|
|
if (times.Count <= 10)
|
|
return PipelineStats.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;
|
|
}
|
|
|
|
public static async ValueTask<List<BuildInfo>?> GetMasterBuildsAsync(this BuildHttpClient? azureDevOpsClient, string? oldestMergeCommit, string? newestMergeCommit, DateTime? oldestTimestamp, CancellationToken cancellationToken)
|
|
{
|
|
if (azureDevOpsClient == null || string.IsNullOrEmpty(oldestMergeCommit) || string.IsNullOrEmpty(newestMergeCommit))
|
|
return null;
|
|
|
|
oldestMergeCommit = oldestMergeCommit.ToLower();
|
|
newestMergeCommit = newestMergeCommit.ToLower();
|
|
var builds = await azureDevOpsClient.GetBuildsAsync(
|
|
Config.AzureDevOpsProjectId,
|
|
repositoryId: RepoId,
|
|
repositoryType: RepoType,
|
|
reasonFilter: BuildReason.IndividualCI,
|
|
minFinishTime: oldestTimestamp,
|
|
cancellationToken: cancellationToken
|
|
).ConfigureAwait(false);
|
|
builds = builds
|
|
.Where(b => b.SourceBranch == "refs/heads/master" && b.Status is BuildStatus.Completed)
|
|
.OrderByDescending(b => b.StartTime)
|
|
.ToList();
|
|
builds = builds
|
|
.SkipWhile(b => !newestMergeCommit.Equals(b.SourceVersion, StringComparison.InvariantCultureIgnoreCase))
|
|
.Skip(1)
|
|
.TakeWhile(b => !oldestMergeCommit.Equals(b.SourceVersion, StringComparison.InvariantCultureIgnoreCase))
|
|
.ToList();
|
|
return await builds
|
|
.ToAsyncEnumerable()
|
|
.SelectAwait(async b => await azureDevOpsClient.GetArtifactsInfoAsync(b.SourceVersion, b, cancellationToken).ConfigureAwait(false))
|
|
.ToListAsync(cancellationToken: cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
public static async ValueTask<BuildInfo?> GetMasterBuildInfoAsync(this BuildHttpClient? azureDevOpsClient, string? commit, DateTime? oldestTimestamp, CancellationToken cancellationToken)
|
|
{
|
|
if (azureDevOpsClient == null || string.IsNullOrEmpty(commit))
|
|
return null;
|
|
|
|
commit = commit.ToLower();
|
|
if (BuildInfoCache.TryGetValue(commit, out BuildInfo? result) && result is not null)
|
|
return result;
|
|
|
|
var builds = await azureDevOpsClient.GetBuildsAsync(
|
|
Config.AzureDevOpsProjectId,
|
|
repositoryId: RepoId,
|
|
repositoryType: RepoType,
|
|
reasonFilter: BuildReason.IndividualCI,
|
|
minFinishTime: oldestTimestamp,
|
|
cancellationToken: cancellationToken
|
|
).ConfigureAwait(false);
|
|
builds = builds
|
|
.Where(b => b.SourceBranch == "refs/heads/master"
|
|
&& commit.Equals(b.SourceVersion, StringComparison.InvariantCultureIgnoreCase)
|
|
&& b.Status == BuildStatus.Completed
|
|
)
|
|
.OrderByDescending(b => b.StartTime)
|
|
.ToList();
|
|
var latestBuild = builds.FirstOrDefault();
|
|
if (latestBuild == null)
|
|
return null;
|
|
|
|
result = await azureDevOpsClient.GetArtifactsInfoAsync(commit, latestBuild, cancellationToken).ConfigureAwait(false);
|
|
if (result.Status == BuildStatus.Completed && (result.Result == BuildResult.Succeeded || result.Result == BuildResult.PartiallySucceeded))
|
|
BuildInfoCache.Set(commit, result, TimeSpan.FromHours(1));
|
|
return result;
|
|
}
|
|
|
|
public static async ValueTask<BuildInfo?> GetPrBuildInfoAsync(this BuildHttpClient? azureDevOpsClient, string? commit, DateTime? oldestTimestamp, int pr, CancellationToken cancellationToken)
|
|
{
|
|
if (azureDevOpsClient == null || string.IsNullOrEmpty(commit))
|
|
return null;
|
|
|
|
commit = commit.ToLower();
|
|
if (BuildInfoCache.TryGetValue(commit, out BuildInfo? result) && result is not null)
|
|
return result;
|
|
|
|
var builds = await azureDevOpsClient.GetBuildsAsync(
|
|
Config.AzureDevOpsProjectId,
|
|
repositoryId: RepoId,
|
|
repositoryType: RepoType,
|
|
reasonFilter: BuildReason.PullRequest,
|
|
minFinishTime: oldestTimestamp,
|
|
cancellationToken: cancellationToken
|
|
).ConfigureAwait(false);
|
|
var filterBranch = $"refs/pull/{pr}/merge";
|
|
builds = builds
|
|
.Where(b => b.SourceBranch == filterBranch
|
|
&& b.TriggerInfo.TryGetValue("pr.sourceSha", out var trc)
|
|
&& trc.Equals(commit, StringComparison.InvariantCultureIgnoreCase))
|
|
.OrderByDescending(b => b.StartTime)
|
|
.ToList();
|
|
var latestBuild = builds.FirstOrDefault();
|
|
if (latestBuild == null)
|
|
return null;
|
|
|
|
result = await azureDevOpsClient.GetArtifactsInfoAsync(commit, latestBuild, cancellationToken).ConfigureAwait(false);
|
|
if (result is { Status: BuildStatus.Completed, Result: BuildResult.Succeeded or BuildResult.PartiallySucceeded })
|
|
BuildInfoCache.Set(commit, result, TimeSpan.FromHours(1));
|
|
return result;
|
|
}
|
|
|
|
public static async ValueTask<BuildInfo> GetArtifactsInfoAsync(this BuildHttpClient azureDevOpsClient, string commit, Build build, CancellationToken cancellationToken)
|
|
{
|
|
var result = new BuildInfo
|
|
{
|
|
Commit = commit,
|
|
StartTime = build.StartTime,
|
|
FinishTime = build.FinishTime,
|
|
Status = build.Status,
|
|
Result = build.Result,
|
|
};
|
|
var artifacts = await azureDevOpsClient.GetArtifactsAsync(Config.AzureDevOpsProjectId, build.Id, cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
// windows build
|
|
var windowsBuildArtifact = artifacts.FirstOrDefault(a => a.Name.Contains("Windows"));
|
|
var windowsBuild = windowsBuildArtifact?.Resource;
|
|
if (windowsBuild?.DownloadUrl is string winDownloadUrl)
|
|
{
|
|
result = result with {WindowsBuildDownloadLink = winDownloadUrl};
|
|
if (windowsBuild.DownloadUrl.Contains("format=zip", StringComparison.InvariantCultureIgnoreCase))
|
|
try
|
|
{
|
|
using var httpClient = HttpClientFactory.Create();
|
|
await using var stream = await httpClient.GetStreamAsync(winDownloadUrl, cancellationToken).ConfigureAwait(false);
|
|
using var zipStream = ReaderFactory.Open(stream);
|
|
while (zipStream.MoveToNextEntry() && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
if (zipStream.Entry.Key?.EndsWith(".7z", StringComparison.InvariantCultureIgnoreCase) is true)
|
|
{
|
|
result = result with {WindowsFilename = Path.GetFileName(zipStream.Entry.Key)};
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e2)
|
|
{
|
|
Config.Log.Error(e2, "Failed to get windows build filename");
|
|
}
|
|
}
|
|
|
|
// linux build
|
|
var linuxBuildArtifact = artifacts.FirstOrDefault(a => a.Name.EndsWith(".GCC")
|
|
|| a.Name.EndsWith("Linux")
|
|
|| a.Name.EndsWith("(gcc)")
|
|
|| a.Name.EndsWith("(clang)"));
|
|
var linuxBuild = linuxBuildArtifact?.Resource;
|
|
if (linuxBuild?.DownloadUrl is string linDownloadUrl)
|
|
{
|
|
result = result with {LinuxBuildDownloadLink = linDownloadUrl};
|
|
if (linuxBuild.DownloadUrl.Contains("format=zip", StringComparison.InvariantCultureIgnoreCase))
|
|
try
|
|
{
|
|
using var httpClient = HttpClientFactory.Create();
|
|
await using var stream = await httpClient.GetStreamAsync(linDownloadUrl, cancellationToken).ConfigureAwait(false);
|
|
using var zipStream = ReaderFactory.Open(stream);
|
|
while (zipStream.MoveToNextEntry() && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
if (zipStream.Entry.Key?.EndsWith(".AppImage", StringComparison.InvariantCultureIgnoreCase) is true)
|
|
{
|
|
result = result with {LinuxFilename = Path.GetFileName(zipStream.Entry.Key)};
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e2)
|
|
{
|
|
Config.Log.Error(e2, "Failed to get linux build filename");
|
|
}
|
|
}
|
|
|
|
// mac build
|
|
var macBuildArtifact = artifacts.FirstOrDefault(a => a.Name.Contains("Mac") && a.Name.Contains("Intel"));
|
|
var macBuild = macBuildArtifact?.Resource;
|
|
if (macBuild?.DownloadUrl is string macDownloadUrl)
|
|
{
|
|
result = result with { MacBuildDownloadLink = macDownloadUrl };
|
|
if (macBuild.DownloadUrl.Contains("format=zip", StringComparison.InvariantCultureIgnoreCase))
|
|
try
|
|
{
|
|
using var httpClient = HttpClientFactory.Create();
|
|
await using var stream = await httpClient.GetStreamAsync(macDownloadUrl, cancellationToken).ConfigureAwait(false);
|
|
using var zipStream = ReaderFactory.Open(stream);
|
|
while (zipStream.MoveToNextEntry() && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
if (zipStream.Entry.Key?.EndsWith(".dmg", StringComparison.InvariantCultureIgnoreCase) is true
|
|
|| zipStream.Entry.Key?.EndsWith(".7z", StringComparison.InvariantCultureIgnoreCase) is true)
|
|
{
|
|
result = result with { MacFilename = Path.GetFileName(zipStream.Entry.Key) };
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e2)
|
|
{
|
|
Config.Log.Error(e2, "Failed to get mac build filename");
|
|
}
|
|
}
|
|
|
|
// mac arm build
|
|
var macArmBuildArtifact = artifacts.FirstOrDefault(a => a.Name.Contains("Mac") && a.Name.Contains("Apple"));
|
|
var macArmBuild = macArmBuildArtifact?.Resource;
|
|
if (macArmBuild?.DownloadUrl is string macArmDownloadUrl)
|
|
{
|
|
result = result with { MacArmBuildDownloadLink = macArmDownloadUrl };
|
|
if (macArmBuild.DownloadUrl.Contains("format=zip", StringComparison.InvariantCultureIgnoreCase))
|
|
try
|
|
{
|
|
using var httpClient = HttpClientFactory.Create();
|
|
await using var stream = await httpClient.GetStreamAsync(macArmDownloadUrl, cancellationToken).ConfigureAwait(false);
|
|
using var zipStream = ReaderFactory.Open(stream);
|
|
while (zipStream.MoveToNextEntry() && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
if (zipStream.Entry.Key?.EndsWith(".dmg", StringComparison.InvariantCultureIgnoreCase) is true
|
|
|| zipStream.Entry.Key?.EndsWith(".7z", StringComparison.InvariantCultureIgnoreCase) is true)
|
|
{
|
|
result = result with { MacArmFilename = Path.GetFileName(zipStream.Entry.Key) };
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e2)
|
|
{
|
|
Config.Log.Error(e2, "Failed to get mac arm build filename");
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
} |