init
This commit is contained in:
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Approve.Checker;
|
||||||
|
|
||||||
|
public interface IApp
|
||||||
|
{
|
||||||
|
Task<int> Run();
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user