cirrus-ci client

This commit is contained in:
13xforever 2021-04-13 18:00:25 +05:00
parent c68678c740
commit 057a4763bb
17 changed files with 1228 additions and 16 deletions

18
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,18 @@
{
"version": 1,
"isRoot": true,
"tools": {
"strawberryshake.tools": {
"version": "11.1.0",
"commands": [
"dotnet-graphql"
]
},
"dotnet-ef": {
"version": "5.0.5",
"commands": [
"dotnet-ef"
]
}
}
}

3
.gitignore vendored
View File

@ -265,4 +265,5 @@ launchSettings.json
*.db-journal
logs/
*.ird
credentials.json
credentials.json
Generated/

View File

@ -0,0 +1,12 @@
{
"schema": "schema.graphql",
"documents": "**/*.graphql",
"extensions": {
"strawberryShake": {
"name": "Client",
"namespace": "CirrusCiClient",
"url": "https://api.cirrus-ci.com/graphql",
"dependencyInjection": true
}
}
}

View File

@ -0,0 +1,130 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
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?.GithubRepository?.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 startTime = FromTimestamp(node.BuildCreatedTimestamp);
var finishTime = GetFinishTime(node);
return new()
{
Commit = node.ChangeIdInRepo,
WindowsFilename = winArtifact?.Path is string wp ? Path.GetFileName(wp) : null,
LinuxFilename = linArtifact?.Path is string lp ? Path.GetFileName(lp) : null,
WindowsBuildDownloadLink = winTask?.Id is string wtid && winArtifact?.Path is string wtap ? $"https://api.cirrus-ci.com/v1/artifact/task/{wtid}/Artifact/{wtap}" : null,
LinuxBuildDownloadLink = linTask?.Id is string ltid && linArtifact?.Path is string ltap ? $"https://api.cirrus-ci.com/v1/artifact/task/{ltid}/Artifact/{ltap}" : null,
StartTime = startTime,
FinishTime = finishTime,
Status = node.Status,
};
}
return null;
}
public static async Task<ProjectBuildStats> GetPipelineDurationAsync(CancellationToken cancellationToken)
{
const string cacheKey = "project-build-stats";
if (BuildInfoCache.TryGetValue(cacheKey, out ProjectBuildStats result))
return result;
try
{
var queryResult = await Client.GetLastFewBuilds.ExecuteAsync(200, cancellationToken).ConfigureAwait(false);
queryResult.EnsureNoErrors();
var times = (
from edge in queryResult.Data?.GithubRepository?.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);
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CompatApiClient\CompatApiClient.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="StrawberryShake.CodeGeneration.CSharp.Analyzers" Version="11.1.0" />
<PackageReference Include="StrawberryShake.Transport.Http" Version="11.1.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,16 @@
using System;
namespace CirrusCiClient.POCOs
{
public record BuildInfo
{
public string? Commit { get; init; }
public string? WindowsFilename { get; init; }
public string? LinuxFilename { get; init; }
public string? WindowsBuildDownloadLink { get; init; }
public string? LinuxBuildDownloadLink { get; init; }
public DateTime StartTime { get; init; }
public DateTime? FinishTime { get; init; }
public BuildStatus? Status { get; init; }
}
}

View File

@ -0,0 +1,25 @@
using System;
namespace CirrusCiClient.POCOs
{
public record ProjectBuildStats
{
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 ProjectBuildStats Defaults = new()
{
Percentile95 = TimeSpan.FromSeconds(1120),
Percentile90 = TimeSpan.FromSeconds(900),
Percentile85 = TimeSpan.FromSeconds(870),
Percentile80 = TimeSpan.FromSeconds(865),
Mean = TimeSpan.FromSeconds(860),
StdDev = TimeSpan.FromSeconds(420),
};
}
}

View File

@ -0,0 +1,65 @@
query GetPrBuilds($branch: String, $after: String) {
githubRepository(owner: "RPCS3", name: "rpcs3") {
builds(branch: $branch, after: $after, last: 20) {
edges {
node {
id
changeIdInRepo
pullRequest
...BaseNodeInfo
tasks {
id
name
status
artifacts {
files {
path
size
}
}
}
}
}
}
}
}
query GetBuildWithArtifacts($buildId: ID!) {
build(id: $buildId) {
pullRequest
buildCreatedTimestamp
clockDurationInSeconds
tasks {
name
status
artifacts {
name
files {
path
size
}
}
}
}
}
query GetLastFewBuilds($count: Int!) {
githubRepository(owner: "RPCS3", name: "rpcs3") {
builds(last: $count) {
edges {
node {
...BaseNodeInfo
}
}
}
}
}
fragment BaseNodeInfo on Build {
status
buildCreatedTimestamp
clockDurationInSeconds
latestGroupTasks {
finalStatusTimestamp
}
}

View File

@ -0,0 +1 @@
extend schema @key(fields: "id")

View File

@ -0,0 +1,913 @@
schema {
query: Root
mutation: Mutation
subscription: Subscription
}
type AccountTransaction {
accountId: Long!
taskId: Long!
repositoryId: Long!
timestamp: Long!
microCreditsAmount: Long!
creditsAmount: String!
initialCreditsAmount: String
task: Task
repository: Repository
}
"An edge in a connection"
type AccountTransactionEdge {
"The item at the end of the edge"
node: AccountTransaction
"cursor marks a unique position or index into the connection"
cursor: String!
}
"A connection to a list of items."
type AccountTransactionsConnection {
"a list of edges"
edges: [AccountTransactionEdge]
"details about this specific page"
pageInfo: PageInfo!
}
type ApiAccessToken {
maskedToken: String
creationTimestamp: Long
}
type ArtifactFileInfo {
path: String!
size: Long!
}
type Artifacts {
name: String!
type: String
format: String
files: [ArtifactFileInfo]
}
type BillingSettings {
accountId: Long!
enabled: Boolean!
billingCreditsLimit: Long!
billingEmailAddress: String!
invoiceTemplate: String
}
input BillingSettingsInput {
accountId: ID!
enabled: Boolean!
billingEmailAddress: String!
invoiceTemplate: String
clientMutationId: String
}
type BillingSettingsPayload {
settings: BillingSettings
clientMutationId: String
}
type Build {
id: ID!
repositoryId: ID!
branch: String!
changeIdInRepo: String!
changeMessageTitle: String
changeMessage: String
durationInSeconds: Long
clockDurationInSeconds: Long
pullRequest: Long
checkSuiteId: Long
isSenderUserCollaborator: Boolean
senderUserPermissions: String
changeTimestamp: Long!
buildCreatedTimestamp: Long!
status: BuildStatus
notifications: [Notification]
parsingResult: ParsingResult
tasks: [Task]
taskGroupsAmount: Long
latestGroupTasks: [Task]
repository: Repository!
viewerPermission: PermissionType!
source: String
hooks: [Hook]
}
input BuildApproveInput {
buildId: ID!
clientMutationId: String
}
type BuildApprovePayload {
build: Build!
clientMutationId: String
}
input BuildReTriggerInput {
buildId: ID!
clientMutationId: String
}
type BuildReTriggerPayload {
build: Build!
clientMutationId: String
}
"Build status."
enum BuildStatus {
CREATED
NEEDS_APPROVAL
TRIGGERED
EXECUTING
FAILED
COMPLETED
ABORTED
ERRORED
}
input BuyComputeCreditsInput {
accountId: ID!
amountOfCredits: String!
paymentTokenId: String!
receiptEmail: String
clientMutationId: String
}
type BuyComputeCreditsPayload {
error: String
info: GitHubOrganizationInfo
user: User
clientMutationId: String
}
"Repository Setting to choose where to look for the configuration file."
enum ConfigResolutionStrategy {
SAME_SHA
MERGE_FOR_PRS
DEFAULT_BRANCH
}
input CreatePersistentWorkerPoolInput {
ownerId: Long!
name: String!
enabledForPublic: Boolean!
clientMutationId: String
}
type CreatePersistentWorkerPoolPayload {
pool: PersistentWorkerPool!
clientMutationId: String
}
type DayDate {
year: Int
month: Int
day: Int
}
"Repository Setting to choose how to decrypt variables."
enum DecryptEnvironmentVariablesFor {
USERS_WITH_WRITE_PERMISSIONS
EVERYONE
}
input DeletePersistentWorkerInput {
poolId: String!
name: String!
clientMutationId: String
}
type DeletePersistentWorkerPayload {
clientMutationId: String
}
input DeletePersistentWorkerPoolInput {
poolId: String!
clientMutationId: String
}
type DeletePersistentWorkerPoolPayload {
deletedPoolId: String!
clientMutationId: String
}
input DeleteWebPushConfigurationInput {
endpoint: String!
clientMutationId: String
}
type DeleteWebPushConfigurationPayload {
clientMutationId: String
}
type ExecutionChart {
startTimestamp: Long!
maxValue: Float!
minValue: Float!
points: [ExecutionChartPoint]!
}
type ExecutionChartPoint {
value: Float!
secondsFromStart: Long!
}
type ExecutionEvent {
timestamp: Long!
message: String!
}
type ExecutionInfo {
labels: [String]
events: [ExecutionEvent]
cpuChart: ExecutionChart
memoryChart: ExecutionChart
}
input GenerateNewAccessTokenInput {
accountId: ID
clientMutationId: String
}
type GenerateNewAccessTokenPayload {
token: String!
clientMutationId: String
}
input GetPersistentWorkerPoolRegistrationTokenInput {
poolId: ID
clientMutationId: String
}
type GetPersistentWorkerPoolRegistrationTokenPayload {
token: String!
clientMutationId: String
}
type GitHubMarketplacePurchase {
accountId: Long!
login: String!
planId: Long!
planName: String!
unitCount: Long!
onFreeTrial: Boolean!
freeTrialDaysLeft: Int!
}
type GitHubOrganizationInfo {
id: ID!
name: String!
role: String!
purchase: GitHubMarketplacePurchase
activeUsersAmount: Int!
activeUserNames: [String]
balanceInCredits: String!
transactions("fetching only nodes before this node (exclusive)" before: String "fetching only nodes after this node (exclusive)" after: String "fetching only the first certain number of nodes" first: Int "fetching only the last certain number of nodes" last: Int): AccountTransactionsConnection
billingSettings: BillingSettings
webhookSettings: WebHookSettings
webhookDeliveries("fetching only nodes before this node (exclusive)" before: String "fetching only nodes after this node (exclusive)" after: String "fetching only the first certain number of nodes" first: Int "fetching only the last certain number of nodes" last: Int): WebhookDeliveriesConnection
apiToken: ApiAccessToken
persistentWorkerPools: [PersistentWorkerPool]
}
type Hook {
id: ID!
repositoryId: ID!
repository: Repository!
buildId: ID
build: Build
taskId: ID
task: Task
timestamp: Long!
name: String!
info: HookExecutionInfo!
}
type HookExecutionInfo {
error: String!
arguments: String
result: String
outputLogs: [String]
durationNanos: Long!
}
type InstanceResources {
cpu: Float!
memory: Long!
}
"Long type"
scalar Long
type MetricsChart {
title: String
points: [TimePoint]
dataUnits: String
}
input MetricsQueryParameters {
status: TaskStatus
platform: PlatformType
type: String
isCommunity: Boolean
isPR: Boolean
usedComputeCredits: Boolean
branch: String
}
type Mutation {
securedVariable(input: RepositorySecuredVariableInput!): RepositorySecuredVariablePayload
securedOrganizationVariable(input: OrganizationSecuredVariableInput!): OrganizationSecuredVariablePayload
updateSecuredOrganizationVariable(input: UpdateOrganizationSecuredVariableInput!): UpdateOrganizationSecuredVariablePayload
createBuild(input: RepositoryCreateBuildInput!): RepositoryCreateBuildPayload
deleteRepository(input: RepositoryDeleteInput!): RepositoryDeletePayload
rerun(input: TaskReRunInput!): TaskReRunPayload
batchReRun(input: TasksReRunInput!): TasksReRunPayload
abortTask(input: TaskAbortInput!): TaskAbortPayload
batchAbort(input: TaskBatchAbortInput!): TaskBatchAbortPayload
retrigger(input: BuildReTriggerInput!): BuildReTriggerPayload
saveSettings(input: RepositorySettingsInput!): RepositorySettingsPayload
saveCronSettings(input: RepositorySaveCronSettingsInput!): RepositorySaveCronSettingsPayload
removeCronSettings(input: RepositoryRemoveCronSettingsInput!): RepositoryRemoveCronSettingsPayload
approve(input: BuildApproveInput!): BuildApprovePayload
trigger(input: TaskTriggerInput!): TaskTriggerPayload
buyComputeCredits(input: BuyComputeCreditsInput!): BuyComputeCreditsPayload
saveWebHookSettings(input: SaveWebHookSettingsInput!): SaveWebHookSettingsPayload
saveBillingSettings(input: BillingSettingsInput!): BillingSettingsPayload
generateNewAccessToken(input: GenerateNewAccessTokenInput!): GenerateNewAccessTokenPayload
deletePersistentWorker(input: DeletePersistentWorkerInput!): DeletePersistentWorkerPayload
updatePersistentWorker(input: UpdatePersistentWorkerInput!): UpdatePersistentWorkerPayload
persistentWorkerPoolRegistrationToken(input: GetPersistentWorkerPoolRegistrationTokenInput!): GetPersistentWorkerPoolRegistrationTokenPayload
createPersistentWorkerPool(input: CreatePersistentWorkerPoolInput!): CreatePersistentWorkerPoolPayload
updatePersistentWorkerPool(input: UpdatePersistentWorkerPoolInput!): UpdatePersistentWorkerPoolPayload
deletePersistentWorkerPool(input: DeletePersistentWorkerPoolInput!): DeletePersistentWorkerPoolPayload
saveWebPushConfiguration(input: SaveWebPushConfigurationInput!): SaveWebPushConfigurationPayload
deleteWebPushConfiguration(input: DeleteWebPushConfigurationInput!): DeleteWebPushConfigurationPayload
}
type Notification {
level: NotificationLevel
message: String!
link: String
}
"Notification level."
enum NotificationLevel {
INFO
WARNING
ERROR
}
input OrganizationSecuredVariableInput {
organizationId: ID!
valueToSecure: String!
clientMutationId: String
}
type OrganizationSecuredVariablePayload {
variableName: String!
clientMutationId: String
}
"Information about pagination in a connection."
type PageInfo {
"When paginating forwards, are there more items?"
hasNextPage: Boolean!
"When paginating backwards, are there more items?"
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String
"When paginating forwards, the cursor to continue."
endCursor: String
}
type ParsingResult {
rawYamlConfig: String!
rawStarlarkConfig: String!
processedYamlConfig: String!
issues: [ParsingResultIssue]
outputLogs: [String]
}
type ParsingResultIssue {
level: ParsingResultIssueLevel!
message: String!
line: Long!
column: Long!
}
enum ParsingResultIssueLevel {
INFO
WARNING
ERROR
}
"User access level."
enum PermissionType {
NONE
READ
WRITE
ADMIN
}
type PersistentWorker {
name: String!
disabled: Boolean!
arch: String!
hostname: String!
os: String!
version: String!
labels: [String]
info: PersistentWorkerInfo
assignedTasks("fetching only nodes before this node (exclusive)" before: String "fetching only nodes after this node (exclusive)" after: String "fetching only the first certain number of nodes" first: Int "fetching only the last certain number of nodes" last: Int): PersistentWorkerAssignedTasksConnection
}
"An edge in a connection"
type PersistentWorkerAssignedTaskEdge {
"The item at the end of the edge"
node: Task
"cursor marks a unique position or index into the connection"
cursor: String!
}
"A connection to a list of items."
type PersistentWorkerAssignedTasksConnection {
"a list of edges"
edges: [PersistentWorkerAssignedTaskEdge]
"details about this specific page"
pageInfo: PageInfo!
}
type PersistentWorkerInfo {
heartbeatTimestamp: Long!
runningTasks: [Task]
}
type PersistentWorkerPool {
id: ID!
name: String!
enabledForPublic: Boolean!
workers: [PersistentWorker]
viewerPermission: PermissionType
}
"Task platform."
enum PlatformType {
LINUX
DARWIN
WINDOWS
FREEBSD
}
type Repository {
id: ID!
owner: String!
name: String!
cloneUrl: String!
masterBranch: String!
isPrivate: Boolean!
builds("fetching only nodes before this node (exclusive)" before: String "fetching only nodes after this node (exclusive)" after: String "fetching only the first certain number of nodes" first: Int "fetching only the last certain number of nodes" last: Int "branch to fetch builds for" branch: String): RepositoryBuildsConnection
settings: RepositorySettings
cronSettings: [RepositoryCronSettings]
viewerPermission: PermissionType
lastDefaultBranchBuild: Build
metrics(parameters: MetricsQueryParameters): [MetricsChart]
}
"An edge in a connection"
type RepositoryBuildEdge {
"The item at the end of the edge"
node: Build
"cursor marks a unique position or index into the connection"
cursor: String!
}
"A connection to a list of items."
type RepositoryBuildsConnection {
"a list of edges"
edges: [RepositoryBuildEdge]
"details about this specific page"
pageInfo: PageInfo!
}
input RepositoryCreateBuildInput {
repositoryId: ID!
branch: String!
sha: String
configOverride: String
clientMutationId: String
}
type RepositoryCreateBuildPayload {
build: Build!
clientMutationId: String
}
type RepositoryCronSettings {
name: String!
expression: String!
branch: String!
nextInvocationTimestamp: Long!
lastInvocationBuild: Build
}
input RepositoryDeleteInput {
repositoryId: ID!
clientMutationId: String
}
type RepositoryDeletePayload {
deleted: Boolean!
clientMutationId: String
}
input RepositoryRemoveCronSettingsInput {
repositoryId: ID!
name: String!
clientMutationId: String
}
type RepositoryRemoveCronSettingsPayload {
settings: [RepositoryCronSettings]
clientMutationId: String
}
input RepositorySaveCronSettingsInput {
repositoryId: ID!
name: String!
expression: String!
branch: String!
clientMutationId: String
}
type RepositorySaveCronSettingsPayload {
settings: [RepositoryCronSettings]
clientMutationId: String
}
input RepositorySecuredVariableInput {
repositoryId: ID!
valueToSecure: String!
clientMutationId: String
}
type RepositorySecuredVariablePayload {
variableName: String!
clientMutationId: String
}
type RepositorySettings {
needsApproval: Boolean
decryptEnvironmentVariables: DecryptEnvironmentVariablesFor
configResolutionStrategy: ConfigResolutionStrategy
additionalEnvironment: [String]
}
input RepositorySettingsInput {
repositoryId: ID!
needsApproval: Boolean
decryptEnvironmentVariables: DecryptEnvironmentVariablesFor
configResolutionStrategy: ConfigResolutionStrategy
additionalEnvironment: [String]
clientMutationId: String
}
type RepositorySettingsPayload {
settings: RepositorySettings!
clientMutationId: String
}
type Root {
viewer: User
repository(id: ID!): Repository
githubRepository(owner: String! name: String!): Repository
githubRepositories(owner: String!): [Repository]
githubOrganizationInfo(organization: String!): GitHubOrganizationInfo
build(id: ID!): Build
searchBuilds(repositoryOwner: String! repositoryName: String! SHA: String): [Build]
task(id: ID!): Task
hook(id: ID!): Hook
webhookDelivery(id: String!): WebHookDelivery
persistentWorkerPool(poolId: ID): PersistentWorkerPool
persistentWorker(poolId: ID name: String): PersistentWorker
}
input SaveWebHookSettingsInput {
accountId: ID!
webhookURL: String!
clientMutationId: String
}
type SaveWebHookSettingsPayload {
error: String
info: GitHubOrganizationInfo
clientMutationId: String
}
input SaveWebPushConfigurationInput {
endpoint: String!
p256dhKey: String!
authKey: String!
clientMutationId: String
}
type SaveWebPushConfigurationPayload {
clientMutationId: String
}
type Subscription {
task(id: ID!): Task
build(id: ID!): Build
repository(id: ID!): Repository
}
type Task {
id: ID!
buildId: ID!
repositoryId: ID!
name: String!
localGroupId: Long!
requiredGroups: [Long]
status: TaskStatus
notifications: [Notification]
commands: [TaskCommand]
firstFailedCommand: TaskCommand
artifacts: [Artifacts]
commandLogsTail(name: String!): [String]
statusTimestamp: Long!
creationTimestamp: Long!
scheduledTimestamp: Long!
executingTimestamp: Long!
finalStatusTimestamp: Long!
durationInSeconds: Long!
labels: [String]
uniqueLabels: [String]
requiredPRLabels: [String]
timeoutInSeconds: Long!
optional: Boolean
statusDurations: [TaskStatusDuration]
repository: Repository!
build: Build!
previousRuns: [Task]
allOtherRuns: [Task]
dependencies: [Task]
automaticReRun: Boolean!
automaticallyReRunnable: Boolean!
experimental: Boolean!
stateful: Boolean!
useComputeCredits: Boolean!
usedComputeCredits: Boolean!
transaction: AccountTransaction
triggerType: TaskTriggerType!
instanceResources: InstanceResources
executionInfo: ExecutionInfo
baseEnvironment: [String]
hooks: [Hook]
}
input TaskAbortInput {
taskId: ID!
clientMutationId: String
}
type TaskAbortPayload {
abortedTask: Task!
clientMutationId: String
}
input TaskBatchAbortInput {
taskIds: [ID]
clientMutationId: String
}
type TaskBatchAbortPayload {
tasks: [Task]
clientMutationId: String
}
type TaskCommand {
name: String
type: TaskCommandType
status: TaskCommandStatus
durationInSeconds: Int
logsTail: [String]
}
"Task Command status."
enum TaskCommandStatus {
UNDEFINED
SUCCESS
FAILURE
EXECUTING
SKIPPED
ABORTED
}
"Task Command type."
enum TaskCommandType {
WAIT
EXIT
EXECUTE_SCRIPT
CACHE
UPLOAD_CACHE
CLONE
EXECUTE_BACKGROUND_SCRIPT
ARTIFACTS
}
input TaskReRunInput {
taskId: ID!
clientMutationId: String
}
type TaskReRunPayload {
newTask: Task!
clientMutationId: String
}
"Task status."
enum TaskStatus {
CREATED
TRIGGERED
SCHEDULED
EXECUTING
ABORTED
FAILED
COMPLETED
SKIPPED
PAUSED
}
type TaskStatusDuration {
status: TaskStatus!
durationInSeconds: Long!
}
input TaskTriggerInput {
taskId: ID!
clientMutationId: String
}
type TaskTriggerPayload {
task: Task!
clientMutationId: String
}
"Task trigger type."
enum TaskTriggerType {
AUTOMATIC
MANUAL
}
input TasksReRunInput {
taskIds: [ID]
clientMutationId: String
}
type TasksReRunPayload {
newTasks: [Task]
clientMutationId: String
}
type TimePoint {
date: DayDate
value: Float
}
input UpdateOrganizationSecuredVariableInput {
organizationId: ID!
name: String!
updatedValueToSecure: String!
clientMutationId: String
}
type UpdateOrganizationSecuredVariablePayload {
variableName: String!
clientMutationId: String
}
input UpdatePersistentWorkerInput {
poolId: String!
name: String!
disabled: Boolean!
clientMutationId: String
}
type UpdatePersistentWorkerPayload {
worker: PersistentWorker!
clientMutationId: String
}
input UpdatePersistentWorkerPoolInput {
poolId: String!
name: String!
enabledForPublic: Boolean!
clientMutationId: String
}
type UpdatePersistentWorkerPoolPayload {
pool: PersistentWorkerPool!
clientMutationId: String
}
type User {
id: ID!
githubUserId: Long
githubUserName: String!
category: UserCategoryType!
avatarURL: String
builds("fetching only nodes before this node (exclusive)" before: String "fetching only nodes after this node (exclusive)" after: String "fetching only the first certain number of nodes" first: Int "fetching only the last certain number of nodes" last: Int): UserBuildsConnection
githubMarketplacePurchase: GitHubMarketplacePurchase
topActiveRepositories: [Repository]
organizations: [GitHubOrganizationInfo]
balanceInCredits: String!
transactions("fetching only nodes before this node (exclusive)" before: String "fetching only nodes after this node (exclusive)" after: String "fetching only the first certain number of nodes" first: Int "fetching only the last certain number of nodes" last: Int): UserTransactionsConnection
apiToken: ApiAccessToken
webPushServerKey: String!
persistentWorkerPools: [PersistentWorkerPool]
}
"An edge in a connection"
type UserBuildEdge {
"The item at the end of the edge"
node: Build
"cursor marks a unique position or index into the connection"
cursor: String!
}
"A connection to a list of items."
type UserBuildsConnection {
"a list of edges"
edges: [UserBuildEdge]
"details about this specific page"
pageInfo: PageInfo!
}
"User type."
enum UserCategoryType {
DEFAULT
BLOCKED
TRUSTED
ADMIN
}
"An edge in a connection"
type UserTransactionEdge {
"The item at the end of the edge"
node: AccountTransaction
"cursor marks a unique position or index into the connection"
cursor: String!
}
"A connection to a list of items."
type UserTransactionsConnection {
"a list of edges"
edges: [UserTransactionEdge]
"details about this specific page"
pageInfo: PageInfo!
}
type WebHookDelivery {
id: String!
accountId: Long!
repositoryId: Long!
timestamp: Long!
payload: WebHookDeliveryPayload!
response: WebHookDeliveryResponse!
}
"An edge in a connection"
type WebHookDeliveryEdge {
"The item at the end of the edge"
node: WebHookDelivery
"cursor marks a unique position or index into the connection"
cursor: String!
}
type WebHookDeliveryPayload {
event: String!
action: String!
data: String!
}
type WebHookDeliveryResponse {
status: Int!
duration: Long!
data: String!
}
type WebHookSettings {
webhookURL: String
}
"A connection to a list of items."
type WebhookDeliveriesConnection {
"a list of edges"
edges: [WebHookDeliveryEdge]
"details about this specific page"
pageInfo: PageInfo!
}

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace CompatBot.Utils
namespace CompatApiClient.Utils
{
public static class Statistics
{

View File

@ -33,7 +33,7 @@ namespace GithubClient
public Client()
{
client = HttpClientFactory.Create(new CompressionMessageHandler());
jsonOptions = new JsonSerializerOptions
jsonOptions = new()
{
PropertyNamingPolicy = SpecialJsonNamingPolicy.SnakeCase,
IgnoreNullValues = true,
@ -72,7 +72,7 @@ namespace GithubClient
if (result == null)
{
ApiConfig.Log.Debug($"Failed to get {nameof(PrInfo)}, returning empty result");
return new PrInfo { Number = pr };
return new() { Number = pr };
}
StatusesCache.Set(pr, result, PrStatusCacheTime);
@ -111,7 +111,7 @@ namespace GithubClient
if (result == null)
{
ApiConfig.Log.Debug($"Failed to get {nameof(IssueInfo)}, returning empty result");
return new IssueInfo { Number = issue };
return new() { Number = issue };
}
IssuesCache.Set(issue, result, IssueStatusCacheTime);

View File

@ -2,6 +2,7 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CirrusCiClient;
using CompatApiClient.Utils;
using CompatBot.Commands.Attributes;
using CompatBot.Utils;
@ -12,6 +13,7 @@ using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.Entities;
using Microsoft.TeamFoundation.Build.WebApi;
using BuildStatus = Microsoft.TeamFoundation.Build.WebApi.BuildStatus;
namespace CompatBot.Commands
{
@ -127,12 +129,12 @@ namespace CompatBot.Commands
string? linuxDownloadText = null;
string? buildTime = null;
if (azureClient != null && prInfo.Head?.Sha is string commit)
if (prInfo.Head?.Sha is string commit)
try
{
windowsDownloadText = "⏳ Pending...";
linuxDownloadText = "⏳ Pending...";
var latestBuild = await azureClient.GetPrBuildInfoAsync(commit, prInfo.MergedAt, pr, Config.Cts.Token).ConfigureAwait(false);
var latestBuild = await CirrusCi.GetPrBuildInfoAsync(commit, prInfo.MergedAt, pr, Config.Cts.Token).ConfigureAwait(false);
if (latestBuild == null)
{
if (state == "Open")
@ -143,18 +145,16 @@ namespace CompatBot.Commands
else
{
bool shouldHaveArtifacts = false;
if (latestBuild.Status == BuildStatus.Completed
&& (latestBuild.Result == BuildResult.Succeeded || latestBuild.Result == BuildResult.PartiallySucceeded)
&& latestBuild.FinishTime.HasValue)
if (latestBuild is {Status: CirrusCiClient.BuildStatus.Completed, FinishTime: not null})
{
buildTime = $"Built on {latestBuild.FinishTime:u} ({(DateTime.UtcNow - latestBuild.FinishTime.Value).AsTimeDeltaDescription()} ago)";
shouldHaveArtifacts = true;
}
else if (latestBuild.Result == BuildResult.Failed || latestBuild.Result == BuildResult.Canceled)
windowsDownloadText = $"❌ {latestBuild.Result}";
else if (latestBuild.Status == BuildStatus.InProgress && latestBuild.StartTime != null)
else if (latestBuild.Status is CirrusCiClient.BuildStatus.Failed or CirrusCiClient.BuildStatus.Errored or CirrusCiClient.BuildStatus.Aborted)
windowsDownloadText = $"❌ {latestBuild.Status}";
else if (latestBuild is {Status: CirrusCiClient.BuildStatus.Executing})
{
var estimatedCompletionTime = latestBuild.StartTime.Value + (await azureClient.GetPipelineDurationAsync(Config.Cts.Token).ConfigureAwait(false)).Mean;
var estimatedCompletionTime = latestBuild.StartTime + (await CirrusCi.GetPipelineDurationAsync(Config.Cts.Token).ConfigureAwait(false)).Mean;
var estimatedTime = TimeSpan.FromMinutes(1);
if (estimatedCompletionTime > DateTime.UtcNow)
estimatedTime = estimatedCompletionTime - DateTime.UtcNow;
@ -202,7 +202,7 @@ namespace CompatBot.Commands
if (!string.IsNullOrEmpty(buildTime))
embed.WithFooter(buildTime);
}
else if (state == "Merged")
else if (state == "Merged" && azureClient is not null)
{
var mergeTime = prInfo.MergedAt.GetValueOrDefault();
var now = DateTime.UtcNow;

View File

@ -86,6 +86,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Clients\CirrusCiClient\CirrusCiClient.csproj" />
<ProjectReference Include="..\SourceGenerators\SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Clients\CompatApiClient\CompatApiClient.csproj" />
<ProjectReference Include="..\Clients\GithubClient\GithubClient.csproj" />

View File

@ -6,6 +6,7 @@ using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CirrusCiClient;
using CompatBot.Commands;
using CompatBot.Commands.Converters;
using CompatBot.Database;
@ -113,7 +114,8 @@ namespace CompatBot
#endif
StatsStorage.BackgroundSaveAsync(),
CompatList.ImportCompatListAsync(),
Config.GetAzureDevOpsClient().GetPipelineDurationAsync(Config.Cts.Token)
Config.GetAzureDevOpsClient().GetPipelineDurationAsync(Config.Cts.Token),
CirrusCi.GetPipelineDurationAsync(Config.Cts.Token)
);
try

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CompatApiClient.Utils;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.TeamFoundation.Build.WebApi;
using SharpCompress.Readers;

View File

@ -40,6 +40,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YandexDiskClient", "Clients
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGenerators", "SourceGenerators\SourceGenerators.csproj", "{1A75FAF1-1DD1-43FF-A789-1AB216F4B94E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CirrusCiClient", "Clients\CirrusCiClient\CirrusCiClient.csproj", "{897476B0-B80A-4134-A576-8CAEAEA14A28}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -89,6 +91,10 @@ Global
{1A75FAF1-1DD1-43FF-A789-1AB216F4B94E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A75FAF1-1DD1-43FF-A789-1AB216F4B94E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A75FAF1-1DD1-43FF-A789-1AB216F4B94E}.Release|Any CPU.Build.0 = Release|Any CPU
{897476B0-B80A-4134-A576-8CAEAEA14A28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{897476B0-B80A-4134-A576-8CAEAEA14A28}.Debug|Any CPU.Build.0 = Debug|Any CPU
{897476B0-B80A-4134-A576-8CAEAEA14A28}.Release|Any CPU.ActiveCfg = Release|Any CPU
{897476B0-B80A-4134-A576-8CAEAEA14A28}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -101,6 +107,7 @@ Global
{5C4BCF33-2EC6-455F-B026-8A0001B7B7AD} = {E7FE0ADD-CBA6-4321-8A1C-0A3B5C3F54C2}
{1F743D3D-4A87-47EF-B88D-A0DCEE1C5FB7} = {E7FE0ADD-CBA6-4321-8A1C-0A3B5C3F54C2}
{CABC3E5E-2153-443B-A5A8-DA3E389359EC} = {E7FE0ADD-CBA6-4321-8A1C-0A3B5C3F54C2}
{897476B0-B80A-4134-A576-8CAEAEA14A28} = {E7FE0ADD-CBA6-4321-8A1C-0A3B5C3F54C2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D7696F56-AEAC-4D83-9BD8-BE0C122A5DCE}