This commit is contained in:
kislovdm
2024-02-07 12:54:23 +03:00
parent 7a4c2a2b28
commit 3325102956
12 changed files with 362 additions and 8 deletions
+133
View File
@@ -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<int> 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<bool> 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<string>();
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<string, ApproversDto>? ParseApprovers()
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();
try
{
return deserializer.Deserialize<Dictionary<string, ApproversDto>>(_settings.Approvers);
}
catch (Exception ex)
{
Console.WriteLine($"Error while deserializing approvers: {ex}");
return null;
}
}
}
@@ -7,4 +7,10 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="GitLabApiClient" Version="1.8.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="YamlDotNet" Version="15.1.1" />
</ItemGroup>
</Project> </Project>
@@ -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<string> Users { get; set; }
}
@@ -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<ApprovedBy> approved_by { get; set; }
}
@@ -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; }
}
@@ -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; }
}
+12
View File
@@ -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; }
}
@@ -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<Reviewers[]> 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<Reviewers[]>();
return responseObj!;
}
public async Task<ApprovalStateResponse> 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<ApprovalStateResponse>();
return responseObj!;
}
public async Task<ApprovalStateResponse> 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<ApprovalStateResponse>();
return responseObj!;
}
public async Task<MergeRequest[]> 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<MergeRequest[]>(responseStr)!;
return responseObj;
}
public async Task<MergeRequest[]> 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<MergeRequest[]>(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<Job[]> 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<Job[]>(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();
}
}
@@ -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<Reviewers[]> GetReviewers(string projectId, int Iid);
Task<ApprovalStateResponse> GetApprovals(string projectId, int Iid);
Task<ApprovalStateResponse> GetDiscussions(string projectId, int Iid);
Task<MergeRequest[]> GetMergeRequestsByTargetBranch(string targetBranch);
Task<Job[]> GetPipelineTriggers(string projectId, int pipelineId);
Task RestartJob(string projectId, int failedJobId);
Task<MergeRequest[]> GetMergeRequestsBySourceBranch(string targetBranch);
Task SetMrReviewer(string mrProjectId, int mergeRequestId, int reviewerId);
Task DeleteEmoji(string projectId, int Iid, int emojiId);
}
+23
View File
@@ -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}");
}
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace Approve.Checker;
public interface IApp
{
Task<int> Run();
}
+4 -8
View File
@@ -1,15 +1,11 @@
using System.Collections; using System.Collections;
using Approve.Checker;
internal static class Program internal static class Program
{ {
internal static Task<int> Main(string[] args) internal static async Task<int> Main(string[] args)
{ {
var app = new App();
foreach (DictionaryEntry e in System.Environment.GetEnvironmentVariables()) return await app.Run();
{
Console.WriteLine(e.Key + " = " + e.Value);
}
return Task.FromResult(-1);
} }
} }