diff --git a/.graphqlrc.json b/.graphqlrc.json new file mode 100644 index 0000000000..753dd76ce7 --- /dev/null +++ b/.graphqlrc.json @@ -0,0 +1,20 @@ +{ + "projects": [ + { + "schema": [ + "src/Tgstation.Server.Client.GraphQL/schema.graphql" + ], + "documents": [ + "src/Tgstation.Server.Client.GraphQL/**/*.graphql" + ] + }, + { + "schema": [ + "src/Tgstation.Server.Host.Utils.GitLab.GraphQL/schema.graphql" + ], + "documents": [ + "src/Tgstation.Server.Host.Utils.GitLab.GraphQL/**/*.graphql" + ] + } + ] +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Mutations/CreateNote.graphql b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Mutations/CreateNote.graphql new file mode 100644 index 0000000000..53f7647942 --- /dev/null +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Mutations/CreateNote.graphql @@ -0,0 +1,12 @@ +mutation CreateNote($id: NoteableID!, $body: String!) { + createNote(input: { noteableId: $id, body: $body }) { + note { + id + body + discussion { + id + } + } + errors + } +} diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Mutations/ModifyNote.graphql b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Mutations/ModifyNote.graphql new file mode 100644 index 0000000000..827bf550c0 --- /dev/null +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Mutations/ModifyNote.graphql @@ -0,0 +1,9 @@ +mutation ModifyNote($id: NoteID!, $body: String!) { + updateNote(input: { id: $id, body: $body }) { + note { + id + body + } + errors + } +} diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetCurrentUser.graphql b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetCurrentUser.graphql new file mode 100644 index 0000000000..c03c5ccef7 --- /dev/null +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetCurrentUser.graphql @@ -0,0 +1,6 @@ +query GetCurrentUser { + currentUser + { + username + } +} diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetMergeRequest.graphql b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetMergeRequest.graphql new file mode 100644 index 0000000000..86187fd7a6 --- /dev/null +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetMergeRequest.graphql @@ -0,0 +1,13 @@ +query GetMergeRequest($project: ID!, $number: String!) { + project(fullPath: $project) { + mergeRequest(iid: $number) { + author { username } + description + title + diffHeadSha + webUrl + iid + id + } + } +} diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetMergeRequestNotes.graphql b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetMergeRequestNotes.graphql new file mode 100644 index 0000000000..7228e4827a --- /dev/null +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetMergeRequestNotes.graphql @@ -0,0 +1,15 @@ +query GetMergeRequestNotes($project: ID!, $number: String!) { + project(fullPath: $project) { + mergeRequest(iid: $number) { + iid + id + notes { + nodes { + author { username } + body + id + } + } + } + } +} diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetMergeRequests.graphql b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetMergeRequests.graphql index c7c6353910..fe23c1e77c 100644 --- a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetMergeRequests.graphql +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/GQL/Queries/GetMergeRequests.graphql @@ -3,9 +3,10 @@ query GetMergeRequests($project: ID!, $numbers: [String!]!) { mergeRequests(iids: $numbers) { nodes { state - mergeCommitSha + diffHeadSha closedAt iid + id } } } diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj index 3f8437288d..704675f94d 100644 --- a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj @@ -1,4 +1,4 @@ - + @@ -33,7 +33,12 @@ - + + + + + + @@ -42,6 +47,7 @@ + diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs index 51a390dcdc..d48fdf15dc 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs @@ -6,9 +6,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using GitLabApiClient; -using GitLabApiClient.Models.Notes.Requests; - using Microsoft.Extensions.Logging; using StrawberryShake; @@ -92,10 +89,10 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote if (mergeRequest.State != MergeRequestState.Merged) return; - var mergeCommitSha = mergeRequest.MergeCommitSha; - if (mergeCommitSha == null) + var diffHeadSha = mergeRequest.DiffHeadSha; + if (diffHeadSha == null) { - Logger.LogWarning("MergeRequest #{id} had no MergeCommitSha!", mergeRequest.Iid); + Logger.LogWarning("MergeRequest #{id} had no DiffHeadSha!", mergeRequest.Iid); return; } @@ -119,7 +116,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote } // We don't just assume, actually check the repo contains the merge commit. - if (await repository.CommittishIsParent(mergeCommitSha, cancellationToken)) + if (await repository.CommittishIsParent(diffHeadSha, cancellationToken)) newList.Remove( newList.First( potential => potential.Number == number)); @@ -163,19 +160,65 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote int testMergeNumber, CancellationToken cancellationToken) { - var client = repositorySettings.AccessToken != null - ? new GitLabClient(GitLabRemoteFeatures.GitLabUrl, repositorySettings.AccessToken) - : new GitLabClient(GitLabRemoteFeatures.GitLabUrl); - + await using var client = await GraphQLGitLabClientFactory.CreateClient(repositorySettings.AccessToken); try { - await client - .MergeRequests - .CreateNoteAsync( - $"{remoteRepositoryOwner}/{remoteRepositoryName}", - testMergeNumber, - new CreateMergeRequestNoteRequest(comment)) - .WaitAsync(cancellationToken); + string header = String.Format(CultureInfo.InvariantCulture, "{0}## Test merge deployment history:{0}{0}", Environment.NewLine); + + // Try to find an existing note + var notesQueryResult = await client.GraphQL.GetMergeRequestNotes.ExecuteAsync( + $"{remoteRepositoryOwner}/{remoteRepositoryName}", + testMergeNumber.ToString(CultureInfo.InvariantCulture), + cancellationToken); + + notesQueryResult.EnsureNoErrors(); + + var mergeRequest = notesQueryResult.Data?.Project?.MergeRequest; + if (mergeRequest == null) + { + Logger.LogWarning("GitLab GetMergeRequestNotes mergeRequest returned null!"); + return; + } + + var comments = mergeRequest.Notes?.Nodes; + IGetMergeRequestNotes_Project_MergeRequest_Notes_Nodes? existingComment = null; + if (comments != null) + { + for (int i = comments.Count - 1; i > -1; i--) + { + var currentComment = comments[i]; + if (currentComment?.Author?.Username == repositorySettings.AccessUser && (currentComment?.Body?.StartsWith(header) ?? false)) + { + if (currentComment.Body.Length > 987856) + { // Limit should be 999,999 so we'll leave a 12,143 buffer + break; + } + + existingComment = currentComment; + break; + } + } + } + + // Either amend or create the note + if (existingComment != null) + { + var noteModificationResult = await client.GraphQL.ModifyNote.ExecuteAsync( + existingComment.Id, + existingComment.Body + comment, + cancellationToken); + + notesQueryResult.EnsureNoErrors(); + } + else + { + var noteCreationResult = await client.GraphQL.CreateNote.ExecuteAsync( + mergeRequest.Id, + header + comment, + cancellationToken); + + noteCreationResult.EnsureNoErrors(); + } } catch (Exception ex) when (ex is not OperationCanceledException) { diff --git a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs index ffbd577ac2..58e2ebf274 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs @@ -1,11 +1,14 @@ using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; -using GitLabApiClient; using Microsoft.Extensions.Logging; +using StrawberryShake; + using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Utils.GitLab.GraphQL; namespace Tgstation.Server.Host.Components.Repository { @@ -45,30 +48,25 @@ namespace Tgstation.Server.Host.Components.Repository RepositorySettings repositorySettings, CancellationToken cancellationToken) { - var client = repositorySettings.AccessToken != null - ? new GitLabClient(GitLabUrl, repositorySettings.AccessToken) - : new GitLabClient(GitLabUrl); - + await using var client = await GraphQLGitLabClientFactory.CreateClient(repositorySettings.AccessToken); try { - var mr = await client - .MergeRequests - .GetAsync($"{RemoteRepositoryOwner}/{RemoteRepositoryName}", parameters.Number) - .WaitAsync(cancellationToken); + var operationResult = await client.GraphQL.GetMergeRequest.ExecuteAsync( + $"{RemoteRepositoryOwner}/{RemoteRepositoryName}", + parameters.Number.ToString(CultureInfo.InvariantCulture), + cancellationToken); - var revisionToUse = parameters.TargetCommitSha == null - || mr.Sha.StartsWith(parameters.TargetCommitSha, StringComparison.OrdinalIgnoreCase) - ? mr.Sha - : parameters.TargetCommitSha; + operationResult.EnsureNoErrors(); + var mr = operationResult.Data?.Project?.MergeRequest ?? throw new InvalidOperationException("GitLab MergeRequest check returned null!"); return new Models.TestMerge { - Author = mr.Author.Username, + Author = mr.Author?.Username, BodyAtMerge = mr.Description, TitleAtMerge = mr.Title, Comment = parameters.Comment, Number = parameters.Number, - TargetCommitSha = mr.Sha, + TargetCommitSha = mr.DiffHeadSha, Url = mr.WebUrl, }; } diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 533aec872d..e99a801607 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -7,12 +7,12 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using GitLabApiClient; - using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using StrawberryShake; + using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Request; @@ -27,6 +27,7 @@ using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Utils; using Tgstation.Server.Host.Utils.GitHub; +using Tgstation.Server.Host.Utils.GitLab.GraphQL; namespace Tgstation.Server.Host.Controllers { @@ -585,70 +586,84 @@ namespace Tgstation.Server.Host.Controllers switch (remoteFeatures.RemoteGitProvider!.Value) { case RemoteGitProvider.GitHub: - var gitHubClient = await gitHubClientFactory.CreateClientForRepository( + { + var gitHubClient = await gitHubClientFactory.CreateClientForRepository( model.AccessToken, new RepositoryIdentifier( remoteFeatures.RemoteRepositoryOwner!, remoteFeatures.RemoteRepositoryName!), cancellationToken); - if (gitHubClient == null) - { - return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError) + if (gitHubClient == null) { - AdditionalData = "GitHub authentication failed!", - }); - } - - try - { - string username; - if (!model.AccessToken.StartsWith(Api.Models.RepositorySettings.TgsAppPrivateKeyPrefix)) - { - var user = await gitHubClient.User.Current(); - username = user.Login; - } - else - { - // we literally need to app auth again to get the damn bot username - var appClient = gitHubClientFactory.CreateAppClient(model.AccessToken)!; - var app = await appClient.GitHubApps.GetCurrent(); - username = app.Name; + return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError) + { + AdditionalData = "GitHub authentication failed!", + }); } - if (username != model.AccessUser) - return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch)); - } - catch (Exception ex) - { - return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError) + try { - AdditionalData = $"GitHub Authentication Failure: {ex.Message}", - }); + string username; + if (!model.AccessToken.StartsWith(Api.Models.RepositorySettings.TgsAppPrivateKeyPrefix)) + { + var user = await gitHubClient.User.Current(); + username = user.Login; + } + else + { + // we literally need to app auth again to get the damn bot username + var appClient = gitHubClientFactory.CreateAppClient(model.AccessToken)!; + var app = await appClient.GitHubApps.GetCurrent(); + username = app.Name; + } + + if (username != model.AccessUser) + return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch)); + } + catch (Exception ex) + { + return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError) + { + AdditionalData = $"GitHub Authentication Failure: {ex.Message}", + }); + } + + break; } - break; case RemoteGitProvider.GitLab: - // need to abstract this eventually - var gitLabClient = new GitLabClient(GitLabRemoteFeatures.GitLabUrl, model.AccessToken); - try { - var user = await gitLabClient.Users.GetCurrentSessionAsync(); - if (user.Username != model.AccessUser) - return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch)); - } - catch (Exception ex) - { - return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError) + // need to abstract this eventually + await using var gitLabClient = await GraphQLGitLabClientFactory.CreateClient(model.AccessToken); + try { - AdditionalData = $"GitLab Authentication Failure: {ex.Message}", - }); + var operationResult = await gitLabClient.GraphQL.GetCurrentUser.ExecuteAsync(cancellationToken); + + operationResult.EnsureNoErrors(); + + var user = operationResult.Data?.CurrentUser; + if (user == null || user.Username != model.AccessUser) + { + return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch)); + } + } + catch (Exception ex) + { + return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError) + { + AdditionalData = $"GitLab Authentication Failure: {ex.Message}", + }); + } + + break; } - break; case RemoteGitProvider.Unknown: default: - Logger.LogWarning("RemoteGitProvider is {provider}, no auth check implemented!", remoteFeatures.RemoteGitProvider.Value); - break; + { + Logger.LogWarning("RemoteGitProvider is {provider}, no auth check implemented!", remoteFeatures.RemoteGitProvider.Value); + break; + } } return null;