From 3325102956f8a5c7b605f174d449d164acc8b6ed Mon Sep 17 00:00:00 2001 From: kislovdm Date: Wed, 7 Feb 2024 12:54:23 +0300 Subject: [PATCH] init --- src/Approve.Checker/App.cs | 133 ++++++++++++++++++ src/Approve.Checker/Approve.Checker.csproj | 6 + src/Approve.Checker/Gitlab/ApproversDto.cs | 13 ++ .../ApprovalState/ApprovalStateResponse.cs | 11 ++ .../Gitlab/Dto/ApprovalState/ApprovedBy.cs | 7 + src/Approve.Checker/Gitlab/Dto/Reviewers.cs | 9 ++ src/Approve.Checker/Gitlab/Dto/User.cs | 12 ++ .../Gitlab/GitlabCustomClient.cs | 119 ++++++++++++++++ .../Gitlab/IGitlabCustomClient.cs | 19 +++ src/Approve.Checker/Gitlab/Settings.cs | 23 +++ src/Approve.Checker/IApp.cs | 6 + src/Approve.Checker/Program.cs | 12 +- 12 files changed, 362 insertions(+), 8 deletions(-) create mode 100644 src/Approve.Checker/App.cs create mode 100644 src/Approve.Checker/Gitlab/ApproversDto.cs create mode 100644 src/Approve.Checker/Gitlab/Dto/ApprovalState/ApprovalStateResponse.cs create mode 100644 src/Approve.Checker/Gitlab/Dto/ApprovalState/ApprovedBy.cs create mode 100644 src/Approve.Checker/Gitlab/Dto/Reviewers.cs create mode 100644 src/Approve.Checker/Gitlab/Dto/User.cs create mode 100644 src/Approve.Checker/Gitlab/GitlabCustomClient.cs create mode 100644 src/Approve.Checker/Gitlab/IGitlabCustomClient.cs create mode 100644 src/Approve.Checker/Gitlab/Settings.cs create mode 100644 src/Approve.Checker/IApp.cs diff --git a/src/Approve.Checker/App.cs b/src/Approve.Checker/App.cs new file mode 100644 index 0000000..e728562 --- /dev/null +++ b/src/Approve.Checker/App.cs @@ -0,0 +1,133 @@ +using System.Text; +using Approve.Checker.Gitlab; +using GitLabApiClient; +using GitLabApiClient.Models.Commits.Responses; +using GitLabApiClient.Models.MergeRequests.Responses; +using GitLabApiClient.Models.Notes.Requests; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using Settings = Approve.Checker.Gitlab.Settings; + +namespace Approve.Checker; + +public class App : IApp +{ + private readonly GitLabClient _client; + private readonly Settings _settings; + private readonly GitlabCustomClient _customClient; + + public App() + { + var httpHandler = new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + _settings = new Settings(); + if (string.IsNullOrEmpty(_settings.GitlabUrl)) + { + Console.WriteLine("CI_SERVER_URL is not defined."); + return; + } + _client = new GitLabClient(_settings.GitlabUrl, _settings.GitlabToken, httpHandler); + _customClient = new GitlabCustomClient(_settings.GitlabUrl, _settings.GitlabToken, httpHandler); + } + + public async Task Run() + { + if (string.IsNullOrEmpty(_settings.Approvers)) + { + Console.WriteLine("Approvers in empty, please define APPROVERS_FILEPATH."); + return 0; + } + if (string.IsNullOrEmpty(_settings.MrPath)) + { + Console.WriteLine("Not in MergeRequest. Exiting."); + return 0; + } + var mrId = int.Parse(_settings.MrPath.Split('!')[1]); + + var mergeRequest = await _client.MergeRequests.GetAsync(_settings.ProjectId, mrId); + var commit = await _client.Commits.GetAsync(_settings.ProjectId, mergeRequest.Sha); + if (!await IsApproved(mergeRequest, commit)) + { + return -1; + } + + return 0; + } + + private async Task IsApproved(MergeRequest mergeRequest, Commit commit) + { + var approversDict = ParseApprovers(); + if (approversDict is null) + { + return false; + } + var emojis = await _client.MergeRequests.GetAwardEmojisAsync(_settings.ProjectId, mergeRequest.Iid); + + var sb = new StringBuilder(); + foreach (var emoji in emojis) + { + if (emoji.UpdatedAt <= commit.CommittedDate) + { + sb.AppendLine($" - Удален {emoji.Name} от {emoji.User.Username} ({emoji.UpdatedAt}), т.к. после него были коммиты ({commit.CommittedDate})."); + await _customClient.DeleteEmoji(mergeRequest.ProjectId, mergeRequest.Iid, emoji.Id); + } + } + if (sb.Length > 0) + { + await _client.MergeRequests.CreateNoteAsync(mergeRequest.ProjectId, mergeRequest.Iid, + new CreateMergeRequestNoteRequest(sb.ToString())); + } + emojis = emojis.Where(f => f.UpdatedAt > commit.CommittedDate).ToArray(); + + foreach (var approversGroup in approversDict) + { + var needCount = approversGroup.Value.CountApprovers; + var groupApproves = 0; + var approvedInGroup = new List(); + foreach (var approveUser in approversGroup.Value.Users) + { + if (emojis.Any(f => f.User.Username == approveUser)) + { + approvedInGroup.Add(approveUser); + groupApproves++; + if (groupApproves >= needCount) + { + var approvers = string.Join(",", approvedInGroup); + Console.WriteLine($"[+] Approved by group of {approversGroup.Key} ({approvers})."); + break; + } + } + } + + if (groupApproves < needCount) + { + Console.WriteLine($"[-] Need to be approved by group of {approversGroup.Key} (current approves: {groupApproves}, need: {needCount}):"); + foreach (var approveUser in approversGroup.Value.Users) + { + Console.WriteLine($" - {approveUser}"); + } + return false; + } + } + + return true; + } + + private Dictionary? ParseApprovers() + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .Build(); + try + { + return deserializer.Deserialize>(_settings.Approvers); + } + catch (Exception ex) + { + Console.WriteLine($"Error while deserializing approvers: {ex}"); + return null; + } + } +} \ No newline at end of file diff --git a/src/Approve.Checker/Approve.Checker.csproj b/src/Approve.Checker/Approve.Checker.csproj index 2150e37..eac58a4 100644 --- a/src/Approve.Checker/Approve.Checker.csproj +++ b/src/Approve.Checker/Approve.Checker.csproj @@ -7,4 +7,10 @@ enable + + + + + + diff --git a/src/Approve.Checker/Gitlab/ApproversDto.cs b/src/Approve.Checker/Gitlab/ApproversDto.cs new file mode 100644 index 0000000..6d370be --- /dev/null +++ b/src/Approve.Checker/Gitlab/ApproversDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Approve.Checker.Gitlab; + +public class ApproversDto +{ + public int CountApprovers { get; set; } = 1; + public List Users { get; set; } +} \ No newline at end of file diff --git a/src/Approve.Checker/Gitlab/Dto/ApprovalState/ApprovalStateResponse.cs b/src/Approve.Checker/Gitlab/Dto/ApprovalState/ApprovalStateResponse.cs new file mode 100644 index 0000000..dbad680 --- /dev/null +++ b/src/Approve.Checker/Gitlab/Dto/ApprovalState/ApprovalStateResponse.cs @@ -0,0 +1,11 @@ +// ReSharper disable UnusedMember.Global +namespace ReleaseScripts.ExternalSystems.Gitlab.CustomClient.Dto.ApprovalState; +#pragma warning disable CS8618 + +public class ApprovalStateResponse +{ + public bool user_has_approved { get; set; } + public bool user_can_approve { get; set; } + public bool approved { get; set; } + public List approved_by { get; set; } +} \ No newline at end of file diff --git a/src/Approve.Checker/Gitlab/Dto/ApprovalState/ApprovedBy.cs b/src/Approve.Checker/Gitlab/Dto/ApprovalState/ApprovedBy.cs new file mode 100644 index 0000000..20364d0 --- /dev/null +++ b/src/Approve.Checker/Gitlab/Dto/ApprovalState/ApprovedBy.cs @@ -0,0 +1,7 @@ +namespace ReleaseScripts.ExternalSystems.Gitlab.CustomClient.Dto.ApprovalState; +#pragma warning disable CS8618 + +public class ApprovedBy +{ + public GitLabApiClient.Models.Users.Responses.User user { get; set; } +} \ No newline at end of file diff --git a/src/Approve.Checker/Gitlab/Dto/Reviewers.cs b/src/Approve.Checker/Gitlab/Dto/Reviewers.cs new file mode 100644 index 0000000..baa798f --- /dev/null +++ b/src/Approve.Checker/Gitlab/Dto/Reviewers.cs @@ -0,0 +1,9 @@ +namespace ReleaseScripts.ExternalSystems.Gitlab.CustomClient.Dto; +#pragma warning disable CS8618 + +public class Reviewers +{ + public User user { get; set; } + public string state { get; set; } + public DateTime created_at { get; set; } +} \ No newline at end of file diff --git a/src/Approve.Checker/Gitlab/Dto/User.cs b/src/Approve.Checker/Gitlab/Dto/User.cs new file mode 100644 index 0000000..3c8e7bb --- /dev/null +++ b/src/Approve.Checker/Gitlab/Dto/User.cs @@ -0,0 +1,12 @@ +#pragma warning disable CS8618 +namespace ReleaseScripts.ExternalSystems.Gitlab.CustomClient.Dto; + +public class User +{ + public int id { get; set; } + public string username { get; set; } + public string name { get; set; } + public string state { get; set; } + public string avatar_url { get; set; } + public string web_url { get; set; } +} \ No newline at end of file diff --git a/src/Approve.Checker/Gitlab/GitlabCustomClient.cs b/src/Approve.Checker/Gitlab/GitlabCustomClient.cs new file mode 100644 index 0000000..a1ae1a3 --- /dev/null +++ b/src/Approve.Checker/Gitlab/GitlabCustomClient.cs @@ -0,0 +1,119 @@ +using System.Net.Http.Json; +using System.Text; +using GitLabApiClient.Models.Job.Responses; +using GitLabApiClient.Models.MergeRequests.Responses; +using Newtonsoft.Json; +using ReleaseScripts.ExternalSystems.Gitlab.CustomClient.Dto; +using ReleaseScripts.ExternalSystems.Gitlab.CustomClient.Dto.ApprovalState; + +namespace Approve.Checker.Gitlab; + +public class GitlabCustomClient : IGitlabCustomClient +{ + private readonly string _url; + private readonly string _token; + private readonly HttpClient _client; + + public GitlabCustomClient(string url, string token, HttpClientHandler? httpClientHandler = null) + { + _url = url; + _token = token; + _client = new HttpClient(httpClientHandler ?? new HttpClientHandler()); + } + + public async Task GetReviewers(string projectId, int Iid) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{_url}/api/v4/projects/{projectId}/merge_requests/{Iid}/reviewers"); + request.Headers.Add("PRIVATE-TOKEN", _token); + var response = await _client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var responseObj = await response.Content.ReadFromJsonAsync(); + return responseObj!; + } + + public async Task GetApprovals(string projectId, int Iid) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{_url}/api/v4/projects/{projectId}/merge_requests/{Iid}/approvals"); + request.Headers.Add("PRIVATE-TOKEN", _token); + var response = await _client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var responseObj = await response.Content.ReadFromJsonAsync(); + return responseObj!; + } + + public async Task GetDiscussions(string projectId, int Iid) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{_url}/api/v4/projects/{projectId}/merge_requests/{Iid}/discussions"); + request.Headers.Add("PRIVATE-TOKEN", _token); + var response = await _client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var responseObj = await response.Content.ReadFromJsonAsync(); + return responseObj!; + } + + public async Task GetMergeRequestsByTargetBranch(string targetBranch) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{_url}/api/v4/merge_requests?target_branch={targetBranch}&scope=all&per_page=1000"); + request.Headers.Add("PRIVATE-TOKEN", _token); + var response = await _client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var responseStr = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject(responseStr)!; + return responseObj; + } + + public async Task GetMergeRequestsBySourceBranch(string targetBranch) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{_url}/api/v4/merge_requests?source_branch={targetBranch}&scope=all&per_page=1000"); + request.Headers.Add("PRIVATE-TOKEN", _token); + var response = await _client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var responseStr = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject(responseStr)!; + return responseObj; + } + + public async Task SetMrReviewer(string mrProjectId, int mergeRequestId, int reviewerId) + { + var request = new HttpRequestMessage(HttpMethod.Put, $"{_url}/api/v4/projects/{mrProjectId}/merge_requests/{mergeRequestId}"); + request.Headers.Add("PRIVATE-TOKEN", _token); + var json = "{\"reviewer_ids\": [" + reviewerId + "]}"; + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await _client.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + + public async Task GetPipelineTriggers(string projectId, int pipelineId) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{_url}/api/v4/projects/{projectId}/pipelines/{pipelineId}/bridges?per_page=100&page=1&include_retried=true"); + request.Headers.Add("PRIVATE-TOKEN", _token); + var response = await _client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var responseStr = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject(responseStr)!; + return responseObj; + } + + public async Task RestartJob(string projectId, int failedJobId) + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{_url}/api/v4/projects/{projectId}/jobs/{failedJobId}/retry"); + request.Headers.Add("PRIVATE-TOKEN", _token); + var response = await _client.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + public async Task DeleteEmoji(string projectId, int Iid, int emojiId) + { + var request = new HttpRequestMessage(HttpMethod.Delete, $"{_url}/api/v4/projects/{projectId}/merge_requests/{Iid}/award_emoji/{emojiId}"); + request.Headers.Add("PRIVATE-TOKEN", _token); + var response = await _client.SendAsync(request); + response.EnsureSuccessStatusCode(); + } +} \ No newline at end of file diff --git a/src/Approve.Checker/Gitlab/IGitlabCustomClient.cs b/src/Approve.Checker/Gitlab/IGitlabCustomClient.cs new file mode 100644 index 0000000..dbaf6a3 --- /dev/null +++ b/src/Approve.Checker/Gitlab/IGitlabCustomClient.cs @@ -0,0 +1,19 @@ +using GitLabApiClient.Models.Job.Responses; +using GitLabApiClient.Models.MergeRequests.Responses; +using ReleaseScripts.ExternalSystems.Gitlab.CustomClient.Dto; +using ReleaseScripts.ExternalSystems.Gitlab.CustomClient.Dto.ApprovalState; + +namespace Approve.Checker.Gitlab; + +public interface IGitlabCustomClient +{ + Task GetReviewers(string projectId, int Iid); + Task GetApprovals(string projectId, int Iid); + Task GetDiscussions(string projectId, int Iid); + Task GetMergeRequestsByTargetBranch(string targetBranch); + Task GetPipelineTriggers(string projectId, int pipelineId); + Task RestartJob(string projectId, int failedJobId); + Task GetMergeRequestsBySourceBranch(string targetBranch); + Task SetMrReviewer(string mrProjectId, int mergeRequestId, int reviewerId); + Task DeleteEmoji(string projectId, int Iid, int emojiId); +} \ No newline at end of file diff --git a/src/Approve.Checker/Gitlab/Settings.cs b/src/Approve.Checker/Gitlab/Settings.cs new file mode 100644 index 0000000..3dcdad0 --- /dev/null +++ b/src/Approve.Checker/Gitlab/Settings.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Approve.Checker.Gitlab +{ + public class Settings + { + public string GitlabUrl = Environment.GetEnvironmentVariable("CI_SERVER_URL") ?? CheckAndThrow("CI_SERVER_URL"); + public string GitlabToken = Environment.GetEnvironmentVariable("GITLAB_TOKEN") ?? CheckAndThrow("GITLAB_TOKEN"); + public string MrPath = Environment.GetEnvironmentVariable("CI_OPEN_MERGE_REQUESTS")!; + public string ProjectId = Environment.GetEnvironmentVariable("CI_PROJECT_ID") ?? CheckAndThrow("CI_PROJECT_ID"); + public string Approvers = Environment.GetEnvironmentVariable("APPROVERS_FILEPATH ") ?? CheckAndThrow("APPROVERS_FILEPATH"); + + private static string CheckAndThrow(string name) + { + Console.WriteLine($"Please define {name}"); + throw new ArgumentException($"Please define {name}"); + } + } +} diff --git a/src/Approve.Checker/IApp.cs b/src/Approve.Checker/IApp.cs new file mode 100644 index 0000000..583e6a9 --- /dev/null +++ b/src/Approve.Checker/IApp.cs @@ -0,0 +1,6 @@ +namespace Approve.Checker; + +public interface IApp +{ + Task Run(); +} \ No newline at end of file diff --git a/src/Approve.Checker/Program.cs b/src/Approve.Checker/Program.cs index 99cbfe7..3217e35 100644 --- a/src/Approve.Checker/Program.cs +++ b/src/Approve.Checker/Program.cs @@ -1,15 +1,11 @@ using System.Collections; +using Approve.Checker; internal static class Program { - internal static Task Main(string[] args) + internal static async Task Main(string[] args) { - - foreach (DictionaryEntry e in System.Environment.GetEnvironmentVariables()) - { - Console.WriteLine(e.Key + " = " + e.Value); - } - - return Task.FromResult(-1); + var app = new App(); + return await app.Run(); } } \ No newline at end of file