From 0b7c14a2a172412c875a1edd54fe4975c58b2bfd Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Sat, 16 Aug 2025 00:16:03 +0300 Subject: [PATCH 01/26] refactor: extracted interactions with SQL into repository, commands and queries --- .../CustomWebApplicationFactory.cs | 4 + SS14.Labeller.Tests/IntegrationTests.Issue.cs | 4 +- .../IntegrationTests.PullRequest.cs | 42 +++++------ .../IntegrationTests.PullRequestReview.cs | 26 +++---- SS14.Labeller/Database/DataManager.cs | 51 ------------- ...abaseMigrationApplyingBackgroundService.cs | 17 +++++ SS14.Labeller/GitHubApi/GitHubApiClient.cs | 12 +-- SS14.Labeller/GitHubApi/IGitHubApiClient.cs | 12 +-- .../Handlers/LabelPullRequestHandler.cs | 45 +++-------- SS14.Labeller/Models/EventBase.cs | 4 +- SS14.Labeller/Program.cs | 67 +---------------- SS14.Labeller/Registry.cs | 74 +++++++++++++++++++ .../Repository/Commands/DatabaseCommand.cs | 9 +++ .../Commands/DatabaseRequestBase.cs | 14 ++++ .../Commands/InsertDiscourseTopicCommand.cs | 30 ++++++++ .../Repository/DiscourseTopicsRepository.cs | 36 +++++++++ .../Repository/IDiscourseTopicsRepository.cs | 10 +++ .../Repository/Queries/DatabaseQueryBase.cs | 9 +++ .../Repository/Queries/FindTopicQuery.cs | 27 +++++++ SS14.Labeller/Repository/RepositoryBase.cs | 16 ++++ 20 files changed, 307 insertions(+), 202 deletions(-) delete mode 100644 SS14.Labeller/Database/DataManager.cs create mode 100644 SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs create mode 100644 SS14.Labeller/Registry.cs create mode 100644 SS14.Labeller/Repository/Commands/DatabaseCommand.cs create mode 100644 SS14.Labeller/Repository/Commands/DatabaseRequestBase.cs create mode 100644 SS14.Labeller/Repository/Commands/InsertDiscourseTopicCommand.cs create mode 100644 SS14.Labeller/Repository/DiscourseTopicsRepository.cs create mode 100644 SS14.Labeller/Repository/IDiscourseTopicsRepository.cs create mode 100644 SS14.Labeller/Repository/Queries/DatabaseQueryBase.cs create mode 100644 SS14.Labeller/Repository/Queries/FindTopicQuery.cs create mode 100644 SS14.Labeller/Repository/RepositoryBase.cs diff --git a/SS14.Labeller.Tests/CustomWebApplicationFactory.cs b/SS14.Labeller.Tests/CustomWebApplicationFactory.cs index 3bbd5d5..4ab5096 100644 --- a/SS14.Labeller.Tests/CustomWebApplicationFactory.cs +++ b/SS14.Labeller.Tests/CustomWebApplicationFactory.cs @@ -8,6 +8,7 @@ using NSubstitute; using SS14.Labeller.DiscourseApi; using SS14.Labeller.GitHubApi; +using SS14.Labeller.Repository; namespace SS14.Labeller.Tests; @@ -16,18 +17,21 @@ public class CustomWebApplicationFactory : WebApplicationFactory { public IGitHubApiClient GitHubApiClient { get; private set; } public IDiscourseClient DiscourseClient { get; private set; } + public IDiscourseTopicsRepository TopicsRepository { get; private set; } /// protected override void ConfigureWebHost(IWebHostBuilder builder) { GitHubApiClient = Substitute.For(); DiscourseClient = Substitute.For(); + TopicsRepository = Substitute.For(); base.ConfigureWebHost(builder); builder.ConfigureServices(sp => { sp.Replace(new ServiceDescriptor(typeof(IGitHubApiClient), GitHubApiClient)); sp.Replace(new ServiceDescriptor(typeof(IDiscourseClient), DiscourseClient)); + sp.Replace(new ServiceDescriptor(typeof(IDiscourseTopicsRepository), TopicsRepository)); }).ConfigureAppConfiguration((context, configurationBuilder) => { configurationBuilder.AddInMemoryCollection(new Dictionary diff --git a/SS14.Labeller.Tests/IntegrationTests.Issue.cs b/SS14.Labeller.Tests/IntegrationTests.Issue.cs index d42e433..d9e37e5 100644 --- a/SS14.Labeller.Tests/IntegrationTests.Issue.cs +++ b/SS14.Labeller.Tests/IntegrationTests.Issue.cs @@ -31,7 +31,7 @@ public async Task Issue_Created_AddedUntriagedLabel() await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "Kaizen" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "Kaizen" && x.Owner.Login == "Fildrance"), 31, StatusLabels.Untriaged, Arg.Any() @@ -58,7 +58,7 @@ public async Task Issue_Closed_NoLabelsAssigned() await _applicationFactory.GitHubApiClient .DidNotReceive() .AddLabel( - Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() diff --git a/SS14.Labeller.Tests/IntegrationTests.PullRequest.cs b/SS14.Labeller.Tests/IntegrationTests.PullRequest.cs index febaa14..d51dd06 100644 --- a/SS14.Labeller.Tests/IntegrationTests.PullRequest.cs +++ b/SS14.Labeller.Tests/IntegrationTests.PullRequest.cs @@ -43,7 +43,7 @@ public async Task PullRequest_ToStaging_ApplyStagingBranchLabel() await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, BranchLabels.Staging, Arg.Any() @@ -64,7 +64,7 @@ public async Task PullRequest_ToStable_ApplyStagingBranchLabel() await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, BranchLabels.Stable, Arg.Any() @@ -85,7 +85,7 @@ public async Task PullRequest_ToMaster_ApplyNoBranchLabel() await _applicationFactory.GitHubApiClient .DidNotReceive() .AddLabel( - Arg.Any(), + Arg.Any(), Arg.Any(), BranchLabels.Stable, Arg.Any() @@ -93,7 +93,7 @@ await _applicationFactory.GitHubApiClient await _applicationFactory.GitHubApiClient .DidNotReceive() .AddLabel( - Arg.Any(), + Arg.Any(), Arg.Any(), BranchLabels.Staging, Arg.Any() @@ -108,12 +108,12 @@ public async Task PullRequest_ByMaintainer_ApplyApprovedLabel() var requestContent = await CreateRequestContent(fileName, "pull_request"); _applicationFactory.GitHubApiClient.GetChangedFiles( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, Arg.Any() ).Returns(["LootSystem.cs", "Loot.png",]); _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), "Fildrance", Arg.Any() ).Returns(Task.FromResult("write")); @@ -125,7 +125,7 @@ public async Task PullRequest_ByMaintainer_ApplyApprovedLabel() await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, StatusLabels.Approved, Arg.Any() @@ -140,12 +140,12 @@ public async Task PullRequest_ByNonMaintainer_ApplyRequireReviewLabel() var requestContent = await CreateRequestContent(fileName, "pull_request"); _applicationFactory.GitHubApiClient.GetChangedFiles( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, Arg.Any() ).Returns(["LootSystem.cs", "Loot.png",]); _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), "Fildrance", Arg.Any() ).Returns(Task.FromResult("read")); @@ -157,7 +157,7 @@ public async Task PullRequest_ByNonMaintainer_ApplyRequireReviewLabel() await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, StatusLabels.RequireReview, Arg.Any() @@ -172,7 +172,7 @@ public async Task PullRequest_Synchronize_SizeLabelInserted() var requestContent = await CreateRequestContent(fileName, "pull_request"); _applicationFactory.GitHubApiClient.GetChangedFiles( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, Arg.Any() ).Returns(["LootSystem.cs", "Loot.png",]); @@ -184,7 +184,7 @@ public async Task PullRequest_Synchronize_SizeLabelInserted() await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, "size/L", Arg.Any() @@ -193,7 +193,7 @@ await _applicationFactory.GitHubApiClient await _applicationFactory.GitHubApiClient .Received() .RemoveLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, "size/S", Arg.Any() @@ -208,7 +208,7 @@ public async Task PullRequest_Synchronize_ContentLabelSet() var requestContent = await CreateRequestContent(fileName, "pull_request"); _applicationFactory.GitHubApiClient.GetChangedFiles( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, Arg.Any() ).Returns(["LootSystem.cs", "MyLoots.rsi/Loot.png", "PickLoot.ogg"]); @@ -220,7 +220,7 @@ public async Task PullRequest_Synchronize_ContentLabelSet() await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, ChangesLabels.Sprites, Arg.Any() @@ -229,7 +229,7 @@ await _applicationFactory.GitHubApiClient await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, ChangesLabels.Audio, Arg.Any() @@ -238,7 +238,7 @@ await _applicationFactory.GitHubApiClient await _applicationFactory.GitHubApiClient .DidNotReceive() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, ChangesLabels.NoCSharp, Arg.Any() @@ -247,7 +247,7 @@ await _applicationFactory.GitHubApiClient await _applicationFactory.GitHubApiClient .DidNotReceive() .RemoveLabel( - Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() @@ -262,7 +262,7 @@ public async Task PullRequest_SynchronizeNoCSharp_NoCSharpLabelSet() var requestContent = await CreateRequestContent(fileName, "pull_request"); _applicationFactory.GitHubApiClient.GetChangedFiles( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, Arg.Any() ).Returns(["MyLoots.rsi/Loot.png", "PickLoot.ogg", "shaders/swag.swsl", "Resources/Maps/main.ylm", "Resources/PrototypesMaps/main.ylm", "loot-picker.xaml"]); @@ -274,7 +274,7 @@ public async Task PullRequest_SynchronizeNoCSharp_NoCSharpLabelSet() await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, ChangesLabels.NoCSharp, Arg.Any() @@ -283,7 +283,7 @@ await _applicationFactory.GitHubApiClient await _applicationFactory.GitHubApiClient .DidNotReceive() .RemoveLabel( - Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() diff --git a/SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs b/SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs index 78f6dbb..140d3ce 100644 --- a/SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs +++ b/SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs @@ -19,7 +19,7 @@ public async Task PullRequestReview_ReviewByNonMaintainer_DoNothing() var requestContent = await CreateRequestContent(fileName, "pull_request_review"); _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), "NonFildrance", Arg.Any() ).Returns(Task.FromResult("user")); @@ -38,7 +38,7 @@ public async Task PullRequestReview_ReviewByNonMaintainer_DoNothing() await _applicationFactory.GitHubApiClient .DidNotReceive() .AddLabel( - Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() @@ -53,7 +53,7 @@ public async Task PullRequestReview_ApproveByMaintainer_SwitchStatusLabels() var requestContent = await CreateRequestContent(fileName, "pull_request_review"); _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), "Fildrance", Arg.Any() ).Returns(Task.FromResult("write")); @@ -72,7 +72,7 @@ public async Task PullRequestReview_ApproveByMaintainer_SwitchStatusLabels() await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, StatusLabels.Approved, Arg.Any() @@ -87,7 +87,7 @@ public async Task PullRequestReview_RequestChangesByMaintainer_SwitchStatusLabel var requestContent = await CreateRequestContent(fileName, "pull_request_review"); _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), "Fildrance", Arg.Any() ).Returns(Task.FromResult("write")); @@ -106,7 +106,7 @@ public async Task PullRequestReview_RequestChangesByMaintainer_SwitchStatusLabel await _applicationFactory.GitHubApiClient .Received() .AddLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, StatusLabels.AwaitingChanges, Arg.Any() @@ -115,7 +115,7 @@ await _applicationFactory.GitHubApiClient await _applicationFactory.GitHubApiClient .Received() .RemoveLabel( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, StatusLabels.RequireReview, Arg.Any() @@ -130,7 +130,7 @@ public async Task PullRequestReview_CommentedMergedByMaintainer_DoNothing() var requestContent = await CreateRequestContent(fileName, "pull_request_review"); _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), "Fildrance", Arg.Any() ).Returns(Task.FromResult("write")); @@ -149,7 +149,7 @@ public async Task PullRequestReview_CommentedMergedByMaintainer_DoNothing() await _applicationFactory.GitHubApiClient .DidNotReceive() .AddLabel( - Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() @@ -158,7 +158,7 @@ await _applicationFactory.GitHubApiClient await _applicationFactory.GitHubApiClient .DidNotReceive() .RemoveLabel( - Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() @@ -173,7 +173,7 @@ public async Task PullRequestReview_CommentedByMaintainer_DoNothing() var requestContent = await CreateRequestContent(fileName, "pull_request_review"); _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), "Fildrance", Arg.Any() ).Returns(Task.FromResult("write")); @@ -192,7 +192,7 @@ public async Task PullRequestReview_CommentedByMaintainer_DoNothing() await _applicationFactory.GitHubApiClient .DidNotReceive() .AddLabel( - Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() @@ -201,7 +201,7 @@ await _applicationFactory.GitHubApiClient await _applicationFactory.GitHubApiClient .DidNotReceive() .RemoveLabel( - Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() diff --git a/SS14.Labeller/Database/DataManager.cs b/SS14.Labeller/Database/DataManager.cs deleted file mode 100644 index 67d91d4..0000000 --- a/SS14.Labeller/Database/DataManager.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Dapper; -using Microsoft.Data.Sqlite; - -namespace SS14.Labeller.Database; - -public sealed class DataManager(ILogger logger, IConfiguration configuration) : IHostedService -{ - public SqliteConnection OpenConnection() - { - var con = new SqliteConnection(GetConnectionString()); - con.Open(); - return con; - } - - public async Task GetTopicIdForDiscussion(string owner, string repoName, int issueNumber) - { - await using var connection = OpenConnection(); - const string sql = """ - SELECT TopicId FROM Discussions - WHERE RepoOwner = @Owner AND RepoName = @Name AND IssueNumber = @Number - """; - - var value = await connection.QuerySingleAsync(sql, new - { - Owner = owner, - Name = repoName, - Number = issueNumber - }); - - return value; - } - - private string GetConnectionString() - { - return configuration.GetConnectionString("Default") ?? "Data Source=Application.db"; - } - - Task IHostedService.StartAsync(CancellationToken cancellationToken) - { - var con = OpenConnection(); - - Migrator.Migrate(con, "SS14.Labeller.Database.Migrations", logger); - return Task.CompletedTask; - } - - Task IHostedService.StopAsync(CancellationToken cancellationToken) - { - // No shutdown needed. - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs b/SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs new file mode 100644 index 0000000..734e4b2 --- /dev/null +++ b/SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs @@ -0,0 +1,17 @@ +using Microsoft.Data.Sqlite; + +namespace SS14.Labeller.Database; + +public sealed class DatabaseMigrationApplyingBackgroundService(ILogger logger, IConfiguration configuration) + : BackgroundService +{ + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var connectionString = configuration.GetConnectionString("Default") + ?? "Data Source=Application.db"; + await using var con = new SqliteConnection(connectionString); + + Migrator.Migrate(con, "SS14.Labeller.Database.Migrations", logger); + } +} \ No newline at end of file diff --git a/SS14.Labeller/GitHubApi/GitHubApiClient.cs b/SS14.Labeller/GitHubApi/GitHubApiClient.cs index b9e5760..8792f06 100644 --- a/SS14.Labeller/GitHubApi/GitHubApiClient.cs +++ b/SS14.Labeller/GitHubApi/GitHubApiClient.cs @@ -9,7 +9,7 @@ public class GitHubApiClient(HttpClient httpClient) : IGitHubApiClient { private const string BaseUrl = "https://api.github.com"; - public async Task AddLabel(Repository repo, int number, string label, CancellationToken ct) + public async Task AddLabel(GithubRepo repo, int number, string label, CancellationToken ct) { var request = new AddLabelRequest { labels = [label] }; var json = JsonSerializer.Serialize(request, SourceGenerationContext.Default.AddLabelRequest); @@ -18,12 +18,12 @@ public async Task AddLabel(Repository repo, int number, string label, Cancellati await httpClient.PostAsync($"{BaseUrl}/repos/{repo.Owner.Login}/{repo.Name}/issues/{number}/labels", content, ct); } - public async Task RemoveLabel(Repository repo, int number, string label, CancellationToken ct) + public async Task RemoveLabel(GithubRepo repo, int number, string label, CancellationToken ct) { await httpClient.DeleteAsync($"{BaseUrl}/repos/{repo.Owner.Login}/{repo.Name}/issues/{number}/labels/{Uri.EscapeDataString(label)}", ct); } - public async Task> GetChangedFiles(Repository repo, int prNumber, CancellationToken ct) + public async Task> GetChangedFiles(GithubRepo repo, int prNumber, CancellationToken ct) { // TODO: Ratelimit? Might explode on big PRs??? // TODO: Update to use ParseNextPageUrl @@ -50,7 +50,7 @@ public async Task> GetChangedFiles(Repository repo, int prNumber, C } /// - public async Task GetPermission(Repository repo, string? user, CancellationToken ct) + public async Task GetPermission(GithubRepo repo, string? user, CancellationToken ct) { var permRes = await httpClient.GetAsync($"{BaseUrl}/repos/{repo.Owner.Login}/{repo.Name}/collaborators/{user}/permission", ct); if (!permRes.IsSuccessStatusCode) @@ -61,7 +61,7 @@ public async Task> GetChangedFiles(Repository repo, int prNumber, C return permJson.RootElement.GetProperty("permission").GetString(); } - public async Task AddComment(Repository repo, int number, string comment, CancellationToken ct) + public async Task AddComment(GithubRepo repo, int number, string comment, CancellationToken ct) { var request = new AddCommentRequest { body = $"{comment}\n\n{StatusMessages.CommentPostfix}" }; var json = JsonSerializer.Serialize(request, SourceGenerationContext.Default.AddCommentRequest); @@ -70,7 +70,7 @@ public async Task AddComment(Repository repo, int number, string comment, Cancel await httpClient.PostAsync($"{BaseUrl}/repos/{repo.Owner.Login}/{repo.Name}/issues/{number}/comments", content, ct); } - public async Task> GetComments(Repository repo, int prNumber, CancellationToken ct) + public async Task> GetComments(GithubRepo repo, int prNumber, CancellationToken ct) { var allComments = new List(); var url = $"{BaseUrl}/repos/{repo.Owner.Login}/{repo.Name}/issues/{prNumber}/comments?per_page=100"; diff --git a/SS14.Labeller/GitHubApi/IGitHubApiClient.cs b/SS14.Labeller/GitHubApi/IGitHubApiClient.cs index e4780d8..c6df9fd 100644 --- a/SS14.Labeller/GitHubApi/IGitHubApiClient.cs +++ b/SS14.Labeller/GitHubApi/IGitHubApiClient.cs @@ -4,10 +4,10 @@ namespace SS14.Labeller.GitHubApi; public interface IGitHubApiClient { - Task AddLabel(Repository repo, int number, string label, CancellationToken ct); - Task RemoveLabel(Repository repo, int number, string label, CancellationToken ct); - Task> GetChangedFiles(Repository repo, int prNumber, CancellationToken ct); - Task GetPermission(Repository repo, string? user, CancellationToken ct); - Task AddComment(Repository repo, int number, string comment, CancellationToken ct); - Task> GetComments(Repository repo, int prNumber, CancellationToken ct); + Task AddLabel(GithubRepo repo, int number, string label, CancellationToken ct); + Task RemoveLabel(GithubRepo repo, int number, string label, CancellationToken ct); + Task> GetChangedFiles(GithubRepo repo, int prNumber, CancellationToken ct); + Task GetPermission(GithubRepo repo, string? user, CancellationToken ct); + Task AddComment(GithubRepo repo, int number, string comment, CancellationToken ct); + Task> GetComments(GithubRepo repo, int prNumber, CancellationToken ct); } \ No newline at end of file diff --git a/SS14.Labeller/Handlers/LabelPullRequestHandler.cs b/SS14.Labeller/Handlers/LabelPullRequestHandler.cs index 199ea5b..d5d8c97 100644 --- a/SS14.Labeller/Handlers/LabelPullRequestHandler.cs +++ b/SS14.Labeller/Handlers/LabelPullRequestHandler.cs @@ -1,21 +1,21 @@ -using Dapper; -using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Options; using SS14.Labeller.Configuration; -using SS14.Labeller.Database; using SS14.Labeller.DiscourseApi; using SS14.Labeller.GitHubApi; using SS14.Labeller.Labels; using SS14.Labeller.Messages; using SS14.Labeller.Models; +using SS14.Labeller.Repository; namespace SS14.Labeller.Handlers; public class LabelPullRequestHandler( IGitHubApiClient client, IDiscourseClient discourseClient, + IDiscourseTopicsRepository topicRepository, IOptions config, - DataManager dataManager + IDiscourseTopicsRepository topicsRepository ) : RequestHandlerBase { private readonly DiscourseConfig _discourseConfig = config.Value; @@ -82,22 +82,8 @@ protected override async Task HandleInternal(PullRequestEvent request, Cancellat // ReSharper disable once NullableWarningSuppressionIsUsed if (request.Label!.Name == StatusLabels.UndergoingDiscussion && _discourseConfig.Enable) { - // We are making a discussion, yipee! - await using var connection = dataManager.OpenConnection(); - const string sql = """ - SELECT 1 FROM Discussions - WHERE RepoOwner = @Owner AND RepoName = @Name AND IssueNumber = @Number - LIMIT 1; - """; - - var exists = await connection.ExecuteScalarAsync(sql, new - { - Owner = request.Repository.Owner.Login, - Name = request.Repository.Name, - Number = request.PullRequest.Number - }); - - if (exists is null) + var exists = await topicRepository.HasTopic(request.Repository.Owner.Login, request.Repository.Name, request.PullRequest.Number, ct); + if (exists) { // need to make a new discussion. var topic = await discourseClient.CreateTopic( _discourseConfig.DiscussionCategoryId, @@ -110,20 +96,10 @@ SELECT 1 FROM Discussions await client.AddComment(repository, number, StatusMessages.StartedDiscussion + topicLink, ct); - const string insert = """ - INSERT INTO Discussions (RepoOwner, RepoName, IssueNumber, TopicId) - VALUES (@Owner, @Name, @Number, @TopicId); - """; await discourseClient.ApplyTags(topic.TopicId, ct, _discourseConfig.Tagging.PrOpenTag); - await connection.ExecuteAsync(insert, new - { - Owner = request.Repository.Owner.Login, - Name = request.Repository.Name, - Number = request.PullRequest.Number, - TopicId = topic.TopicId - }); + await topicRepository.Add(request.Repository.Owner.Login, request.Repository.Name, request.PullRequest.Number, topic.TopicId, ct); } } } @@ -142,9 +118,7 @@ INSERT INTO Discussions (RepoOwner, RepoName, IssueNumber, TopicId) if (request.Action is "closed" && !string.IsNullOrEmpty(request.PullRequest.MergedAt)) { // PR got merged - var discussion = - await dataManager.GetTopicIdForDiscussion(request.Repository.Owner.Login, request.Repository.Name, - number); + var discussion = await topicsRepository.FindTopicIdForDiscussion(request.Repository.Owner.Login, request.Repository.Name, number, ct); if (discussion is not null) { @@ -160,8 +134,7 @@ await dataManager.GetTopicIdForDiscussion(request.Repository.Owner.Login, reques { // pr was just closed, not merged. var discussion = - await dataManager.GetTopicIdForDiscussion(request.Repository.Owner.Login, request.Repository.Name, - number); + await topicsRepository.FindTopicIdForDiscussion(request.Repository.Owner.Login, request.Repository.Name, number, ct); if (discussion is not null) { diff --git a/SS14.Labeller/Models/EventBase.cs b/SS14.Labeller/Models/EventBase.cs index 646f4d9..1fb82e4 100644 --- a/SS14.Labeller/Models/EventBase.cs +++ b/SS14.Labeller/Models/EventBase.cs @@ -4,10 +4,10 @@ public abstract class EventBase { public required string Action { get; init; } - public required Repository Repository { get; init; } + public required GithubRepo Repository { get; init; } } -public class Repository +public class GithubRepo { public required User Owner { get; init; } diff --git a/SS14.Labeller/Program.cs b/SS14.Labeller/Program.cs index f2544ec..3ade6ef 100644 --- a/SS14.Labeller/Program.cs +++ b/SS14.Labeller/Program.cs @@ -1,11 +1,7 @@ -using System.Net.Http.Headers; using Dapper; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using SS14.Labeller.Configuration; -using SS14.Labeller.Database; -using SS14.Labeller.DiscourseApi; -using SS14.Labeller.GitHubApi; using SS14.Labeller.Handlers; using SS14.Labeller.Helpers; @@ -18,74 +14,15 @@ public class Program public static void Main(string[] args) { var builder = WebApplication.CreateSlimBuilder(args); + builder.Configuration.AddJsonFile("appsettings.json", true, true); builder.Configuration.AddJsonFile("appsettings.Secret.json", true, true); builder.Configuration.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true); - builder.Services.AddOptions() - .Bind(builder.Configuration.GetSection(DiscourseConfig.Name)) - .ValidateDataAnnotations(); - - builder.Services.AddOptions() - .Bind(builder.Configuration.GetSection(GitHubConfig.Name)) - .ValidateDataAnnotations(); - - builder.Services.ConfigureHttpJsonOptions(options => - { - options.SerializerOptions.TypeInfoResolverChain.Insert(0, SourceGenerationContext.Default); - }); - builder.Logging.ClearProviders(); builder.Logging.AddConsole(); - builder.Services.AddHttpLogging(options => - { - options.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All; - }); - builder.Services.AddHttpClient((sp, client) => - { - var githubConfig = sp.GetRequiredService>().Value; - - client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SS14.Labeller", "1.0")); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", githubConfig.Token); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); - }); - - { - var discourseStartupConfig = new DiscourseConfig(); - builder.Configuration.Bind(DiscourseConfig.Name, discourseStartupConfig); - - if (discourseStartupConfig.Enable) - { - builder.Services.AddHttpClient((sp, client) => - { - var discourseConfig = sp.GetRequiredService>().Value; - - client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SS14.Labeller", "1.0")); - client.DefaultRequestHeaders.Add("Api-Key", discourseConfig.ApiKey); - client.DefaultRequestHeaders.Add("Api-Username", discourseConfig.Username); - client.BaseAddress = new Uri(discourseConfig.Url); - }); - } - else - { - builder.Services.AddHttpClient(); - } - } - - - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddHostedService(p => p.GetRequiredService()); - - builder.Services.AddSingleton>( - sp => sp.GetServices() - .ToDictionary(x => x.EventType) - ); + builder.Services.RegisterDependencies(builder.Configuration); var app = builder.Build(); diff --git a/SS14.Labeller/Registry.cs b/SS14.Labeller/Registry.cs new file mode 100644 index 0000000..e3efcc0 --- /dev/null +++ b/SS14.Labeller/Registry.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.Options; +using SS14.Labeller.Configuration; +using SS14.Labeller.Database; +using SS14.Labeller.DiscourseApi; +using SS14.Labeller.GitHubApi; +using SS14.Labeller.Handlers; +using SS14.Labeller.Repository; +using System.Net.Http.Headers; + +namespace SS14.Labeller; + +public static class Registry +{ + public static void RegisterDependencies(this IServiceCollection service, IConfiguration configuration) + { + service.AddOptions() + .Bind(configuration.GetSection(DiscourseConfig.Name)) + .ValidateDataAnnotations(); + + service.AddOptions() + .Bind(configuration.GetSection(GitHubConfig.Name)) + .ValidateDataAnnotations(); + + service.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.TypeInfoResolverChain.Insert(0, SourceGenerationContext.Default); + }); + + service.AddHttpLogging(options => + { + options.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All; + }); + service.AddHttpClient((sp, client) => + { + var githubConfig = sp.GetRequiredService>().Value; + + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SS14.Labeller", "1.0")); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", githubConfig.Token); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); + }); + + var discourseStartupConfig = new DiscourseConfig(); + configuration.Bind(DiscourseConfig.Name, discourseStartupConfig); + + if (discourseStartupConfig.Enable) + { + service.AddHttpClient((sp, client) => + { + var discourseConfig = sp.GetRequiredService>().Value; + + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SS14.Labeller", "1.0")); + client.DefaultRequestHeaders.Add("Api-Key", discourseConfig.ApiKey); + client.DefaultRequestHeaders.Add("Api-Username", discourseConfig.Username); + client.BaseAddress = new Uri(discourseConfig.Url); + }); + } + else + { + service.AddHttpClient(); + } + + service.AddSingleton(); + service.AddSingleton(); + service.AddSingleton(); + + service.AddSingleton(); + service.AddHostedService(); + + service.AddSingleton>( + sp => sp.GetServices() + .ToDictionary(x => x.EventType) + ); + } +} \ No newline at end of file diff --git a/SS14.Labeller/Repository/Commands/DatabaseCommand.cs b/SS14.Labeller/Repository/Commands/DatabaseCommand.cs new file mode 100644 index 0000000..7a5f89e --- /dev/null +++ b/SS14.Labeller/Repository/Commands/DatabaseCommand.cs @@ -0,0 +1,9 @@ +using System.Data.Common; + +namespace SS14.Labeller.Repository.Commands; + +public abstract class DatabaseCommandBase : DatabaseRequestBase +{ + public abstract Task Execute(DbConnection connection, CancellationToken ct); + +} \ No newline at end of file diff --git a/SS14.Labeller/Repository/Commands/DatabaseRequestBase.cs b/SS14.Labeller/Repository/Commands/DatabaseRequestBase.cs new file mode 100644 index 0000000..d31ff4b --- /dev/null +++ b/SS14.Labeller/Repository/Commands/DatabaseRequestBase.cs @@ -0,0 +1,14 @@ +using Dapper; + +namespace SS14.Labeller.Repository.Commands; + +public abstract class DatabaseRequestBase +{ + protected CommandDefinition GetCommand(CancellationToken ct) + { + var commandText = GetSql(); + return new CommandDefinition(commandText, this, cancellationToken: ct); + } + + protected abstract string GetSql(); +} \ No newline at end of file diff --git a/SS14.Labeller/Repository/Commands/InsertDiscourseTopicCommand.cs b/SS14.Labeller/Repository/Commands/InsertDiscourseTopicCommand.cs new file mode 100644 index 0000000..ddcd3b7 --- /dev/null +++ b/SS14.Labeller/Repository/Commands/InsertDiscourseTopicCommand.cs @@ -0,0 +1,30 @@ +using Dapper; +using System.Data.Common; + +namespace SS14.Labeller.Repository.Commands +{ + public class InsertDiscourseTopicCommand : DatabaseCommandBase + { + public required string RepoOwner { get; init; } + + public required string RepoName { get; init; } + + public required int IssueNumber { get; init; } + + public required int TopicId { get; init; } + + public override async Task Execute(DbConnection connection, CancellationToken ct) + { + var cd = GetCommand(ct); + return await connection.ExecuteAsync(cd); + } + + private const string Sql = $""" + INSERT INTO Discussions (RepoOwner, RepoName, IssueNumber, TopicId) + VALUES (@{nameof(RepoOwner)}, @{nameof(RepoName)}, @{nameof(IssueNumber)}, @{nameof(TopicId)}); + """; + + /// + protected override string GetSql() => Sql; + } +} \ No newline at end of file diff --git a/SS14.Labeller/Repository/DiscourseTopicsRepository.cs b/SS14.Labeller/Repository/DiscourseTopicsRepository.cs new file mode 100644 index 0000000..35204ef --- /dev/null +++ b/SS14.Labeller/Repository/DiscourseTopicsRepository.cs @@ -0,0 +1,36 @@ +using SS14.Labeller.Repository.Commands; +using SS14.Labeller.Repository.Queries; + +namespace SS14.Labeller.Repository; + +public class DiscourseTopicsRepository(IConfiguration configuration) + : RepositoryBase(configuration), IDiscourseTopicsRepository +{ + public async Task FindTopicIdForDiscussion(string owner, string repoName, int issueNumber, CancellationToken ct) + { + await using var connection = OpenConnection(); + return await new FindTopicQuery + { + RepoOwner = owner, + RepoName = repoName, + IssueNumber = issueNumber + }.Query(connection, ct); + } + + public async Task HasTopic(string repoOwner, string repoName, int issueNumber, CancellationToken ct) + { + return (await FindTopicIdForDiscussion(repoOwner, repoName, issueNumber, ct)).HasValue; + } + + public async Task Add(string owner, string name, int issueNumber, int topicId, CancellationToken ct) + { + await using var connection = OpenConnection(); + await new InsertDiscourseTopicCommand + { + IssueNumber = issueNumber, + RepoName = name, + RepoOwner = owner, + TopicId = topicId + }.Execute(connection, ct); + } +} \ No newline at end of file diff --git a/SS14.Labeller/Repository/IDiscourseTopicsRepository.cs b/SS14.Labeller/Repository/IDiscourseTopicsRepository.cs new file mode 100644 index 0000000..3d49a5f --- /dev/null +++ b/SS14.Labeller/Repository/IDiscourseTopicsRepository.cs @@ -0,0 +1,10 @@ +namespace SS14.Labeller.Repository; + +public interface IDiscourseTopicsRepository +{ + Task FindTopicIdForDiscussion(string owner, string repoName, int issueNumber, CancellationToken ct); + + Task HasTopic(string repoOwner, string repoName, int issueNumber, CancellationToken ct); + + Task Add(string owner, string name, int issueNumber, int topicId, CancellationToken ct); +} \ No newline at end of file diff --git a/SS14.Labeller/Repository/Queries/DatabaseQueryBase.cs b/SS14.Labeller/Repository/Queries/DatabaseQueryBase.cs new file mode 100644 index 0000000..5f8183c --- /dev/null +++ b/SS14.Labeller/Repository/Queries/DatabaseQueryBase.cs @@ -0,0 +1,9 @@ +using System.Data.Common; +using SS14.Labeller.Repository.Commands; + +namespace SS14.Labeller.Repository.Queries; + +public abstract class DatabaseQueryBase : DatabaseRequestBase +{ + public abstract Task Query(DbConnection connection, CancellationToken ct); +} \ No newline at end of file diff --git a/SS14.Labeller/Repository/Queries/FindTopicQuery.cs b/SS14.Labeller/Repository/Queries/FindTopicQuery.cs new file mode 100644 index 0000000..2d8665a --- /dev/null +++ b/SS14.Labeller/Repository/Queries/FindTopicQuery.cs @@ -0,0 +1,27 @@ +using System.Data.Common; +using Dapper; + +namespace SS14.Labeller.Repository.Queries; + +public class FindTopicQuery : DatabaseQueryBase +{ + public required string RepoOwner { get; init; } + + public required string RepoName { get; init; } + + public required int IssueNumber { get; init; } + + public override async Task Query(DbConnection connection, CancellationToken ct) + { + var cd = GetCommand(ct); + return await connection.QueryFirstOrDefaultAsync(cd); + } + + private const string Sql = $""" + SELECT TopicId FROM Discussions + WHERE RepoOwner = @{nameof(RepoOwner)} AND RepoName = @{nameof(RepoName)} AND IssueNumber = @{nameof(IssueNumber)} + """; + + /// + protected override string GetSql() => Sql; +} \ No newline at end of file diff --git a/SS14.Labeller/Repository/RepositoryBase.cs b/SS14.Labeller/Repository/RepositoryBase.cs new file mode 100644 index 0000000..d636243 --- /dev/null +++ b/SS14.Labeller/Repository/RepositoryBase.cs @@ -0,0 +1,16 @@ +using System.Data.Common; +using Microsoft.Data.Sqlite; + +namespace SS14.Labeller.Repository; + +public class RepositoryBase(IConfiguration configuration) +{ + protected DbConnection OpenConnection() + { + var connectionString = configuration.GetConnectionString("Default") + ?? "Data Source=Application.db"; + var con = new SqliteConnection(connectionString); + con.Open(); + return con; + } +} \ No newline at end of file From 7f56cd501903aee3a0b83c611d70f0d61305132e Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Sat, 16 Aug 2025 16:03:47 +0300 Subject: [PATCH 02/26] fix tests y removing migration on start in test run --- SS14.Labeller.Tests/CustomWebApplicationFactory.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/SS14.Labeller.Tests/CustomWebApplicationFactory.cs b/SS14.Labeller.Tests/CustomWebApplicationFactory.cs index 4ab5096..02c0eb7 100644 --- a/SS14.Labeller.Tests/CustomWebApplicationFactory.cs +++ b/SS14.Labeller.Tests/CustomWebApplicationFactory.cs @@ -1,11 +1,14 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using NSubstitute; +using SS14.Labeller.Database; using SS14.Labeller.DiscourseApi; using SS14.Labeller.GitHubApi; using SS14.Labeller.Repository; @@ -32,6 +35,11 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) sp.Replace(new ServiceDescriptor(typeof(IGitHubApiClient), GitHubApiClient)); sp.Replace(new ServiceDescriptor(typeof(IDiscourseClient), DiscourseClient)); sp.Replace(new ServiceDescriptor(typeof(IDiscourseTopicsRepository), TopicsRepository)); + var hostedServiceDescriptor = sp.First(d => + d.ServiceType == typeof(IHostedService) && + d.ImplementationType == typeof(DatabaseMigrationApplyingBackgroundService)); // Replace YourHostedService with the actual type + + sp.Remove(hostedServiceDescriptor); }).ConfigureAppConfiguration((context, configurationBuilder) => { configurationBuilder.AddInMemoryCollection(new Dictionary From 42a870f982fd69cd2c8cc9bbfd5a7f368bf4dbad Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Sat, 16 Aug 2025 23:24:03 +0300 Subject: [PATCH 03/26] refactor: extracted labels as separate classes, and label manager to actually control sets of labels --- SS14.Labeller.Tests/IntegrationTests.Issue.cs | 6 +- .../IntegrationTests.PullRequest.cs | 42 ++--- .../IntegrationTests.PullRequestReview.cs | 48 +++--- SS14.Labeller/GitHubApi/GitHubApiClient.cs | 28 +++- SS14.Labeller/GitHubApi/IGitHubApiClient.cs | 9 +- SS14.Labeller/Handlers/LabelIssueHandler.cs | 4 +- .../Handlers/LabelPullRequestHandler.cs | 154 ++++++++---------- .../Handlers/LabelPullRequestReviewHandler.cs | 21 +-- SS14.Labeller/Labelling/ILabelManager.cs | 11 ++ SS14.Labeller/Labelling/LabelManager.cs | 42 +++++ SS14.Labeller/Labelling/Labels/BranchLabel.cs | 16 ++ .../Labelling/Labels/ChangesLabel.cs | 20 +++ .../Labelling/Labels/LabelGenericBase.cs | 50 ++++++ SS14.Labeller/Labelling/Labels/SizeLabel.cs | 52 ++++++ .../Labelling/Labels/StageOfWorkLabel.cs | 16 ++ SS14.Labeller/Labelling/Labels/StatusLabel.cs | 17 ++ SS14.Labeller/Labels/BranchLabels.cs | 9 - SS14.Labeller/Labels/ChangesLabels.cs | 13 -- SS14.Labeller/Labels/SizeLabels.cs | 31 ---- SS14.Labeller/Labels/StatusLabels.cs | 12 -- .../Models/IPullRequestAwareEvent.cs | 7 + SS14.Labeller/Models/PullRequestEvent.cs | 2 +- .../Models/PullRequestReviewEvent.cs | 2 +- SS14.Labeller/Registry.cs | 4 + 24 files changed, 386 insertions(+), 230 deletions(-) create mode 100644 SS14.Labeller/Labelling/ILabelManager.cs create mode 100644 SS14.Labeller/Labelling/LabelManager.cs create mode 100644 SS14.Labeller/Labelling/Labels/BranchLabel.cs create mode 100644 SS14.Labeller/Labelling/Labels/ChangesLabel.cs create mode 100644 SS14.Labeller/Labelling/Labels/LabelGenericBase.cs create mode 100644 SS14.Labeller/Labelling/Labels/SizeLabel.cs create mode 100644 SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs create mode 100644 SS14.Labeller/Labelling/Labels/StatusLabel.cs delete mode 100644 SS14.Labeller/Labels/BranchLabels.cs delete mode 100644 SS14.Labeller/Labels/ChangesLabels.cs delete mode 100644 SS14.Labeller/Labels/SizeLabels.cs delete mode 100644 SS14.Labeller/Labels/StatusLabels.cs create mode 100644 SS14.Labeller/Models/IPullRequestAwareEvent.cs diff --git a/SS14.Labeller.Tests/IntegrationTests.Issue.cs b/SS14.Labeller.Tests/IntegrationTests.Issue.cs index d9e37e5..74de488 100644 --- a/SS14.Labeller.Tests/IntegrationTests.Issue.cs +++ b/SS14.Labeller.Tests/IntegrationTests.Issue.cs @@ -1,10 +1,10 @@ using NSubstitute; using NUnit.Framework; -using SS14.Labeller.Labels; using SS14.Labeller.Models; using System.Net; using System.Threading; using System.Threading.Tasks; +using SS14.Labeller.Labelling.Labels; namespace SS14.Labeller.Tests; @@ -33,7 +33,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "Kaizen" && x.Owner.Login == "Fildrance"), 31, - StatusLabels.Untriaged, + StatusLabel.Untriaged, Arg.Any() ); } @@ -60,7 +60,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Any(), Arg.Any(), - Arg.Any(), + Arg.Any(), Arg.Any() ); } diff --git a/SS14.Labeller.Tests/IntegrationTests.PullRequest.cs b/SS14.Labeller.Tests/IntegrationTests.PullRequest.cs index d51dd06..366cfba 100644 --- a/SS14.Labeller.Tests/IntegrationTests.PullRequest.cs +++ b/SS14.Labeller.Tests/IntegrationTests.PullRequest.cs @@ -4,7 +4,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -using SS14.Labeller.Labels; +using SS14.Labeller.Labelling.Labels; namespace SS14.Labeller.Tests; @@ -45,7 +45,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - BranchLabels.Staging, + BranchLabel.Staging, Arg.Any() ); } @@ -66,7 +66,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - BranchLabels.Stable, + BranchLabel.Stable, Arg.Any() ); } @@ -87,7 +87,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Any(), Arg.Any(), - BranchLabels.Stable, + BranchLabel.Stable, Arg.Any() ); await _applicationFactory.GitHubApiClient @@ -95,7 +95,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Any(), Arg.Any(), - BranchLabels.Staging, + BranchLabel.Staging, Arg.Any() ); } @@ -112,11 +112,11 @@ public async Task PullRequest_ByMaintainer_ApplyApprovedLabel() 4, Arg.Any() ).Returns(["LootSystem.cs", "Loot.png",]); - _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + _applicationFactory.GitHubApiClient.IsMaintainer( "Fildrance", + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), Arg.Any() - ).Returns(Task.FromResult("write")); + ).Returns(Task.FromResult(true)); // Act await _client.PostAsync("/webhook", requestContent); @@ -127,7 +127,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - StatusLabels.Approved, + StatusLabel.Approved, Arg.Any() ); } @@ -144,11 +144,11 @@ public async Task PullRequest_ByNonMaintainer_ApplyRequireReviewLabel() 4, Arg.Any() ).Returns(["LootSystem.cs", "Loot.png",]); - _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + _applicationFactory.GitHubApiClient.IsMaintainer( "Fildrance", + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), Arg.Any() - ).Returns(Task.FromResult("read")); + ).Returns(Task.FromResult(true)); // Act await _client.PostAsync("/webhook", requestContent); @@ -159,7 +159,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - StatusLabels.RequireReview, + StageOfWorkLabel.RequireReview, Arg.Any() ); } @@ -186,7 +186,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - "size/L", + SizeLabel.L, Arg.Any() ); @@ -195,7 +195,7 @@ await _applicationFactory.GitHubApiClient .RemoveLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - "size/S", + SizeLabel.S, Arg.Any() ); } @@ -222,7 +222,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - ChangesLabels.Sprites, + ChangesLabel.Sprites, Arg.Any() ); @@ -231,7 +231,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - ChangesLabels.Audio, + ChangesLabel.Audio, Arg.Any() ); @@ -240,7 +240,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - ChangesLabels.NoCSharp, + ChangesLabel.NoCSharp, Arg.Any() ); @@ -249,7 +249,7 @@ await _applicationFactory.GitHubApiClient .RemoveLabel( Arg.Any(), Arg.Any(), - Arg.Any(), + Arg.Any(), Arg.Any() ); } @@ -276,7 +276,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - ChangesLabels.NoCSharp, + ChangesLabel.NoCSharp, Arg.Any() ); @@ -285,7 +285,7 @@ await _applicationFactory.GitHubApiClient .RemoveLabel( Arg.Any(), Arg.Any(), - Arg.Any(), + Arg.Any(), Arg.Any() ); } diff --git a/SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs b/SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs index 140d3ce..ffeb1bc 100644 --- a/SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs +++ b/SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs @@ -1,10 +1,10 @@ using NSubstitute; using NUnit.Framework; -using SS14.Labeller.Labels; using SS14.Labeller.Models; using System.Net; using System.Threading.Tasks; using System.Threading; +using SS14.Labeller.Labelling.Labels; namespace SS14.Labeller.Tests; @@ -18,11 +18,11 @@ public async Task PullRequestReview_ReviewByNonMaintainer_DoNothing() const string fileName = "pull_request_review_approve.json"; var requestContent = await CreateRequestContent(fileName, "pull_request_review"); - _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + _applicationFactory.GitHubApiClient.IsMaintainer( "NonFildrance", + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), Arg.Any() - ).Returns(Task.FromResult("user")); + ).Returns(Task.FromResult(true)); // Act var result = await _client.PostAsync("/webhook", requestContent); @@ -40,7 +40,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Any(), Arg.Any(), - Arg.Any(), + Arg.Any(), Arg.Any() ); } @@ -52,11 +52,11 @@ public async Task PullRequestReview_ApproveByMaintainer_SwitchStatusLabels() const string fileName = "pull_request_review_approve.json"; var requestContent = await CreateRequestContent(fileName, "pull_request_review"); - _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + _applicationFactory.GitHubApiClient.IsMaintainer( "Fildrance", + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), Arg.Any() - ).Returns(Task.FromResult("write")); + ).Returns(Task.FromResult(true)); // Act var result = await _client.PostAsync("/webhook", requestContent); @@ -74,7 +74,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - StatusLabels.Approved, + StatusLabel.Approved, Arg.Any() ); } @@ -86,11 +86,11 @@ public async Task PullRequestReview_RequestChangesByMaintainer_SwitchStatusLabel const string fileName = "pull_request_review_request_changes.json"; var requestContent = await CreateRequestContent(fileName, "pull_request_review"); - _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + _applicationFactory.GitHubApiClient.IsMaintainer( "Fildrance", + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), Arg.Any() - ).Returns(Task.FromResult("write")); + ).Returns(Task.FromResult(true)); // Act var result = await _client.PostAsync("/webhook", requestContent); @@ -108,7 +108,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - StatusLabels.AwaitingChanges, + StageOfWorkLabel.AwaitingChanges, Arg.Any() ); @@ -117,7 +117,7 @@ await _applicationFactory.GitHubApiClient .RemoveLabel( Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), 4, - StatusLabels.RequireReview, + StageOfWorkLabel.RequireReview, Arg.Any() ); } @@ -129,11 +129,11 @@ public async Task PullRequestReview_CommentedMergedByMaintainer_DoNothing() const string fileName = "pull_request_review_approve_merged.json"; var requestContent = await CreateRequestContent(fileName, "pull_request_review"); - _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + _applicationFactory.GitHubApiClient.IsMaintainer( "Fildrance", + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), Arg.Any() - ).Returns(Task.FromResult("write")); + ).Returns(Task.FromResult(true)); // Act var result = await _client.PostAsync("/webhook", requestContent); @@ -151,7 +151,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Any(), Arg.Any(), - Arg.Any(), + Arg.Any(), Arg.Any() ); @@ -160,7 +160,7 @@ await _applicationFactory.GitHubApiClient .RemoveLabel( Arg.Any(), Arg.Any(), - Arg.Any(), + Arg.Any(), Arg.Any() ); } @@ -172,11 +172,11 @@ public async Task PullRequestReview_CommentedByMaintainer_DoNothing() const string fileName = "pull_request_review_commented.json"; var requestContent = await CreateRequestContent(fileName, "pull_request_review"); - _applicationFactory.GitHubApiClient.GetPermission( - Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), + _applicationFactory.GitHubApiClient.IsMaintainer( "Fildrance", + Arg.Is(x => x.Name == "SS14.Labeller" && x.Owner.Login == "Fildrance"), Arg.Any() - ).Returns(Task.FromResult("write")); + ).Returns(Task.FromResult(true)); // Act var result = await _client.PostAsync("/webhook", requestContent); @@ -194,7 +194,7 @@ await _applicationFactory.GitHubApiClient .AddLabel( Arg.Any(), Arg.Any(), - Arg.Any(), + Arg.Any(), Arg.Any() ); @@ -203,7 +203,7 @@ await _applicationFactory.GitHubApiClient .RemoveLabel( Arg.Any(), Arg.Any(), - Arg.Any(), + Arg.Any(), Arg.Any() ); } diff --git a/SS14.Labeller/GitHubApi/GitHubApiClient.cs b/SS14.Labeller/GitHubApi/GitHubApiClient.cs index 8792f06..89f7bc8 100644 --- a/SS14.Labeller/GitHubApi/GitHubApiClient.cs +++ b/SS14.Labeller/GitHubApi/GitHubApiClient.cs @@ -2,6 +2,7 @@ using System.Text; using SS14.Labeller.Messages; using SS14.Labeller.Models; +using SS14.Labeller.Labelling.Labels; namespace SS14.Labeller.GitHubApi; @@ -9,18 +10,30 @@ public class GitHubApiClient(HttpClient httpClient) : IGitHubApiClient { private const string BaseUrl = "https://api.github.com"; - public async Task AddLabel(GithubRepo repo, int number, string label, CancellationToken ct) + /// + public async Task AddLabel(string owner, string repoName, int number, LabelBase label, CancellationToken ct) { var request = new AddLabelRequest { labels = [label] }; var json = JsonSerializer.Serialize(request, SourceGenerationContext.Default.AddLabelRequest); var content = new StringContent(json, Encoding.UTF8, "application/json"); - await httpClient.PostAsync($"{BaseUrl}/repos/{repo.Owner.Login}/{repo.Name}/issues/{number}/labels", content, ct); + await httpClient.PostAsync($"{BaseUrl}/repos/{owner}/{repoName}/issues/{number}/labels", content, ct); + } + + public Task AddLabel(GithubRepo repo, int number, LabelBase label, CancellationToken ct) + { + return AddLabel(repo.Owner.Login, repo.Name, number, label, ct); + } + + /// + public async Task RemoveLabel(string owner, string repoName, int number, LabelBase label, CancellationToken ct) + { + await httpClient.DeleteAsync($"{BaseUrl}/repos/{owner}/{repoName}/issues/{number}/labels/{Uri.EscapeDataString(label)}", ct); } - public async Task RemoveLabel(GithubRepo repo, int number, string label, CancellationToken ct) + public Task RemoveLabel(GithubRepo repo, int number, LabelBase label, CancellationToken ct) { - await httpClient.DeleteAsync($"{BaseUrl}/repos/{repo.Owner.Login}/{repo.Name}/issues/{number}/labels/{Uri.EscapeDataString(label)}", ct); + return RemoveLabel(repo.Owner.Login, repo.Name, number, label, ct); } public async Task> GetChangedFiles(GithubRepo repo, int prNumber, CancellationToken ct) @@ -46,19 +59,22 @@ public async Task> GetChangedFiles(GithubRepo repo, int prNumber, C page++; } + return files; } /// - public async Task GetPermission(GithubRepo repo, string? user, CancellationToken ct) + public async Task IsMaintainer(string? user, GithubRepo repo, CancellationToken ct) { var permRes = await httpClient.GetAsync($"{BaseUrl}/repos/{repo.Owner.Login}/{repo.Name}/collaborators/{user}/permission", ct); if (!permRes.IsSuccessStatusCode) { throw new Exception("Failed to get permissions! Does the github token have enough access?"); } + var permJson = JsonDocument.Parse(await permRes.Content.ReadAsStringAsync(ct)); - return permJson.RootElement.GetProperty("permission").GetString(); + var requestedPermission = permJson.RootElement.GetProperty("permission").GetString(); + return requestedPermission is "write" or "admin"; } public async Task AddComment(GithubRepo repo, int number, string comment, CancellationToken ct) diff --git a/SS14.Labeller/GitHubApi/IGitHubApiClient.cs b/SS14.Labeller/GitHubApi/IGitHubApiClient.cs index c6df9fd..02f61ae 100644 --- a/SS14.Labeller/GitHubApi/IGitHubApiClient.cs +++ b/SS14.Labeller/GitHubApi/IGitHubApiClient.cs @@ -1,13 +1,14 @@ -using SS14.Labeller.Models; +using SS14.Labeller.Labelling.Labels; +using SS14.Labeller.Models; namespace SS14.Labeller.GitHubApi; public interface IGitHubApiClient { - Task AddLabel(GithubRepo repo, int number, string label, CancellationToken ct); - Task RemoveLabel(GithubRepo repo, int number, string label, CancellationToken ct); + Task AddLabel(GithubRepo repo, int number, LabelBase label, CancellationToken ct); + Task RemoveLabel(GithubRepo repo, int number, LabelBase label, CancellationToken ct); Task> GetChangedFiles(GithubRepo repo, int prNumber, CancellationToken ct); - Task GetPermission(GithubRepo repo, string? user, CancellationToken ct); + Task IsMaintainer(string? user, GithubRepo forRepository, CancellationToken ct); Task AddComment(GithubRepo repo, int number, string comment, CancellationToken ct); Task> GetComments(GithubRepo repo, int prNumber, CancellationToken ct); } \ No newline at end of file diff --git a/SS14.Labeller/Handlers/LabelIssueHandler.cs b/SS14.Labeller/Handlers/LabelIssueHandler.cs index ae062ab..59baa52 100644 --- a/SS14.Labeller/Handlers/LabelIssueHandler.cs +++ b/SS14.Labeller/Handlers/LabelIssueHandler.cs @@ -1,5 +1,5 @@ using SS14.Labeller.GitHubApi; -using SS14.Labeller.Labels; +using SS14.Labeller.Labelling.Labels; using SS14.Labeller.Models; namespace SS14.Labeller.Handlers; @@ -19,7 +19,7 @@ protected override async Task HandleInternal(IssuesEvent request, CancellationTo var labels = request.Issue.Labels; if (labels.Length == 0) - await client.AddLabel(request.Repository, number, StatusLabels.Untriaged, ct); + await client.AddLabel(request.Repository, number, StatusLabel.Untriaged, ct); } } } \ No newline at end of file diff --git a/SS14.Labeller/Handlers/LabelPullRequestHandler.cs b/SS14.Labeller/Handlers/LabelPullRequestHandler.cs index d5d8c97..43f3de5 100644 --- a/SS14.Labeller/Handlers/LabelPullRequestHandler.cs +++ b/SS14.Labeller/Handlers/LabelPullRequestHandler.cs @@ -3,7 +3,8 @@ using SS14.Labeller.Configuration; using SS14.Labeller.DiscourseApi; using SS14.Labeller.GitHubApi; -using SS14.Labeller.Labels; +using SS14.Labeller.Labelling; +using SS14.Labeller.Labelling.Labels; using SS14.Labeller.Messages; using SS14.Labeller.Models; using SS14.Labeller.Repository; @@ -13,9 +14,9 @@ namespace SS14.Labeller.Handlers; public class LabelPullRequestHandler( IGitHubApiClient client, IDiscourseClient discourseClient, - IDiscourseTopicsRepository topicRepository, - IOptions config, - IDiscourseTopicsRepository topicsRepository + IDiscourseTopicsRepository topicsRepository, + ILabelManager labelManager, + IOptions config ) : RequestHandlerBase { private readonly DiscourseConfig _discourseConfig = config.Value; @@ -28,7 +29,11 @@ protected override async Task HandleInternal(PullRequestEvent request, Cancellat { var pr = request.PullRequest; - var number = pr.Number; + var prNumber = pr.Number; + + var repoOwner = request.Repository.Owner.Login; + var repoName = request.Repository.Name; + var labels = pr.Labels .Select(x => x.Name) .ToArray(); @@ -39,86 +44,71 @@ protected override async Task HandleInternal(PullRequestEvent request, Cancellat if (request.Action is "opened") { if (labels.Length == 0) - await client.AddLabel(repository, number, StatusLabels.Untriaged, ct); + await labelManager.EnsureLabeled(request, StatusLabel.Untriaged, ct); var targetBranch = pr.Base.Ref; - if (targetBranch == "stable" && !labels.Contains(BranchLabels.Stable)) - await client.AddLabel(repository, number, BranchLabels.Stable, ct); - else if (targetBranch == "staging" && !labels.Contains(BranchLabels.Staging)) - await client.AddLabel(repository, number, BranchLabels.Staging, ct); + if (targetBranch == "stable") + await labelManager.EnsureLabeled(request, BranchLabel.Stable, ct); + else if (targetBranch == "staging") + await labelManager.EnsureLabeled(request, BranchLabel.Staging, ct); - var permission = await client.GetPermission(repository, pr.User.Login, ct); - if (permission is "write" or "admin") - await client.AddLabel(repository, number, StatusLabels.Approved, ct); + var isMaintainer = await client.IsMaintainer(pr.User.Login, repository, ct); + if (isMaintainer) + await labelManager.EnsureLabeled(request, StatusLabel.Approved, ct); - await client.AddLabel(repository, number, StatusLabels.RequireReview, ct); + await labelManager.EnsureLabeled(request, StageOfWorkLabel.RequireReview, ct); } if (request.Action is "synchronize" or "opened") { var totalDiff = pr.Additions + pr.Deletions; - var sizeLabel = SizeLabels.TryGetLabelFor(totalDiff); - - // remove the existing size/* labels - foreach (var label in labels) + var sizeLabel = SizeLabel.TryGetLabelFor(totalDiff); + if (sizeLabel is not null) { - if (label == sizeLabel) - continue; // Don't remove a label that is accurate - - if (label?.StartsWith(SizeLabels.Prefix, StringComparison.OrdinalIgnoreCase) == true) - { - await client.RemoveLabel(repository, number, label, ct); - } - } - - if (sizeLabel is not null && !labels.Contains(sizeLabel)) - { - await client.AddLabel(repository, number, sizeLabel, ct); + await labelManager.EnsureLabeled(request, sizeLabel, ct); } } if (request.Action is "labeled") { // ReSharper disable once NullableWarningSuppressionIsUsed - if (request.Label!.Name == StatusLabels.UndergoingDiscussion && _discourseConfig.Enable) + if (request.Label!.Name == StatusLabel.UndergoingDiscussion && _discourseConfig.Enable) { - var exists = await topicRepository.HasTopic(request.Repository.Owner.Login, request.Repository.Name, request.PullRequest.Number, ct); + var exists = await topicsRepository.HasTopic(repoOwner, repoName, prNumber, ct); if (exists) - { // need to make a new discussion. + { + // need to make a new discussion. var topic = await discourseClient.CreateTopic( _discourseConfig.DiscussionCategoryId, StatusMessages.DiscourseTopicBody - .Replace("{link}", request.PullRequest.Url), + .Replace("{link}", request.PullRequest.Url), request.PullRequest.Title, ct); var topicLink = _discourseConfig.Url + topic.PostUrl[1..]; - await client.AddComment(repository, number, StatusMessages.StartedDiscussion + topicLink, ct); + await client.AddComment(repository, prNumber, StatusMessages.StartedDiscussion + topicLink, ct); await discourseClient.ApplyTags(topic.TopicId, ct, _discourseConfig.Tagging.PrOpenTag); - await topicRepository.Add(request.Repository.Owner.Login, request.Repository.Name, request.PullRequest.Number, topic.TopicId, ct); + await topicsRepository.Add(repoOwner, repoName, prNumber, topic.TopicId, ct); } } } if (request.Action is "review_requested") { - // ReSharper disable once NullableWarningSuppressionIsUsed - Asssuming review_requested, there should always be a requested reviewer. - var requestedPermission = await client.GetPermission(repository, request.RequestedReviewer!.Login, ct); - - if (labels.Contains(StatusLabels.AwaitingChanges) && requestedPermission is "write" or "admin") + if (await client.IsMaintainer(request.RequestedReviewer!.Login, repository, ct)) { - await client.AddLabel(repository, number, StatusLabels.RequireReview, ct); - await client.RemoveLabel(repository, number, StatusLabels.AwaitingChanges, ct); + await labelManager.EnsureLabeled(request, StageOfWorkLabel.RequireReview, ct); } } if (request.Action is "closed" && !string.IsNullOrEmpty(request.PullRequest.MergedAt)) - { // PR got merged - var discussion = await topicsRepository.FindTopicIdForDiscussion(request.Repository.Owner.Login, request.Repository.Name, number, ct); + { + // PR got merged + var discussion = await topicsRepository.FindTopicIdForDiscussion(repoOwner, repoName, prNumber, ct); if (discussion is not null) { @@ -126,15 +116,16 @@ protected override async Task HandleInternal(PullRequestEvent request, Cancellat await discourseClient.ApplyTags(discussion.Value, ct, _discourseConfig.Tagging.PrMergedTag); } - if (labels.Contains(StatusLabels.Untriaged)) + if (labels.Contains(StatusLabel.Untriaged)) { - await client.AddComment(repository, number, StatusMessages.UntriagedPullRequestMergedComment, ct); + await client.AddComment(repository, prNumber, StatusMessages.UntriagedPullRequestMergedComment, ct); } - } else if (request.Action is "closed") + } + else if (request.Action is "closed") { // pr was just closed, not merged. var discussion = - await topicsRepository.FindTopicIdForDiscussion(request.Repository.Owner.Login, request.Repository.Name, number, ct); + await topicsRepository.FindTopicIdForDiscussion(repoOwner, repoName, prNumber, ct); if (discussion is not null) { @@ -142,51 +133,34 @@ protected override async Task HandleInternal(PullRequestEvent request, Cancellat } } - var changedFiles = await client.GetChangedFiles(repository, number, ct); - - var sprites = new Matcher().AddInclude("**/*.rsi/*.png"); - if (sprites.Match(changedFiles).HasMatches) - await client.AddLabel(repository, number, ChangesLabels.Sprites, ct); + var changedFiles = await client.GetChangedFiles(repository, prNumber, ct); - var maps = new Matcher().AddInclude("Resources/Maps/**/*.yml") - .AddInclude("Resources/Prototypes/Maps/**/*.yml"); - if (maps.Match(changedFiles).HasMatches) - await client.AddLabel(repository, number, ChangesLabels.Map, ct); - else - await RemoveLabelIfApplied(ChangesLabels.Map); - - var ui = new Matcher().AddInclude("**/*.xaml*"); - if (ui.Match(changedFiles).HasMatches) - await client.AddLabel(repository, number, ChangesLabels.Ui, ct); - else - await RemoveLabelIfApplied(ChangesLabels.Ui); - - var shaders = new Matcher().AddInclude("**/*.swsl"); - if (shaders.Match(changedFiles).HasMatches) - await client.AddLabel(repository, number, ChangesLabels.Shaders, ct); - else - await RemoveLabelIfApplied(ChangesLabels.Shaders); - - var audio = new Matcher().AddInclude("**/*.ogg"); - if (audio.Match(changedFiles).HasMatches) - await client.AddLabel(repository, number, ChangesLabels.Audio, ct); - else - await RemoveLabelIfApplied(ChangesLabels.Audio); - - var cs = new Matcher().AddInclude("**/*.cs"); - if (!cs.Match(changedFiles).HasMatches) - await client.AddLabel(repository, number, ChangesLabels.NoCSharp, ct); - else - await RemoveLabelIfApplied(ChangesLabels.NoCSharp); - - return; + await EnsureChangesLabels(ChangesLabel.Sprites, ["**/*.rsi/*.png"], request, changedFiles, ct: ct); + await EnsureChangesLabels(ChangesLabel.Map, ["Resources/Maps/**/*.yml", "Resources/Prototypes/Maps/**/*.yml"], request, changedFiles, ct: ct); + await EnsureChangesLabels(ChangesLabel.Ui, ["**/*.xaml*"], request, changedFiles, ct:ct); + await EnsureChangesLabels(ChangesLabel.Shaders, ["**/*.sws"], request, changedFiles, ct: ct); + await EnsureChangesLabels(ChangesLabel.Audio, ["**/*.ogg"], request, changedFiles, ct: ct); + await EnsureChangesLabels(ChangesLabel.NoCSharp, ["**/*.cs"], request, changedFiles, isInverted: true, ct: ct); + } - async Task RemoveLabelIfApplied(string label) + private async Task EnsureChangesLabels( + ChangesLabel label, + string[] patterns, + PullRequestEvent request, + List changedFiles, + bool isInverted = false, + CancellationToken ct = default + ) + { + var matcher = new Matcher(); + foreach (var pattern in patterns) { - if (!labels.Contains(label)) - return; - - await client.RemoveLabel(repository, number, label, ct); + matcher = matcher.AddInclude(pattern); } + + if ((matcher.Match(changedFiles).HasMatches && !isInverted) || (!matcher.Match(changedFiles).HasMatches && isInverted)) + await labelManager.EnsureLabeled(request, label, ct); + else + await labelManager.EnsureNotLabeled(request, label, ct); } } \ No newline at end of file diff --git a/SS14.Labeller/Handlers/LabelPullRequestReviewHandler.cs b/SS14.Labeller/Handlers/LabelPullRequestReviewHandler.cs index 55d5248..7f76fdc 100644 --- a/SS14.Labeller/Handlers/LabelPullRequestReviewHandler.cs +++ b/SS14.Labeller/Handlers/LabelPullRequestReviewHandler.cs @@ -1,10 +1,11 @@ using SS14.Labeller.GitHubApi; -using SS14.Labeller.Labels; +using SS14.Labeller.Labelling; +using SS14.Labeller.Labelling.Labels; using SS14.Labeller.Models; namespace SS14.Labeller.Handlers; -public class LabelPullRequestReviewHandler(IGitHubApiClient client) +public class LabelPullRequestReviewHandler(IGitHubApiClient client, ILabelManager labelManager) : RequestHandlerBase { /// @@ -29,24 +30,18 @@ protected override async Task HandleInternal(PullRequestReviewEvent request, Can if (isClosed || isMerged) return; - var number = pr.Number; - var permission = await client.GetPermission(repo, user, ct); - if (permission is "write" or "admin") + var isMaintainer = await client.IsMaintainer(user, repo, ct); + if (isMaintainer) { #pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). await (state switch { "approved" - => client.AddLabel(repo, number, StatusLabels.Approved, ct), - "changes_requested" => - Task.WhenAll( - // We remove the Needs Review label, later down the line when a review is re-requested, we will apply this label again. - client.RemoveLabel(repo, number, StatusLabels.RequireReview, ct), - client.AddLabel(repo, number, StatusLabels.AwaitingChanges, ct) - ) + => labelManager.EnsureLabeled(request, StatusLabel.Approved, ct), + "changes_requested" + => labelManager.EnsureLabeled(request, StageOfWorkLabel.AwaitingChanges, ct) }); #pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). - } } } \ No newline at end of file diff --git a/SS14.Labeller/Labelling/ILabelManager.cs b/SS14.Labeller/Labelling/ILabelManager.cs new file mode 100644 index 0000000..5ecc761 --- /dev/null +++ b/SS14.Labeller/Labelling/ILabelManager.cs @@ -0,0 +1,11 @@ +using SS14.Labeller.Labelling.Labels; +using SS14.Labeller.Models; + +namespace SS14.Labeller.Labelling; + +public interface ILabelManager +{ + Task EnsureLabeled(IPullRequestAwareEvent @event, LabelBase requestedLabel, CancellationToken ct); + + Task EnsureNotLabeled(IPullRequestAwareEvent @event, LabelBase requestedLabel, CancellationToken ct); +} \ No newline at end of file diff --git a/SS14.Labeller/Labelling/LabelManager.cs b/SS14.Labeller/Labelling/LabelManager.cs new file mode 100644 index 0000000..e7326f4 --- /dev/null +++ b/SS14.Labeller/Labelling/LabelManager.cs @@ -0,0 +1,42 @@ +using SS14.Labeller.GitHubApi; +using SS14.Labeller.Labelling.Labels; +using SS14.Labeller.Models; + +namespace SS14.Labeller.Labelling; + +public class LabelManager(IGitHubApiClient client) : ILabelManager +{ + public async Task EnsureLabeled(IPullRequestAwareEvent @event, LabelBase requestedLabel, CancellationToken ct) + { + var alreadyHaveLabel = false; + foreach (var label in @event.PullRequest.Labels) + { + if (label.Name == null) + continue; + + if (label.Name == requestedLabel) + { + alreadyHaveLabel = true; + continue; + } + + if (!requestedLabel.AllowMultiple && requestedLabel.TryGetFromString(label.Name, out var foundLabel)) + { + await client.RemoveLabel(@event.Repository, @event.PullRequest.Number, foundLabel, ct); + } + } + + if (alreadyHaveLabel) + return; + + await client.AddLabel(@event.Repository, @event.PullRequest.Number, requestedLabel, ct); + } + + public async Task EnsureNotLabeled(IPullRequestAwareEvent @event, LabelBase requestedLabel, CancellationToken ct) + { + if (@event.PullRequest.Labels.Any(label => label.Name == requestedLabel)) + { + await client.RemoveLabel(@event.Repository, @event.PullRequest.Number, requestedLabel, ct); + } + } +} \ No newline at end of file diff --git a/SS14.Labeller/Labelling/Labels/BranchLabel.cs b/SS14.Labeller/Labelling/Labels/BranchLabel.cs new file mode 100644 index 0000000..1333eee --- /dev/null +++ b/SS14.Labeller/Labelling/Labels/BranchLabel.cs @@ -0,0 +1,16 @@ +namespace SS14.Labeller.Labelling.Labels; + +public sealed class BranchLabel : LabelGenericBase +{ + private BranchLabel(string value) : base(value) + { + } + + const string Prefix = "Branch: "; + + public static readonly BranchLabel Stable = new(Prefix + "Stable"); + public static readonly BranchLabel Staging = new(Prefix + "Staging"); + + /// + public override bool AllowMultiple => false; +} \ No newline at end of file diff --git a/SS14.Labeller/Labelling/Labels/ChangesLabel.cs b/SS14.Labeller/Labelling/Labels/ChangesLabel.cs new file mode 100644 index 0000000..867fb99 --- /dev/null +++ b/SS14.Labeller/Labelling/Labels/ChangesLabel.cs @@ -0,0 +1,20 @@ +namespace SS14.Labeller.Labelling.Labels; + +public sealed class ChangesLabel : LabelGenericBase +{ + private ChangesLabel(string value) : base(value) + { + } + + const string Prefix = "Changes: "; + + public static readonly ChangesLabel Audio = new(Prefix + "Audio"); + public static readonly ChangesLabel Map = new(Prefix + "Map"); + public static readonly ChangesLabel NoCSharp = new(Prefix + "No C#"); + public static readonly ChangesLabel Shaders = new(Prefix + "Shaders"); + public static readonly ChangesLabel Sprites = new(Prefix + "Sprites"); + public static readonly ChangesLabel Ui = new(Prefix + "UI"); + + /// + public override bool AllowMultiple => true; +} \ No newline at end of file diff --git a/SS14.Labeller/Labelling/Labels/LabelGenericBase.cs b/SS14.Labeller/Labelling/Labels/LabelGenericBase.cs new file mode 100644 index 0000000..54f7b6f --- /dev/null +++ b/SS14.Labeller/Labelling/Labels/LabelGenericBase.cs @@ -0,0 +1,50 @@ +using System.Reflection; + +namespace SS14.Labeller.Labelling.Labels; + +public abstract class LabelBase(string value) +{ + public abstract bool AllowMultiple { get; } + + public string Value { get; } = value; + + public static implicit operator string(LabelBase myObject) + { + return myObject.Value; + } + + protected abstract LabelBase[] GetAll(); + + public bool TryGetFromString(string labelName, out LabelBase? foundLabel) + { + foundLabel = null; + foreach (var label in GetAll()) + { + if (labelName == label.Value) + { + foundLabel = label; + return true; + } + } + + return false; + } + + /// + public override string ToString() => Value; +} + +public abstract class LabelGenericBase(string value) : LabelBase(value) + where TLabel : LabelBase +{ + private static TLabel[]? _cached; + + protected override TLabel[] GetAll() + { + _cached ??= GetType().GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(x => x.FieldType == typeof(TLabel)) + .Select(x => (TLabel)x.GetValue(null)) + .ToArray(); + return _cached; + } +} \ No newline at end of file diff --git a/SS14.Labeller/Labelling/Labels/SizeLabel.cs b/SS14.Labeller/Labelling/Labels/SizeLabel.cs new file mode 100644 index 0000000..3b160d0 --- /dev/null +++ b/SS14.Labeller/Labelling/Labels/SizeLabel.cs @@ -0,0 +1,52 @@ +namespace SS14.Labeller.Labelling.Labels; + +public sealed class SizeLabel : LabelGenericBase +{ + private SizeLabel(string value) : base(value) + { + } + + const string Prefix = "size/"; + + public static readonly SizeLabel XS = new(Prefix + "XS"); + public static readonly SizeLabel S = new(Prefix + "S"); + public static readonly SizeLabel M = new(Prefix + "M"); + public static readonly SizeLabel L = new(Prefix + "L"); + public static readonly SizeLabel XL = new(Prefix + "XL"); + + private static readonly IReadOnlyDictionary Sizes = new Dictionary + { + { 0, XS }, + { 10, S }, + { 100, M }, + { 1000, L }, + { 5000, XL }, + }; + + public static bool IsSizeLabel(string? labelName) + { + if (labelName == null) + return false; + + return labelName.StartsWith(Prefix); + } + + public static SizeLabel? TryGetLabelFor(int totalDiff) + { + SizeLabel? sizeLabel = null; + // ReSharper disable once LoopCanBeConvertedToQuery no fuck you, the resulting LINQ query is unreadable + foreach (var kvp in Sizes.OrderByDescending(k => k.Key)) + { + if (totalDiff < kvp.Key) + continue; + + sizeLabel = kvp.Value; + break; + } + + return sizeLabel; + } + + /// + public override bool AllowMultiple => false; +} \ No newline at end of file diff --git a/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs b/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs new file mode 100644 index 0000000..0999790 --- /dev/null +++ b/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs @@ -0,0 +1,16 @@ +namespace SS14.Labeller.Labelling.Labels; + +public class StageOfWorkLabel : LabelGenericBase +{ + private const string Prefix = "S:"; + + private StageOfWorkLabel(string value) : base(value) + { + } + + public static readonly StageOfWorkLabel RequireReview = new(Prefix + "Needs Review"); // no idea why its called this + public static readonly StageOfWorkLabel AwaitingChanges = new(Prefix + "Awaiting Changes"); + + /// + public override bool AllowMultiple => false; +} \ No newline at end of file diff --git a/SS14.Labeller/Labelling/Labels/StatusLabel.cs b/SS14.Labeller/Labelling/Labels/StatusLabel.cs new file mode 100644 index 0000000..17b23f3 --- /dev/null +++ b/SS14.Labeller/Labelling/Labels/StatusLabel.cs @@ -0,0 +1,17 @@ +namespace SS14.Labeller.Labelling.Labels; + +public class StatusLabel : LabelGenericBase +{ + private StatusLabel(string value) : base(value) + { + } + + /// + public override bool AllowMultiple => true; + + const string Prefix = "S: "; + + public static readonly StatusLabel Untriaged = new(Prefix + "Untriaged"); + public static readonly StatusLabel Approved = new(Prefix + "Approved"); + public static readonly StatusLabel UndergoingDiscussion = new(Prefix + "Undergoing Discussion"); +} \ No newline at end of file diff --git a/SS14.Labeller/Labels/BranchLabels.cs b/SS14.Labeller/Labels/BranchLabels.cs deleted file mode 100644 index 5ec270b..0000000 --- a/SS14.Labeller/Labels/BranchLabels.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SS14.Labeller.Labels; - -public static class BranchLabels -{ - const string Prefix = "Branch: "; - - public const string Stable = Prefix + "Stable"; - public const string Staging = Prefix + "Staging"; -} \ No newline at end of file diff --git a/SS14.Labeller/Labels/ChangesLabels.cs b/SS14.Labeller/Labels/ChangesLabels.cs deleted file mode 100644 index ce495cc..0000000 --- a/SS14.Labeller/Labels/ChangesLabels.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace SS14.Labeller.Labels; - -public static class ChangesLabels -{ - const string Prefix = "Changes: "; - - public const string Audio = Prefix + "Audio"; - public const string Map = Prefix + "Map"; - public const string NoCSharp = Prefix + "No C#"; - public const string Shaders = Prefix + "Shaders"; - public const string Sprites = Prefix + "Sprites"; - public const string Ui = Prefix + "UI"; -} \ No newline at end of file diff --git a/SS14.Labeller/Labels/SizeLabels.cs b/SS14.Labeller/Labels/SizeLabels.cs deleted file mode 100644 index 3099595..0000000 --- a/SS14.Labeller/Labels/SizeLabels.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace SS14.Labeller.Labels; - -public static class SizeLabels -{ - private static readonly IReadOnlyDictionary Sizes = new Dictionary() - { - { 0, Prefix + "XS" }, - { 10, Prefix + "S" }, - { 100, Prefix + "M" }, - { 1000, Prefix + "L" }, - { 5000, Prefix + "XL" }, - }; - - public const string Prefix = "size/"; - - public static string? TryGetLabelFor(int totalDiff) - { - string? sizeLabel = null; - // ReSharper disable once LoopCanBeConvertedToQuery no fuck you, the resulting LINQ query is unreadable - foreach (var kvp in Sizes.OrderByDescending(k => k.Key)) - { - if (totalDiff < kvp.Key) - continue; - - sizeLabel = kvp.Value; - break; - } - - return sizeLabel; - } -} \ No newline at end of file diff --git a/SS14.Labeller/Labels/StatusLabels.cs b/SS14.Labeller/Labels/StatusLabels.cs deleted file mode 100644 index c029529..0000000 --- a/SS14.Labeller/Labels/StatusLabels.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SS14.Labeller.Labels; - -public static class StatusLabels -{ - const string Prefix = "S: "; - - public const string Untriaged = Prefix + "Untriaged"; - public const string RequireReview = Prefix + "Needs Review"; // no idea why its called this - public const string AwaitingChanges = Prefix + "Awaiting Changes"; - public const string Approved = Prefix + "Approved"; - public const string UndergoingDiscussion = Prefix + "Undergoing Discussion"; -} \ No newline at end of file diff --git a/SS14.Labeller/Models/IPullRequestAwareEvent.cs b/SS14.Labeller/Models/IPullRequestAwareEvent.cs new file mode 100644 index 0000000..d327bbf --- /dev/null +++ b/SS14.Labeller/Models/IPullRequestAwareEvent.cs @@ -0,0 +1,7 @@ +namespace SS14.Labeller.Models; + +public interface IPullRequestAwareEvent +{ + GithubRepo Repository { get; } + PullRequest PullRequest { get; } +} \ No newline at end of file diff --git a/SS14.Labeller/Models/PullRequestEvent.cs b/SS14.Labeller/Models/PullRequestEvent.cs index 028a63a..e48a3b2 100644 --- a/SS14.Labeller/Models/PullRequestEvent.cs +++ b/SS14.Labeller/Models/PullRequestEvent.cs @@ -2,7 +2,7 @@ namespace SS14.Labeller.Models; -public class PullRequestEvent : EventBase +public class PullRequestEvent : EventBase, IPullRequestAwareEvent { [JsonPropertyName("pull_request")] public required PullRequest PullRequest { get; set; } diff --git a/SS14.Labeller/Models/PullRequestReviewEvent.cs b/SS14.Labeller/Models/PullRequestReviewEvent.cs index 9ceb868..0ff2fee 100644 --- a/SS14.Labeller/Models/PullRequestReviewEvent.cs +++ b/SS14.Labeller/Models/PullRequestReviewEvent.cs @@ -2,7 +2,7 @@ namespace SS14.Labeller.Models; -public class PullRequestReviewEvent : EventBase +public class PullRequestReviewEvent : EventBase, IPullRequestAwareEvent { [JsonPropertyName("pull_request")] public required PullRequest PullRequest { get; set; } diff --git a/SS14.Labeller/Registry.cs b/SS14.Labeller/Registry.cs index e3efcc0..89ea163 100644 --- a/SS14.Labeller/Registry.cs +++ b/SS14.Labeller/Registry.cs @@ -4,6 +4,7 @@ using SS14.Labeller.DiscourseApi; using SS14.Labeller.GitHubApi; using SS14.Labeller.Handlers; +using SS14.Labeller.Labelling; using SS14.Labeller.Repository; using System.Net.Http.Headers; @@ -59,11 +60,14 @@ public static void RegisterDependencies(this IServiceCollection service, IConfig service.AddHttpClient(); } + service.AddSingleton(); + service.AddSingleton(); service.AddSingleton(); service.AddSingleton(); service.AddSingleton(); + service.AddHostedService(); service.AddSingleton>( From 816b7003a03637467ddfa5f5e4313cae7bf79840 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Sun, 17 Aug 2025 00:44:09 +0300 Subject: [PATCH 04/26] refactor: extracted pull request sub-events logic --- .../Handlers/LabelPullRequestHandler.cs | 184 +++++++++--------- SS14.Labeller/Labelling/Labels/SizeLabel.cs | 12 +- SS14.Labeller/Messages/StatusMessages.cs | 36 ++-- SS14.Labeller/Models/PullRequestEvent.cs | 33 +++- 4 files changed, 148 insertions(+), 117 deletions(-) diff --git a/SS14.Labeller/Handlers/LabelPullRequestHandler.cs b/SS14.Labeller/Handlers/LabelPullRequestHandler.cs index 43f3de5..f344ba8 100644 --- a/SS14.Labeller/Handlers/LabelPullRequestHandler.cs +++ b/SS14.Labeller/Handlers/LabelPullRequestHandler.cs @@ -41,96 +41,19 @@ protected override async Task HandleInternal(PullRequestEvent request, Cancellat // basic labels var repository = request.Repository; - if (request.Action is "opened") + await (request.EventType switch { - if (labels.Length == 0) - await labelManager.EnsureLabeled(request, StatusLabel.Untriaged, ct); - - var targetBranch = pr.Base.Ref; - if (targetBranch == "stable") - await labelManager.EnsureLabeled(request, BranchLabel.Stable, ct); - else if (targetBranch == "staging") - await labelManager.EnsureLabeled(request, BranchLabel.Staging, ct); - - var isMaintainer = await client.IsMaintainer(pr.User.Login, repository, ct); - if (isMaintainer) - await labelManager.EnsureLabeled(request, StatusLabel.Approved, ct); - - await labelManager.EnsureLabeled(request, StageOfWorkLabel.RequireReview, ct); - } - - if (request.Action is "synchronize" or "opened") - { - var totalDiff = pr.Additions + pr.Deletions; - var sizeLabel = SizeLabel.TryGetLabelFor(totalDiff); - if (sizeLabel is not null) - { - await labelManager.EnsureLabeled(request, sizeLabel, ct); - } - } - - if (request.Action is "labeled") - { - // ReSharper disable once NullableWarningSuppressionIsUsed - if (request.Label!.Name == StatusLabel.UndergoingDiscussion && _discourseConfig.Enable) - { - var exists = await topicsRepository.HasTopic(repoOwner, repoName, prNumber, ct); - if (exists) - { - // need to make a new discussion. - var topic = await discourseClient.CreateTopic( - _discourseConfig.DiscussionCategoryId, - StatusMessages.DiscourseTopicBody - .Replace("{link}", request.PullRequest.Url), - request.PullRequest.Title, - ct); - - var topicLink = _discourseConfig.Url + topic.PostUrl[1..]; - - await client.AddComment(repository, prNumber, StatusMessages.StartedDiscussion + topicLink, ct); - - - await discourseClient.ApplyTags(topic.TopicId, ct, _discourseConfig.Tagging.PrOpenTag); - - await topicsRepository.Add(repoOwner, repoName, prNumber, topic.TopicId, ct); - } - } - } - - if (request.Action is "review_requested") - { - if (await client.IsMaintainer(request.RequestedReviewer!.Login, repository, ct)) - { - await labelManager.EnsureLabeled(request, StageOfWorkLabel.RequireReview, ct); - } - } - - if (request.Action is "closed" && !string.IsNullOrEmpty(request.PullRequest.MergedAt)) - { - // PR got merged - var discussion = await topicsRepository.FindTopicIdForDiscussion(repoOwner, repoName, prNumber, ct); - - if (discussion is not null) - { - // we have an active discussion, lets mark it as doneso - await discourseClient.ApplyTags(discussion.Value, ct, _discourseConfig.Tagging.PrMergedTag); - } - - if (labels.Contains(StatusLabel.Untriaged)) - { - await client.AddComment(repository, prNumber, StatusMessages.UntriagedPullRequestMergedComment, ct); - } - } - else if (request.Action is "closed") + PullRequestEventType.Labelled => OnLabelAdd(request, ct, repoOwner, repoName, prNumber, repository), + PullRequestEventType.ClosedRejected => OnClosed(ct, repoOwner, repoName, prNumber), + PullRequestEventType.ClosedMerged => OnMerged(ct, repoOwner, repoName, prNumber, labels, repository), + PullRequestEventType.Opened => OnOpened(request, ct, labels, pr, repository), + PullRequestEventType.ReviewRequested => OnReviewRequested(request, ct, repository), + }); + + var totalDiff = pr.Additions + pr.Deletions; + if (SizeLabel.TryGetLabelFor(totalDiff, out var sizeLabel)) { - // pr was just closed, not merged. - var discussion = - await topicsRepository.FindTopicIdForDiscussion(repoOwner, repoName, prNumber, ct); - - if (discussion is not null) - { - await discourseClient.ApplyTags(discussion.Value, ct, _discourseConfig.Tagging.PrClosedTag); - } + await labelManager.EnsureLabeled(request, sizeLabel, ct); } var changedFiles = await client.GetChangedFiles(repository, prNumber, ct); @@ -158,9 +81,94 @@ private async Task EnsureChangesLabels( matcher = matcher.AddInclude(pattern); } + // either we found and it is not inverted, or we did not and it is inverted if ((matcher.Match(changedFiles).HasMatches && !isInverted) || (!matcher.Match(changedFiles).HasMatches && isInverted)) await labelManager.EnsureLabeled(request, label, ct); else await labelManager.EnsureNotLabeled(request, label, ct); } + + private async Task OnClosed(CancellationToken ct, string repoOwner, string repoName, int prNumber) + { + // pr was just closed, not merged. + var discussion = await topicsRepository.FindTopicIdForDiscussion(repoOwner, repoName, prNumber, ct); + + if (discussion is not null) + { + await discourseClient.ApplyTags(discussion.Value, ct, _discourseConfig.Tagging.PrClosedTag); + } + } + + private async Task OnMerged(CancellationToken ct, string repoOwner, string repoName, int prNumber, string?[] labels, GithubRepo repository) + { + // PR got merged + var discussion = await topicsRepository.FindTopicIdForDiscussion(repoOwner, repoName, prNumber, ct); + + if (discussion is not null) + { + // we have an active discussion, lets mark it as doneso + await discourseClient.ApplyTags(discussion.Value, ct, _discourseConfig.Tagging.PrMergedTag); + } + + if (labels.Contains(StatusLabel.Untriaged)) + { + await client.AddComment(repository, prNumber, StatusMessages.UntriagedPullRequestMergedComment, ct); + } + } + + private async Task OnReviewRequested(PullRequestEvent request, CancellationToken ct, GithubRepo repository) + { + if (await client.IsMaintainer(request.RequestedReviewer!.Login, repository, ct)) + { + await labelManager.EnsureLabeled(request, StageOfWorkLabel.RequireReview, ct); + } + } + + private async Task OnLabelAdd(PullRequestEvent request, CancellationToken ct, string repoOwner, string repoName, int prNumber, GithubRepo repository) + { + if(!_discourseConfig.Enable) + return; + + // ReSharper disable once NullableWarningSuppressionIsUsed + if (request.Label?.Name != StatusLabel.UndergoingDiscussion) + return; + + var exists = await topicsRepository.HasTopic(repoOwner, repoName, prNumber, ct); + if (exists) + { + // need to make a new discussion. + var topic = await discourseClient.CreateTopic( + _discourseConfig.DiscussionCategoryId, + StatusMessages.DiscourseTopicBody(request.PullRequest.Url), + request.PullRequest.Title, + ct + ); + + var topicLink = _discourseConfig.Url + topic.PostUrl[1..]; + + await client.AddComment(repository, prNumber, StatusMessages.StartedDiscussion(topicLink), ct); + + await discourseClient.ApplyTags(topic.TopicId, ct, _discourseConfig.Tagging.PrOpenTag); + + await topicsRepository.Add(repoOwner, repoName, prNumber, topic.TopicId, ct); + } + } + + private async Task OnOpened(PullRequestEvent request, CancellationToken ct, string?[] labels, PullRequest pr, GithubRepo repository) + { + if (labels.Length == 0) + await labelManager.EnsureLabeled(request, StatusLabel.Untriaged, ct); + + var targetBranch = pr.Base.Ref; + if (targetBranch == "stable") + await labelManager.EnsureLabeled(request, BranchLabel.Stable, ct); + else if (targetBranch == "staging") + await labelManager.EnsureLabeled(request, BranchLabel.Staging, ct); + + var isMaintainer = await client.IsMaintainer(pr.User.Login, repository, ct); + if (isMaintainer) + await labelManager.EnsureLabeled(request, StatusLabel.Approved, ct); + + await labelManager.EnsureLabeled(request, StageOfWorkLabel.RequireReview, ct); + } } \ No newline at end of file diff --git a/SS14.Labeller/Labelling/Labels/SizeLabel.cs b/SS14.Labeller/Labelling/Labels/SizeLabel.cs index 3b160d0..ec9f10a 100644 --- a/SS14.Labeller/Labelling/Labels/SizeLabel.cs +++ b/SS14.Labeller/Labelling/Labels/SizeLabel.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace SS14.Labeller.Labelling.Labels; public sealed class SizeLabel : LabelGenericBase @@ -31,20 +33,20 @@ public static bool IsSizeLabel(string? labelName) return labelName.StartsWith(Prefix); } - public static SizeLabel? TryGetLabelFor(int totalDiff) + public static bool TryGetLabelFor(int totalDiff, [NotNullWhen(true)] out SizeLabel? label) { - SizeLabel? sizeLabel = null; + label = null; // ReSharper disable once LoopCanBeConvertedToQuery no fuck you, the resulting LINQ query is unreadable foreach (var kvp in Sizes.OrderByDescending(k => k.Key)) { if (totalDiff < kvp.Key) continue; - sizeLabel = kvp.Value; - break; + label = kvp.Value; + return true; } - return sizeLabel; + return false; } /// diff --git a/SS14.Labeller/Messages/StatusMessages.cs b/SS14.Labeller/Messages/StatusMessages.cs index 9e2e577..89912fb 100644 --- a/SS14.Labeller/Messages/StatusMessages.cs +++ b/SS14.Labeller/Messages/StatusMessages.cs @@ -8,25 +8,25 @@ public static class StatusMessages [Beep Boop](https://github.com/space-wizards/SS14.Labeller), this comment was made automatically. """; - public const string DiscourseTopicBody = - """ - {link} - - [poll type=regular results=always public=true chartType=bar groups=maintainers] - # What to do? - * Merge - * Close - * Other (Comment) - [/poll] - """; + public static string DiscourseTopicBody(string link) => + $""" + {link} + + [poll type=regular results=always public=true chartType=bar groups=maintainers] + # What to do? + * Merge + * Close + * Other (Comment) + [/poll] + """; - public const string StartedDiscussion = - """ - A discussion thread has been opened. - - Please limit all further game design discussion to the following Topic: - - """; + public static string StartedDiscussion(string topicName) => + $""" + A discussion thread has been opened. + + Please limit all further game design discussion to the following Topic: {topicName} + + """; public const string UntriagedPullRequestMergedComment = """ diff --git a/SS14.Labeller/Models/PullRequestEvent.cs b/SS14.Labeller/Models/PullRequestEvent.cs index e48a3b2..2acd366 100644 --- a/SS14.Labeller/Models/PullRequestEvent.cs +++ b/SS14.Labeller/Models/PullRequestEvent.cs @@ -4,8 +4,7 @@ namespace SS14.Labeller.Models; public class PullRequestEvent : EventBase, IPullRequestAwareEvent { - [JsonPropertyName("pull_request")] - public required PullRequest PullRequest { get; set; } + [JsonPropertyName("pull_request")] public required PullRequest PullRequest { get; set; } /// /// The requested reviewer if the action was "review_requested". @@ -18,6 +17,30 @@ public class PullRequestEvent : EventBase, IPullRequestAwareEvent /// [JsonPropertyName("label")] public Label? Label { get; set; } + + [JsonIgnore] + public PullRequestEventType EventType + { + get + { + if (Action == "opened") return PullRequestEventType.Opened; + if (Action == "labeled") return PullRequestEventType.Labelled; + if (Action == "review_requested") return PullRequestEventType.ReviewRequested; + if (Action == "closed" && !string.IsNullOrEmpty(PullRequest.MergedAt)) return PullRequestEventType.ClosedMerged; + if (Action == "closed") return PullRequestEventType.ClosedRejected; + return PullRequestEventType.None; + } + } +} + +public enum PullRequestEventType +{ + None, + Labelled, + ClosedRejected, + ClosedMerged, + Opened, + ReviewRequested } public class PullRequest @@ -34,12 +57,10 @@ public class PullRequest public int Deletions { get; set; } - [JsonPropertyName("merged_at")] - public string? MergedAt { get; set; } + [JsonPropertyName("merged_at")] public string? MergedAt { get; set; } public required string Title { get; set; } - [JsonPropertyName("html_url")] - public required string Url { get; set; } + [JsonPropertyName("html_url")] public required string Url { get; set; } } public class BranchInfo From 1cfb02f3ecf42c41ef2be8ee0799c4318b04e1dc Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Sun, 17 Aug 2025 11:48:18 +0300 Subject: [PATCH 05/26] fix calling OnReviewRequested when synced --- SS14.Labeller/Handlers/LabelPullRequestHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/SS14.Labeller/Handlers/LabelPullRequestHandler.cs b/SS14.Labeller/Handlers/LabelPullRequestHandler.cs index f344ba8..e2ef100 100644 --- a/SS14.Labeller/Handlers/LabelPullRequestHandler.cs +++ b/SS14.Labeller/Handlers/LabelPullRequestHandler.cs @@ -48,6 +48,7 @@ protected override async Task HandleInternal(PullRequestEvent request, Cancellat PullRequestEventType.ClosedMerged => OnMerged(ct, repoOwner, repoName, prNumber, labels, repository), PullRequestEventType.Opened => OnOpened(request, ct, labels, pr, repository), PullRequestEventType.ReviewRequested => OnReviewRequested(request, ct, repository), + PullRequestEventType.None => Task.CompletedTask }); var totalDiff = pr.Additions + pr.Deletions; From d406e53966ea1ea72ca6c10eccab7e0355c76cae Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Sun, 17 Aug 2025 12:11:58 +0300 Subject: [PATCH 06/26] refactor: fix test data --- .../Resources/pull_request_review_request_changes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SS14.Labeller.Tests/Resources/pull_request_review_request_changes.json b/SS14.Labeller.Tests/Resources/pull_request_review_request_changes.json index 0ca397d..6399256 100644 --- a/SS14.Labeller.Tests/Resources/pull_request_review_request_changes.json +++ b/SS14.Labeller.Tests/Resources/pull_request_review_request_changes.json @@ -79,7 +79,7 @@ "assignees": [], "requested_reviewers": [], "requested_teams": [], - "labels": [], + "labels": [ { "name": "S:Needs Review" } ], "milestone": null, "draft": false, "commits_url": "https://api.github.com/repos/Fildrance/SS14.Labeller/pulls/4/commits", From 64a66cfa982fb1f21bd03b3410dcb274e9fd3cf2 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Sun, 17 Aug 2025 15:21:39 +0300 Subject: [PATCH 07/26] refactor: warning as errors + warnings and startup failures fixups --- .../DatabaseMigrationApplyingBackgroundService.cs | 2 +- SS14.Labeller/DiscourseApi/DiscourseClient.cs | 4 ++-- SS14.Labeller/Handlers/LabelPullRequestHandler.cs | 2 +- SS14.Labeller/Labelling/Labels/LabelGenericBase.cs | 12 +++++++----- SS14.Labeller/Labelling/Labels/SizeLabel.cs | 8 -------- SS14.Labeller/Registry.cs | 10 ++++++---- SS14.Labeller/SS14.Labeller.csproj | 1 + 7 files changed, 18 insertions(+), 21 deletions(-) diff --git a/SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs b/SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs index 734e4b2..885560c 100644 --- a/SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs +++ b/SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs @@ -11,7 +11,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var connectionString = configuration.GetConnectionString("Default") ?? "Data Source=Application.db"; await using var con = new SqliteConnection(connectionString); - + con.Open(); Migrator.Migrate(con, "SS14.Labeller.Database.Migrations", logger); } } \ No newline at end of file diff --git a/SS14.Labeller/DiscourseApi/DiscourseClient.cs b/SS14.Labeller/DiscourseApi/DiscourseClient.cs index d4db2bf..efc7dab 100644 --- a/SS14.Labeller/DiscourseApi/DiscourseClient.cs +++ b/SS14.Labeller/DiscourseApi/DiscourseClient.cs @@ -64,7 +64,7 @@ public class UpdatePostRequest { // ReSharper disable InconsistentNaming public required int category_id { get; set; } - public string[] tags { get; set; } - public string title { get; set; } + public required string[] tags { get; set; } + public required string title { get; set; } // ReSharper restore InconsistentNaming } \ No newline at end of file diff --git a/SS14.Labeller/Handlers/LabelPullRequestHandler.cs b/SS14.Labeller/Handlers/LabelPullRequestHandler.cs index e2ef100..bdddff0 100644 --- a/SS14.Labeller/Handlers/LabelPullRequestHandler.cs +++ b/SS14.Labeller/Handlers/LabelPullRequestHandler.cs @@ -48,7 +48,7 @@ protected override async Task HandleInternal(PullRequestEvent request, Cancellat PullRequestEventType.ClosedMerged => OnMerged(ct, repoOwner, repoName, prNumber, labels, repository), PullRequestEventType.Opened => OnOpened(request, ct, labels, pr, repository), PullRequestEventType.ReviewRequested => OnReviewRequested(request, ct, repository), - PullRequestEventType.None => Task.CompletedTask + _ => Task.CompletedTask }); var totalDiff = pr.Additions + pr.Deletions; diff --git a/SS14.Labeller/Labelling/Labels/LabelGenericBase.cs b/SS14.Labeller/Labelling/Labels/LabelGenericBase.cs index 54f7b6f..eab6ff1 100644 --- a/SS14.Labeller/Labelling/Labels/LabelGenericBase.cs +++ b/SS14.Labeller/Labelling/Labels/LabelGenericBase.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; namespace SS14.Labeller.Labelling.Labels; @@ -15,7 +16,7 @@ public static implicit operator string(LabelBase myObject) protected abstract LabelBase[] GetAll(); - public bool TryGetFromString(string labelName, out LabelBase? foundLabel) + public bool TryGetFromString(string labelName, [NotNullWhen(true)] out LabelBase? foundLabel) { foundLabel = null; foreach (var label in GetAll()) @@ -34,6 +35,7 @@ public bool TryGetFromString(string labelName, out LabelBase? foundLabel) public override string ToString() => Value; } +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] public abstract class LabelGenericBase(string value) : LabelBase(value) where TLabel : LabelBase { @@ -42,9 +44,9 @@ public abstract class LabelGenericBase(string value) : LabelBase(value) protected override TLabel[] GetAll() { _cached ??= GetType().GetFields(BindingFlags.Static | BindingFlags.Public) - .Where(x => x.FieldType == typeof(TLabel)) - .Select(x => (TLabel)x.GetValue(null)) - .ToArray(); + .Where(x => x.FieldType == typeof(TLabel)) + .Select(x => (TLabel)x.GetValue(null)!) + .ToArray(); return _cached; } } \ No newline at end of file diff --git a/SS14.Labeller/Labelling/Labels/SizeLabel.cs b/SS14.Labeller/Labelling/Labels/SizeLabel.cs index ec9f10a..6ffc7f3 100644 --- a/SS14.Labeller/Labelling/Labels/SizeLabel.cs +++ b/SS14.Labeller/Labelling/Labels/SizeLabel.cs @@ -25,14 +25,6 @@ private SizeLabel(string value) : base(value) { 5000, XL }, }; - public static bool IsSizeLabel(string? labelName) - { - if (labelName == null) - return false; - - return labelName.StartsWith(Prefix); - } - public static bool TryGetLabelFor(int totalDiff, [NotNullWhen(true)] out SizeLabel? label) { label = null; diff --git a/SS14.Labeller/Registry.cs b/SS14.Labeller/Registry.cs index 89ea163..8a07c2b 100644 --- a/SS14.Labeller/Registry.cs +++ b/SS14.Labeller/Registry.cs @@ -14,13 +14,15 @@ public static class Registry { public static void RegisterDependencies(this IServiceCollection service, IConfiguration configuration) { +#pragma warning disable IL2026 service.AddOptions() - .Bind(configuration.GetSection(DiscourseConfig.Name)) - .ValidateDataAnnotations(); + .Bind(configuration.GetSection(DiscourseConfig.Name)) + .ValidateDataAnnotations(); service.AddOptions() - .Bind(configuration.GetSection(GitHubConfig.Name)) - .ValidateDataAnnotations(); + .Bind(configuration.GetSection(GitHubConfig.Name)) + .ValidateDataAnnotations(); +#pragma warning restore IL2026 service.ConfigureHttpJsonOptions(options => { diff --git a/SS14.Labeller/SS14.Labeller.csproj b/SS14.Labeller/SS14.Labeller.csproj index 4353ac7..4adab45 100644 --- a/SS14.Labeller/SS14.Labeller.csproj +++ b/SS14.Labeller/SS14.Labeller.csproj @@ -7,6 +7,7 @@ true true $(InterceptorsPreviewNamespaces);Dapper.AOT + true From 46cf052e991c7c52e9edad58d4c747a871419e11 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Sun, 17 Aug 2025 15:28:38 +0300 Subject: [PATCH 08/26] fix missing space for stage of work labels --- .../Resources/pull_request_review_request_changes.json | 2 +- SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SS14.Labeller.Tests/Resources/pull_request_review_request_changes.json b/SS14.Labeller.Tests/Resources/pull_request_review_request_changes.json index 6399256..e20eed6 100644 --- a/SS14.Labeller.Tests/Resources/pull_request_review_request_changes.json +++ b/SS14.Labeller.Tests/Resources/pull_request_review_request_changes.json @@ -79,7 +79,7 @@ "assignees": [], "requested_reviewers": [], "requested_teams": [], - "labels": [ { "name": "S:Needs Review" } ], + "labels": [ { "name": "S: Needs Review" } ], "milestone": null, "draft": false, "commits_url": "https://api.github.com/repos/Fildrance/SS14.Labeller/pulls/4/commits", diff --git a/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs b/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs index 0999790..46a1d0f 100644 --- a/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs +++ b/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs @@ -2,7 +2,7 @@ namespace SS14.Labeller.Labelling.Labels; public class StageOfWorkLabel : LabelGenericBase { - private const string Prefix = "S:"; + private const string Prefix = "S: "; private StageOfWorkLabel(string value) : base(value) { From 6fcbab24d680ada0e4a3d0949fc494b3233c3f46 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Sun, 17 Aug 2025 16:35:33 +0300 Subject: [PATCH 09/26] more DynamicallyAccessedMembers marking for safety! --- SS14.Labeller/Labelling/Labels/BranchLabel.cs | 3 +++ SS14.Labeller/Labelling/Labels/ChangesLabel.cs | 3 +++ SS14.Labeller/Labelling/Labels/SizeLabel.cs | 1 + SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs | 3 +++ SS14.Labeller/Labelling/Labels/StatusLabel.cs | 3 +++ 5 files changed, 13 insertions(+) diff --git a/SS14.Labeller/Labelling/Labels/BranchLabel.cs b/SS14.Labeller/Labelling/Labels/BranchLabel.cs index 1333eee..103ac28 100644 --- a/SS14.Labeller/Labelling/Labels/BranchLabel.cs +++ b/SS14.Labeller/Labelling/Labels/BranchLabel.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + namespace SS14.Labeller.Labelling.Labels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] public sealed class BranchLabel : LabelGenericBase { private BranchLabel(string value) : base(value) diff --git a/SS14.Labeller/Labelling/Labels/ChangesLabel.cs b/SS14.Labeller/Labelling/Labels/ChangesLabel.cs index 867fb99..0f19098 100644 --- a/SS14.Labeller/Labelling/Labels/ChangesLabel.cs +++ b/SS14.Labeller/Labelling/Labels/ChangesLabel.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + namespace SS14.Labeller.Labelling.Labels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] public sealed class ChangesLabel : LabelGenericBase { private ChangesLabel(string value) : base(value) diff --git a/SS14.Labeller/Labelling/Labels/SizeLabel.cs b/SS14.Labeller/Labelling/Labels/SizeLabel.cs index 6ffc7f3..85d779d 100644 --- a/SS14.Labeller/Labelling/Labels/SizeLabel.cs +++ b/SS14.Labeller/Labelling/Labels/SizeLabel.cs @@ -2,6 +2,7 @@ namespace SS14.Labeller.Labelling.Labels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] public sealed class SizeLabel : LabelGenericBase { private SizeLabel(string value) : base(value) diff --git a/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs b/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs index 46a1d0f..4d609dc 100644 --- a/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs +++ b/SS14.Labeller/Labelling/Labels/StageOfWorkLabel.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + namespace SS14.Labeller.Labelling.Labels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] public class StageOfWorkLabel : LabelGenericBase { private const string Prefix = "S: "; diff --git a/SS14.Labeller/Labelling/Labels/StatusLabel.cs b/SS14.Labeller/Labelling/Labels/StatusLabel.cs index 17b23f3..1d3d37b 100644 --- a/SS14.Labeller/Labelling/Labels/StatusLabel.cs +++ b/SS14.Labeller/Labelling/Labels/StatusLabel.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + namespace SS14.Labeller.Labelling.Labels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] public class StatusLabel : LabelGenericBase { private StatusLabel(string value) : base(value) From 31a6d35da48b3c9d563cd4309bfb153e0a5a539f Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Tue, 19 Aug 2025 22:16:01 +0300 Subject: [PATCH 10/26] refactor: introduced fluent migrator, postgresql, docker-compose with initial database --- README.md | 44 +++++++-- .../CustomWebApplicationFactory.cs | 9 +- SS14.Labeller.sln | 5 +- SS14.Labeller/Database/DatabaseMigration.cs | 26 +++++ ...abaseMigrationApplyingBackgroundService.cs | 17 ---- .../001_CreateTable_DiscourseTopics.cs | 34 +++++++ .../Database/Migrations/Script0001_Init.sql | 11 --- SS14.Labeller/Database/Migrator.cs | 95 ------------------- SS14.Labeller/Program.cs | 3 + SS14.Labeller/Registry.cs | 17 +++- .../Commands/InsertDiscourseTopicCommand.cs | 2 +- .../Repository/Queries/FindTopicQuery.cs | 4 +- SS14.Labeller/Repository/RepositoryBase.cs | 4 +- SS14.Labeller/SS14.Labeller.csproj | 9 +- SS14.Labeller/appsettings.json | 4 +- build/database-init/init.sql | 1 + docker-compose-debug.yml | 12 +++ docker-compose.yml | 19 ++-- 18 files changed, 155 insertions(+), 161 deletions(-) create mode 100644 SS14.Labeller/Database/DatabaseMigration.cs delete mode 100644 SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs create mode 100644 SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs delete mode 100644 SS14.Labeller/Database/Migrations/Script0001_Init.sql delete mode 100644 SS14.Labeller/Database/Migrator.cs create mode 100644 build/database-init/init.sql create mode 100644 docker-compose-debug.yml diff --git a/README.md b/README.md index 4e54dc0..72fa960 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,21 @@ Simple ASP.NET NativeAOT application for labelling our [content repository](https://github.com/space-wizards/space-station-14) -## Usage +## Features + +Main features of application are related to reaction to github events: +* Event of issue creation will lead to issue getting marked as ```S: Untriaged``` label +* Event of leaving review under pull request will lead to marking it with ```S: Approved``` or ```S: Awaiting Changes``` labels +* Events, related to pull request support following labels: + * marked with ```size/XS```, ```size/S```, ```size/M```, ```size/L``` or ```size/XL``` for size (depending on a total lines changed in PR) + * marked with ```Changes: Audio```, ```Changes: Map```, ```Changes: NoCSharp```, ```Changes: Shaders```, ```Changes: Sprites```, ```Changes: Ui``` labels based on extensions and routes of files that were affected by PR + * marked ```Branch: Stable``` or ```Branch: Staging``` if PR is targeting specific branch + * marked with ```S: Untriaged``` on creation + * marked ```S: Approved``` if created by maintainer + * can create discourse thread when PR is labelled with ```S: Undergoing Discussion``` by gh users (will post link to discussion in comment of PR) + * marks with either ```S: Needs Review``` or ```S: Awaiting Changes``` depending on review state that maintainers leave on PR (set to ```S: Needs Review``` on opening) + +## Usage Create the a file called appsettings.json like so: ```json @@ -37,9 +51,9 @@ To set the port, use the `ASPNETCORE_URLS` environment variable, e.g. `ASPNETCOR *DiscussionCategoryId*: What category to send new discussion Topics in. You can get this by opening the Topic in your browser and the number in the URL is the category ID.\ *Url*: The Forum URL. Must end with a trailing slash. -## Building +## Building and setting up hooks -To build the application, use the following command: +To build application for release and deployment, use the following command: ```bash dotnet publish ./SS14.Labeller -c Release -r --self-contained true /p:PublishAot=true @@ -54,17 +68,27 @@ The token must have the `Issues` and `Pull requests` scopes enabled for read and ## Testing and debug +To run application locally you can launch run it as any other dotnet application. To set up its dependencies (database) locally you can run docker-compose: +``` +docker-compose up -d +``` +This will run local postgres to which labeller will try attach upon launching and when running integration tests. + +### Forwarding github events for local debugging You will need set up proxy for messages from GitHub to your local machine. For that you can use https://smee.io 1. Install Smee cli using npm (if you don't have it - follow those instructions [here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm))\ ``` npm install --global smee-client ``` 2. Visit https://smee.io, click 'Start a new channel' and copy the link that will be generated. -3. In your console use Smee cli to start proxy forwarding to your local machine\ -```smee -u https://smee.io/{place-you-channel-code-here} -t http://127.0.0.1:5000/webhook``` +3. In your console use Smee cli to start proxy forwarding to your local machine: ``` smee -u https://smee.io/{place-you-channel-code-here} -t http://127.0.0.1:5000/webhook ``` + Upon launching, it will output line like + + ``` Forwarding https://smee.io/{place-you-channel-code-here} to http://127.0.0.1:5000/webhook ``` + That means that every message it receives, including * Its payload * Its headers @@ -93,4 +117,12 @@ Now we need set up the repository. Create new repository or use an existing one. 3. Copy your smee.io url into Payload URL field 4. Select content-type ```application/json``` 5. Input any "secret" word or phrase into the Secret field. -6. In the block 'Which events would you like to trigger this webhook?' select 'Let me select individual events' and check the events as listed in the Usage section. \ No newline at end of file +6. In the block 'Which events would you like to trigger this webhook?' select 'Let me select individual events' and check the events as listed in the Usage section. + +### Debugging behaviour in container + +To debug app behaviour in container environment you can use docker-compose-debug.yaml (it will pick latest version of labeller app from image repository): +``` +docker compose -f docker-compose-debug.yml up -d +``` +Or build Dockerfile yourself to try out your local code. \ No newline at end of file diff --git a/SS14.Labeller.Tests/CustomWebApplicationFactory.cs b/SS14.Labeller.Tests/CustomWebApplicationFactory.cs index 02c0eb7..2f888af 100644 --- a/SS14.Labeller.Tests/CustomWebApplicationFactory.cs +++ b/SS14.Labeller.Tests/CustomWebApplicationFactory.cs @@ -1,14 +1,12 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; +using FluentMigrator.Runner; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; using NSubstitute; -using SS14.Labeller.Database; using SS14.Labeller.DiscourseApi; using SS14.Labeller.GitHubApi; using SS14.Labeller.Repository; @@ -35,11 +33,6 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) sp.Replace(new ServiceDescriptor(typeof(IGitHubApiClient), GitHubApiClient)); sp.Replace(new ServiceDescriptor(typeof(IDiscourseClient), DiscourseClient)); sp.Replace(new ServiceDescriptor(typeof(IDiscourseTopicsRepository), TopicsRepository)); - var hostedServiceDescriptor = sp.First(d => - d.ServiceType == typeof(IHostedService) && - d.ImplementationType == typeof(DatabaseMigrationApplyingBackgroundService)); // Replace YourHostedService with the actual type - - sp.Remove(hostedServiceDescriptor); }).ConfigureAppConfiguration((context, configurationBuilder) => { configurationBuilder.AddInMemoryCollection(new Dictionary diff --git a/SS14.Labeller.sln b/SS14.Labeller.sln index 736dd99..43f20fc 100644 --- a/SS14.Labeller.sln +++ b/SS14.Labeller.sln @@ -1,12 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.13.35818.85 d17.13 +VisualStudioVersion = 17.13.35818.85 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SS14.Labeller", "SS14.Labeller\SS14.Labeller.csproj", "{23A01977-7FE4-491F-BBE8-E8C412D8D753}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + Dockerfile = Dockerfile + docker-compose-debug.yml = docker-compose-debug.yml README.md = README.md EndProjectSection EndProject diff --git a/SS14.Labeller/Database/DatabaseMigration.cs b/SS14.Labeller/Database/DatabaseMigration.cs new file mode 100644 index 0000000..8a8be96 --- /dev/null +++ b/SS14.Labeller/Database/DatabaseMigration.cs @@ -0,0 +1,26 @@ +using FluentMigrator.Runner; + +namespace SS14.Labeller.Database; + +public sealed class DatabaseMigration +{ + public static void MigrateDatabase(IServiceProvider sp) + { + using (var scope = sp.CreateScope()) + { + // Put the database update into a scope to ensure + // that all resources will be disposed. + UpdateDatabase(scope.ServiceProvider); + } + } + + /// Update the database + private static void UpdateDatabase(IServiceProvider serviceProvider) + { + // Instantiate the runner + var runner = serviceProvider.GetRequiredService(); + + // Execute the migrations + runner.MigrateUp(); + } +} diff --git a/SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs b/SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs deleted file mode 100644 index 885560c..0000000 --- a/SS14.Labeller/Database/DatabaseMigrationApplyingBackgroundService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Data.Sqlite; - -namespace SS14.Labeller.Database; - -public sealed class DatabaseMigrationApplyingBackgroundService(ILogger logger, IConfiguration configuration) - : BackgroundService -{ - /// - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - var connectionString = configuration.GetConnectionString("Default") - ?? "Data Source=Application.db"; - await using var con = new SqliteConnection(connectionString); - con.Open(); - Migrator.Migrate(con, "SS14.Labeller.Database.Migrations", logger); - } -} \ No newline at end of file diff --git a/SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs b/SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs new file mode 100644 index 0000000..d5ff73d --- /dev/null +++ b/SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs @@ -0,0 +1,34 @@ +using FluentMigrator; +using System.Diagnostics.CodeAnalysis; + +namespace SS14.Labeller.Database.Migrations; + +[Migration(20250818113000)] +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] +public class CreateTableDiscourseTopics : Migration +{ + public const string TableName = "discussions"; + public const string SchemaName = "discourse"; + + public override void Up() + { + Create.Schema(SchemaName); + + Create.Table(TableName) + .InSchema(SchemaName) + .WithColumn("id").AsInt32().PrimaryKey().Identity() + .WithColumn("repo_owner").AsString().NotNullable() + .WithColumn("repo_name").AsString().NotNullable() + .WithColumn("issue_number").AsInt32().NotNullable() + .WithColumn("topic_id").AsInt32().NotNullable(); + + Create.Index("discussion_repo_owner_ix").OnTable(TableName).InSchema(SchemaName).OnColumn("repo_owner"); + Create.Index("discussion_issue_number_ix").OnTable(TableName).InSchema(SchemaName).OnColumn("issue_number"); + Create.Index("discussion_repo_name_ix").OnTable(TableName).InSchema(SchemaName).OnColumn("repo_name"); + } + + public override void Down() + { + // no-op + } +} \ No newline at end of file diff --git a/SS14.Labeller/Database/Migrations/Script0001_Init.sql b/SS14.Labeller/Database/Migrations/Script0001_Init.sql deleted file mode 100644 index c448df1..0000000 --- a/SS14.Labeller/Database/Migrations/Script0001_Init.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE Discussions( - DiscussionId INTEGER PRIMARY KEY, - RepoOwner TEXT NOT NULL, - RepoName TEXT NOT NULL, - IssueNumber INTEGER NOT NULL, - TopicId INTEGER NOT NULL -); - -CREATE INDEX Discussions_Owner ON Discussions(RepoOwner); -CREATE INDEX Discussions_Name ON Discussions(RepoName); -CREATE INDEX Discussions_Issue ON Discussions(IssueNumber); \ No newline at end of file diff --git a/SS14.Labeller/Database/Migrator.cs b/SS14.Labeller/Database/Migrator.cs deleted file mode 100644 index b46e2ae..0000000 --- a/SS14.Labeller/Database/Migrator.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Dapper; -using Microsoft.Data.Sqlite; - -namespace SS14.Labeller.Database; - -public static class Migrator -{ - public static bool Migrate(SqliteConnection connection, string prefix, ILogger logger) - { - logger.LogDebug("Migrating with prefix {Prefix}", prefix); - - using var transaction = connection.BeginTransaction(); - - connection.Execute(""" - CREATE TABLE IF NOT EXISTS SchemaVersions( - SchemaVersionID INTEGER PRIMARY KEY, - ScriptName TEXT NOT NULL, - Applied DATETIME NOT NULL - ); - """, - transaction: transaction); - - var appliedScripts = connection.Query( - "SELECT ScriptName FROM main.SchemaVersions", - transaction: transaction); - - // ReSharper disable once InvokeAsExtensionMethod - var scriptsToApply = MigrationFileScriptList(prefix).ExceptBy(appliedScripts, s => s.name).OrderBy(x => x.name); - - var success = true; - foreach (var (name, script) in scriptsToApply) - { - logger.LogInformation("Applying migration {Transaction}!", name); - transaction.Save(name); - - try - { - var code = script.Up(connection); - - connection.Execute(code, transaction: transaction); - - connection.Execute( - "INSERT INTO SchemaVersions(ScriptName, Applied) VALUES (@Script, datetime('now'))", - new { Script = name }, - transaction); - - transaction.Release(name); - } - catch (Exception e) - { - logger.LogError(e, "Exception during migration {Transaction}, rolling back...!", name); - transaction.Rollback(name); - success = false; - break; - } - } - - logger.LogInformation("Committing migrations"); - transaction.Commit(); - return success; - } - - private static IEnumerable<(string name, IMigrationScript)> MigrationFileScriptList(string prefix) - { - var assembly = typeof(Migrator).Assembly; - foreach (var resourceName in assembly.GetManifestResourceNames()) - { - if (!resourceName.EndsWith(".sql") || !resourceName.StartsWith(prefix)) - continue; - - var index = resourceName.LastIndexOf('.', resourceName.Length - 5, resourceName.Length - 4); - index += 1; - - var name = resourceName[(index + "Script".Length)..^4]; - - using var reader = new StreamReader(assembly.GetManifestResourceStream(resourceName)!); - var scriptContents = reader.ReadToEnd(); - yield return (name, new FileMigrationScript(scriptContents)); - } - } - - public interface IMigrationScript - { - string Up(SqliteConnection connection); - } - - private sealed class FileMigrationScript : IMigrationScript - { - private readonly string _code; - - public FileMigrationScript(string code) => _code = code; - - public string Up(SqliteConnection connection) => _code; - } -} \ No newline at end of file diff --git a/SS14.Labeller/Program.cs b/SS14.Labeller/Program.cs index 3ade6ef..d2b2529 100644 --- a/SS14.Labeller/Program.cs +++ b/SS14.Labeller/Program.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using SS14.Labeller.Configuration; +using SS14.Labeller.Database; using SS14.Labeller.Handlers; using SS14.Labeller.Helpers; @@ -26,6 +27,8 @@ public static void Main(string[] args) var app = builder.Build(); + DatabaseMigration.MigrateDatabase(app.Services); + app.UseHttpLogging(); app.MapGet("/", () => Results.Ok("Nik is a cat!")); diff --git a/SS14.Labeller/Registry.cs b/SS14.Labeller/Registry.cs index 8a07c2b..f9b0fee 100644 --- a/SS14.Labeller/Registry.cs +++ b/SS14.Labeller/Registry.cs @@ -7,6 +7,7 @@ using SS14.Labeller.Labelling; using SS14.Labeller.Repository; using System.Net.Http.Headers; +using FluentMigrator.Runner; namespace SS14.Labeller; @@ -70,11 +71,23 @@ public static void RegisterDependencies(this IServiceCollection service, IConfig service.AddSingleton(); - service.AddHostedService(); - service.AddSingleton>( sp => sp.GetServices() .ToDictionary(x => x.EventType) ); + + var connectionString = configuration.GetConnectionString("Default") + ?? "Data Source=Application.db"; + + service.AddFluentMigratorCore() + .ConfigureRunner(rb => rb + // Add SQLite support to FluentMigrator + .AddPostgres() + // Set the connection string + .WithGlobalConnectionString(connectionString) + // Define the assembly containing the migrations, maintenance migrations and other customizations + .ScanIn(typeof(DatabaseMigration).Assembly).For.All()) + // Enable logging to console in the FluentMigrator way + .AddLogging(lb => lb.AddFluentMigratorConsole()); } } \ No newline at end of file diff --git a/SS14.Labeller/Repository/Commands/InsertDiscourseTopicCommand.cs b/SS14.Labeller/Repository/Commands/InsertDiscourseTopicCommand.cs index 4a46c72..a99d61c 100644 --- a/SS14.Labeller/Repository/Commands/InsertDiscourseTopicCommand.cs +++ b/SS14.Labeller/Repository/Commands/InsertDiscourseTopicCommand.cs @@ -20,7 +20,7 @@ public override async Task Execute(DbConnection connection, CancellationTok } private const string Sql = $""" - INSERT INTO Discussions (RepoOwner, RepoName, IssueNumber, TopicId) + INSERT INTO discourse.discussions (repo_owner, repo_name, issue_number, topic_id) VALUES (@{nameof(RepoOwner)}, @{nameof(RepoName)}, @{nameof(IssueNumber)}, @{nameof(TopicId)}); """; diff --git a/SS14.Labeller/Repository/Queries/FindTopicQuery.cs b/SS14.Labeller/Repository/Queries/FindTopicQuery.cs index 2d8665a..3ce99b5 100644 --- a/SS14.Labeller/Repository/Queries/FindTopicQuery.cs +++ b/SS14.Labeller/Repository/Queries/FindTopicQuery.cs @@ -18,8 +18,8 @@ public class FindTopicQuery : DatabaseQueryBase } private const string Sql = $""" - SELECT TopicId FROM Discussions - WHERE RepoOwner = @{nameof(RepoOwner)} AND RepoName = @{nameof(RepoName)} AND IssueNumber = @{nameof(IssueNumber)} + SELECT topic_id FROM discourse.discussions + WHERE repo_owner = @{nameof(RepoOwner)} AND repo_name = @{nameof(RepoName)} AND issue_number = @{nameof(IssueNumber)} """; /// diff --git a/SS14.Labeller/Repository/RepositoryBase.cs b/SS14.Labeller/Repository/RepositoryBase.cs index d636243..8dc9307 100644 --- a/SS14.Labeller/Repository/RepositoryBase.cs +++ b/SS14.Labeller/Repository/RepositoryBase.cs @@ -1,5 +1,5 @@ using System.Data.Common; -using Microsoft.Data.Sqlite; +using Npgsql; namespace SS14.Labeller.Repository; @@ -9,7 +9,7 @@ protected DbConnection OpenConnection() { var connectionString = configuration.GetConnectionString("Default") ?? "Data Source=Application.db"; - var con = new SqliteConnection(connectionString); + var con = new NpgsqlConnection(connectionString); con.Open(); return con; } diff --git a/SS14.Labeller/SS14.Labeller.csproj b/SS14.Labeller/SS14.Labeller.csproj index 4adab45..c999288 100644 --- a/SS14.Labeller/SS14.Labeller.csproj +++ b/SS14.Labeller/SS14.Labeller.csproj @@ -10,15 +10,14 @@ true - - - - - + + + + diff --git a/SS14.Labeller/appsettings.json b/SS14.Labeller/appsettings.json index b266ddb..049921c 100644 --- a/SS14.Labeller/appsettings.json +++ b/SS14.Labeller/appsettings.json @@ -1,5 +1,5 @@ { - "ConnectionStrings":{ - "Default": "Data Source=data/Application.db" + "ConnectionStrings": { + "Default": "Host=localhost;Port=5432;Username=labeller_user;Password=example;Database=labeller;" } } \ No newline at end of file diff --git a/build/database-init/init.sql b/build/database-init/init.sql new file mode 100644 index 0000000..09c6531 --- /dev/null +++ b/build/database-init/init.sql @@ -0,0 +1 @@ + CREATE DATABASE labeller; \ No newline at end of file diff --git a/docker-compose-debug.yml b/docker-compose-debug.yml new file mode 100644 index 0000000..da0313b --- /dev/null +++ b/docker-compose-debug.yml @@ -0,0 +1,12 @@ +services: + ss14-labeller: + image: ghcr.io/space-wizards/ss14.labeller:latest + container_name: ss14-labeller + restart: unless-stopped + ports: + - "5000:5000" + environment: + ASPNETCORE_URLS: http://+:5000 + volumes: + - ./appsettings.json:/app/appsettings.json + - ./data:/app/data \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index da0313b..658f2b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,13 @@ services: - ss14-labeller: - image: ghcr.io/space-wizards/ss14.labeller:latest - container_name: ss14-labeller - restart: unless-stopped - ports: - - "5000:5000" + postgres: + image: postgres:13.22-alpine3.22 + container_name: postgresql + restart: always environment: - ASPNETCORE_URLS: http://+:5000 + POSTGRES_DB: labeller + POSTGRES_USER: labeller_user + POSTGRES_PASSWORD: example + ports: + - "5432:5432" volumes: - - ./appsettings.json:/app/appsettings.json - - ./data:/app/data \ No newline at end of file + - ./build/database-init/init.sql:/docker-entrypoint-initdb.d/init.sql \ No newline at end of file From fcddee29adcfda23a382ea63ca481cbd3af24cec Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Tue, 19 Aug 2025 22:28:23 +0300 Subject: [PATCH 11/26] refactor: fix missing default database in pipeline --- .github/workflows/build-test.yml | 20 ++++++++++++++++++++ SS14.Labeller.sln | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 5cc1e52..de91f27 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -10,6 +10,21 @@ jobs: build: runs-on: ubuntu-latest + services: + postgres: + image: postgres:15 # Or your desired PostgreSQL version + env: + POSTGRES_USER: labeller_user + POSTGRES_PASSWORD: example + POSTGRES_DB: labeller + ports: + - 5432:5432 # Expose the PostgreSQL port + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - uses: actions/checkout@v2 - name: Setup .NET @@ -20,5 +35,10 @@ jobs: run: dotnet restore - name: Build run: dotnet build --configuration Release --no-restore + - name: Create default database + run: | + psql -h postgres -U labeller_user -c "CREATE DATABASE labeller;" + env: + PGPASSWORD: example - name: SS14.Labeller.Tests run: dotnet test SS14.Labeller.Tests/SS14.Labeller.Tests.csproj -v n \ No newline at end of file diff --git a/SS14.Labeller.sln b/SS14.Labeller.sln index 43f20fc..2944248 100644 --- a/SS14.Labeller.sln +++ b/SS14.Labeller.sln @@ -7,9 +7,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SS14.Labeller", "SS14.Label EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" ProjectSection(SolutionItems) = preProject + .github\workflows\build-test.yml = .github\workflows\build-test.yml + docker-compose-debug.yml = docker-compose-debug.yml docker-compose.yml = docker-compose.yml Dockerfile = Dockerfile - docker-compose-debug.yml = docker-compose-debug.yml + build\database-init\init.sql = build\database-init\init.sql README.md = README.md EndProjectSection EndProject From 9880d1acb81ce24265e36fa5d2237c138367cbbc Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Tue, 19 Aug 2025 22:40:20 +0300 Subject: [PATCH 12/26] fix: fix pipeline --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index de91f27..92958a4 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -37,7 +37,7 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Create default database run: | - psql -h postgres -U labeller_user -c "CREATE DATABASE labeller;" + psql -h localhost -U labeller_user -c "CREATE DATABASE labeller;" env: PGPASSWORD: example - name: SS14.Labeller.Tests From 0fcc230ea561b443be021cb456fe6f9c282ea990 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Tue, 19 Aug 2025 22:54:02 +0300 Subject: [PATCH 13/26] fix: fix pipeline --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 92958a4..b700dfc 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -37,7 +37,7 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Create default database run: | - psql -h localhost -U labeller_user -c "CREATE DATABASE labeller;" + psql -h localhost -U labeller_user -d postgres -c "CREATE DATABASE labeller;" env: PGPASSWORD: example - name: SS14.Labeller.Tests From 517c75778a952f1ab4db40a36f0e78bff66527d7 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Tue, 19 Aug 2025 22:58:25 +0300 Subject: [PATCH 14/26] create if not exists --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index b700dfc..47dc366 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -37,7 +37,7 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Create default database run: | - psql -h localhost -U labeller_user -d postgres -c "CREATE DATABASE labeller;" + psql -h localhost -U labeller_user -d postgres -c "SELECT 'CREATE DATABASE labeller' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'labeller');" env: PGPASSWORD: example - name: SS14.Labeller.Tests From 8449f5f87cf81d8c36dda0f4a861ad0e2db612f8 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Thu, 21 Aug 2025 23:36:28 +0300 Subject: [PATCH 15/26] refactor: test for pg sql repo --- .../CustomWebApplicationFactory.cs | 4 +- .../HandlersTests.Issue.cs} | 12 ++-- .../HandlersTests.PullRequest.cs} | 12 ++-- .../HandlersTests.PullRequestReview.cs} | 14 ++-- .../HandlersTests.cs} | 4 +- .../DiscourseTopicsRepositoryTests.cs | 67 +++++++++++++++++++ SS14.Labeller.Tests/TestSetup.cs | 21 ++++++ .../Repository/DiscourseTopicsRepository.cs | 3 +- .../Repository/Queries/FindTopicQuery.cs | 2 +- 9 files changed, 114 insertions(+), 25 deletions(-) rename SS14.Labeller.Tests/{IntegrationTests.Issue.cs => IntegrationTests/HandlersTests.Issue.cs} (94%) rename SS14.Labeller.Tests/{IntegrationTests.PullRequest.cs => IntegrationTests/HandlersTests.PullRequest.cs} (98%) rename SS14.Labeller.Tests/{IntegrationTests.PullRequestReview.cs => IntegrationTests/HandlersTests.PullRequestReview.cs} (98%) rename SS14.Labeller.Tests/{IntegrationTests.cs => IntegrationTests/HandlersTests.cs} (95%) create mode 100644 SS14.Labeller.Tests/IntegrationTests/Repository/DiscourseTopicsRepositoryTests.cs create mode 100644 SS14.Labeller.Tests/TestSetup.cs diff --git a/SS14.Labeller.Tests/CustomWebApplicationFactory.cs b/SS14.Labeller.Tests/CustomWebApplicationFactory.cs index 2f888af..3690997 100644 --- a/SS14.Labeller.Tests/CustomWebApplicationFactory.cs +++ b/SS14.Labeller.Tests/CustomWebApplicationFactory.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using FluentMigrator.Runner; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; @@ -10,6 +9,7 @@ using SS14.Labeller.DiscourseApi; using SS14.Labeller.GitHubApi; using SS14.Labeller.Repository; +using SS14.Labeller.Tests.IntegrationTests; namespace SS14.Labeller.Tests; @@ -41,7 +41,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) { "Discourse:Username", "aw" }, { "Discourse:DiscussionCategoryId", "42" }, { "Discourse:Url", "http://wa.wa" }, - { "GitHub:WebhookSecret", IntegrationTests.HookSecret }, + { "GitHub:WebhookSecret", HandlersTests.HookSecret }, { "GitHub:Token", "test-test" }, }); }); diff --git a/SS14.Labeller.Tests/IntegrationTests.Issue.cs b/SS14.Labeller.Tests/IntegrationTests/HandlersTests.Issue.cs similarity index 94% rename from SS14.Labeller.Tests/IntegrationTests.Issue.cs rename to SS14.Labeller.Tests/IntegrationTests/HandlersTests.Issue.cs index 74de488..9761749 100644 --- a/SS14.Labeller.Tests/IntegrationTests.Issue.cs +++ b/SS14.Labeller.Tests/IntegrationTests/HandlersTests.Issue.cs @@ -1,14 +1,14 @@ -using NSubstitute; -using NUnit.Framework; -using SS14.Labeller.Models; -using System.Net; +using System.Net; using System.Threading; using System.Threading.Tasks; +using NSubstitute; +using NUnit.Framework; using SS14.Labeller.Labelling.Labels; +using SS14.Labeller.Models; -namespace SS14.Labeller.Tests; +namespace SS14.Labeller.Tests.IntegrationTests; -public partial class IntegrationTests +public partial class HandlersTests { [Test] public async Task Issue_Created_AddedUntriagedLabel() diff --git a/SS14.Labeller.Tests/IntegrationTests.PullRequest.cs b/SS14.Labeller.Tests/IntegrationTests/HandlersTests.PullRequest.cs similarity index 98% rename from SS14.Labeller.Tests/IntegrationTests.PullRequest.cs rename to SS14.Labeller.Tests/IntegrationTests/HandlersTests.PullRequest.cs index 366cfba..df09e03 100644 --- a/SS14.Labeller.Tests/IntegrationTests.PullRequest.cs +++ b/SS14.Labeller.Tests/IntegrationTests/HandlersTests.PullRequest.cs @@ -1,14 +1,14 @@ -using NSubstitute; -using NUnit.Framework; -using SS14.Labeller.Models; -using System.Net; +using System.Net; using System.Threading; using System.Threading.Tasks; +using NSubstitute; +using NUnit.Framework; using SS14.Labeller.Labelling.Labels; +using SS14.Labeller.Models; -namespace SS14.Labeller.Tests; +namespace SS14.Labeller.Tests.IntegrationTests; -public partial class IntegrationTests +public partial class HandlersTests { [Test] public async Task PullRequest() diff --git a/SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs b/SS14.Labeller.Tests/IntegrationTests/HandlersTests.PullRequestReview.cs similarity index 98% rename from SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs rename to SS14.Labeller.Tests/IntegrationTests/HandlersTests.PullRequestReview.cs index ffeb1bc..ff1111b 100644 --- a/SS14.Labeller.Tests/IntegrationTests.PullRequestReview.cs +++ b/SS14.Labeller.Tests/IntegrationTests/HandlersTests.PullRequestReview.cs @@ -1,14 +1,14 @@ -using NSubstitute; -using NUnit.Framework; -using SS14.Labeller.Models; -using System.Net; -using System.Threading.Tasks; +using System.Net; using System.Threading; +using System.Threading.Tasks; +using NSubstitute; +using NUnit.Framework; using SS14.Labeller.Labelling.Labels; +using SS14.Labeller.Models; -namespace SS14.Labeller.Tests; +namespace SS14.Labeller.Tests.IntegrationTests; -public partial class IntegrationTests +public partial class HandlersTests { [Test] diff --git a/SS14.Labeller.Tests/IntegrationTests.cs b/SS14.Labeller.Tests/IntegrationTests/HandlersTests.cs similarity index 95% rename from SS14.Labeller.Tests/IntegrationTests.cs rename to SS14.Labeller.Tests/IntegrationTests/HandlersTests.cs index 0a2ac90..b07e1a7 100644 --- a/SS14.Labeller.Tests/IntegrationTests.cs +++ b/SS14.Labeller.Tests/IntegrationTests/HandlersTests.cs @@ -7,10 +7,10 @@ using NUnit.Framework; using SS14.Labeller.Helpers; -namespace SS14.Labeller.Tests; +namespace SS14.Labeller.Tests.IntegrationTests; [ExcludeFromCodeCoverage] -public partial class IntegrationTests +public partial class HandlersTests { public const string HookSecret = "asdasdasdasdasdasdasdadsadad"; diff --git a/SS14.Labeller.Tests/IntegrationTests/Repository/DiscourseTopicsRepositoryTests.cs b/SS14.Labeller.Tests/IntegrationTests/Repository/DiscourseTopicsRepositoryTests.cs new file mode 100644 index 0000000..61d4f12 --- /dev/null +++ b/SS14.Labeller.Tests/IntegrationTests/Repository/DiscourseTopicsRepositoryTests.cs @@ -0,0 +1,67 @@ +using System.Threading.Tasks; +using Dapper; +using Microsoft.Extensions.Configuration; +using Npgsql; +using NUnit.Framework; +using SS14.Labeller.Repository; + +namespace SS14.Labeller.Tests.IntegrationTests.Repository; + +public class DiscourseTopicsRepositoryTests +{ + private IDiscourseTopicsRepository _repository; + + [SetUp] + public void Setup() + { + _repository = new DiscourseTopicsRepository(TestSetup.Configuration); + + CleanUpDb(); + } + + [Test] + public async Task HasTopic_NoSuchParamsCombination_IsFalse() + { + // Arrange + + // Act + var actual = await _repository.HasTopic("some-random-owner", "some-random-name", 54353, default); + + // Assert + Assert.That(actual, Is.False); + } + + + [Test] + public async Task Add_DoesNotExist_IsAdded() + { + // Arrange + const string owner = "some-random-owner"; + const string repoName = "some-random-name"; + const int issueNumber = 54353; + var before = await _repository.HasTopic(owner, repoName, issueNumber, default); + + // Act + await _repository.Add(owner, repoName, issueNumber, 1231, default); + + // Assert + var after = await _repository.HasTopic(owner, repoName, issueNumber, default); + + Assert.That(before, Is.False); + Assert.That(after, Is.True); + } + + [TearDown] + public void TearDown() + { + CleanUpDb(); + } + + private static void CleanUpDb() + { + var connectionString = TestSetup.Configuration.GetConnectionString("Default"); + using var con = new NpgsqlConnection(connectionString); + con.Open(); + con.Execute("TRUNCATE TABLE discourse.discussions"); + } +} diff --git a/SS14.Labeller.Tests/TestSetup.cs b/SS14.Labeller.Tests/TestSetup.cs new file mode 100644 index 0000000..ee0a315 --- /dev/null +++ b/SS14.Labeller.Tests/TestSetup.cs @@ -0,0 +1,21 @@ +using System.IO; +using Microsoft.Extensions.Configuration; +using NUnit.Framework; + +namespace SS14.Labeller.Tests; + +[SetUpFixture] +public class TestSetup +{ + public static IConfiguration Configuration { get; private set; } + + [OneTimeSetUp] + public void RunBeforeAnyTests() + { + Configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .Build(); + } +} \ No newline at end of file diff --git a/SS14.Labeller/Repository/DiscourseTopicsRepository.cs b/SS14.Labeller/Repository/DiscourseTopicsRepository.cs index 35204ef..4724358 100644 --- a/SS14.Labeller/Repository/DiscourseTopicsRepository.cs +++ b/SS14.Labeller/Repository/DiscourseTopicsRepository.cs @@ -19,7 +19,8 @@ public class DiscourseTopicsRepository(IConfiguration configuration) public async Task HasTopic(string repoOwner, string repoName, int issueNumber, CancellationToken ct) { - return (await FindTopicIdForDiscussion(repoOwner, repoName, issueNumber, ct)).HasValue; + var foundId = await FindTopicIdForDiscussion(repoOwner, repoName, issueNumber, ct); + return foundId.HasValue; } public async Task Add(string owner, string name, int issueNumber, int topicId, CancellationToken ct) diff --git a/SS14.Labeller/Repository/Queries/FindTopicQuery.cs b/SS14.Labeller/Repository/Queries/FindTopicQuery.cs index 3ce99b5..5bbeabe 100644 --- a/SS14.Labeller/Repository/Queries/FindTopicQuery.cs +++ b/SS14.Labeller/Repository/Queries/FindTopicQuery.cs @@ -14,7 +14,7 @@ public class FindTopicQuery : DatabaseQueryBase public override async Task Query(DbConnection connection, CancellationToken ct) { var cd = GetCommand(ct); - return await connection.QueryFirstOrDefaultAsync(cd); + return await connection.QueryFirstOrDefaultAsync(cd); } private const string Sql = $""" From 7b71634a6ad85422ca6a0a76880aa3be4ecec0cf Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Sun, 7 Sep 2025 20:06:27 +0300 Subject: [PATCH 16/26] refactor: fix old postgres version, remove sql-related code leftovers --- .github/workflows/build-test.yml | 2 +- SS14.Labeller.Tests/CustomWebApplicationFactory.cs | 1 + SS14.Labeller.sln | 3 --- SS14.Labeller/Database/DatabaseMigration.cs | 10 ++++------ SS14.Labeller/Registry.cs | 9 ++++----- SS14.Labeller/Repository/RepositoryBase.cs | 5 ++++- docker-compose.yml | 2 +- 7 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 47dc366..654b957 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -12,7 +12,7 @@ jobs: services: postgres: - image: postgres:15 # Or your desired PostgreSQL version + image: postgres:16 # Or your desired PostgreSQL version env: POSTGRES_USER: labeller_user POSTGRES_PASSWORD: example diff --git a/SS14.Labeller.Tests/CustomWebApplicationFactory.cs b/SS14.Labeller.Tests/CustomWebApplicationFactory.cs index 3690997..74fe263 100644 --- a/SS14.Labeller.Tests/CustomWebApplicationFactory.cs +++ b/SS14.Labeller.Tests/CustomWebApplicationFactory.cs @@ -10,6 +10,7 @@ using SS14.Labeller.GitHubApi; using SS14.Labeller.Repository; using SS14.Labeller.Tests.IntegrationTests; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. namespace SS14.Labeller.Tests; diff --git a/SS14.Labeller.sln b/SS14.Labeller.sln index 2944248..5d91970 100644 --- a/SS14.Labeller.sln +++ b/SS14.Labeller.sln @@ -7,11 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SS14.Labeller", "SS14.Label EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" ProjectSection(SolutionItems) = preProject - .github\workflows\build-test.yml = .github\workflows\build-test.yml docker-compose-debug.yml = docker-compose-debug.yml docker-compose.yml = docker-compose.yml - Dockerfile = Dockerfile - build\database-init\init.sql = build\database-init\init.sql README.md = README.md EndProjectSection EndProject diff --git a/SS14.Labeller/Database/DatabaseMigration.cs b/SS14.Labeller/Database/DatabaseMigration.cs index 8a8be96..83612b0 100644 --- a/SS14.Labeller/Database/DatabaseMigration.cs +++ b/SS14.Labeller/Database/DatabaseMigration.cs @@ -6,12 +6,10 @@ public sealed class DatabaseMigration { public static void MigrateDatabase(IServiceProvider sp) { - using (var scope = sp.CreateScope()) - { - // Put the database update into a scope to ensure - // that all resources will be disposed. - UpdateDatabase(scope.ServiceProvider); - } + using var scope = sp.CreateScope(); + // Put the database update into a scope to ensure + // that all resources will be disposed. + UpdateDatabase(scope.ServiceProvider); } /// Update the database diff --git a/SS14.Labeller/Registry.cs b/SS14.Labeller/Registry.cs index 01db168..2b26514 100644 --- a/SS14.Labeller/Registry.cs +++ b/SS14.Labeller/Registry.cs @@ -77,17 +77,16 @@ public static void RegisterDependencies(this IServiceCollection service, IConfig ); var connectionString = configuration.GetConnectionString("Default") - ?? "Data Source=Application.db"; + ?? throw new InvalidOperationException( + "Failed to find 'Default' connection string " + + "from application configuration for database initialization." + ); service.AddFluentMigratorCore() .ConfigureRunner(rb => rb - // Add SQLite support to FluentMigrator .AddPostgres() - // Set the connection string .WithGlobalConnectionString(connectionString) - // Define the assembly containing the migrations, maintenance migrations and other customizations .ScanIn(typeof(DatabaseMigration).Assembly).For.All()) - // Enable logging to console in the FluentMigrator way .AddLogging(lb => lb.AddFluentMigratorConsole()); } } \ No newline at end of file diff --git a/SS14.Labeller/Repository/RepositoryBase.cs b/SS14.Labeller/Repository/RepositoryBase.cs index 8dc9307..b40e53a 100644 --- a/SS14.Labeller/Repository/RepositoryBase.cs +++ b/SS14.Labeller/Repository/RepositoryBase.cs @@ -8,7 +8,10 @@ public class RepositoryBase(IConfiguration configuration) protected DbConnection OpenConnection() { var connectionString = configuration.GetConnectionString("Default") - ?? "Data Source=Application.db"; + ?? throw new InvalidOperationException( + "Failed to find 'Default' connection string " + + "from application configuration for repository." + ); ; var con = new NpgsqlConnection(connectionString); con.Open(); return con; diff --git a/docker-compose.yml b/docker-compose.yml index 658f2b2..06c255c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: postgres: - image: postgres:13.22-alpine3.22 + image: postgres:16-alpine container_name: postgresql restart: always environment: From ddbde08a44100aad399dd2cc02d09f8e18da508e Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Mon, 8 Sep 2025 21:17:37 +0300 Subject: [PATCH 17/26] refactor: revert docker-compose changes --- Dockerfile | 2 +- README.md | 14 +++----------- SS14.Labeller.sln | 1 + SS14.Labeller/Program.cs | 2 -- SS14.Labeller/SS14.Labeller.csproj | 4 ---- docker-compose-debug.yml | 19 ++++++++++--------- docker-compose.yml | 19 +++++++++---------- 7 files changed, 24 insertions(+), 37 deletions(-) diff --git a/Dockerfile b/Dockerfile index ae96727..e3519b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN echo "Building for platform: $TARGETPLATFORM" \ "linux/arm64") export RID=linux-arm64 ;; \ *) echo "Unsupported TARGETPLATFORM: $TARGETPLATFORM" && exit 1 ;; \ esac \ - && dotnet publish -c Release -r $RID --self-contained true /p:PublishAot=true -o /app + && dotnet publish -c Release -r $RID --self-contained true -o /app FROM debian:bookworm-slim AS final WORKDIR /app diff --git a/README.md b/README.md index 72fa960..1b1a945 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ To set the port, use the `ASPNETCORE_URLS` environment variable, e.g. `ASPNETCOR To build application for release and deployment, use the following command: ```bash -dotnet publish ./SS14.Labeller -c Release -r --self-contained true /p:PublishAot=true +dotnet publish ./SS14.Labeller -c Release -r --self-contained true ``` Running the application is just like any other executable. On Unix systems, you may need to set the executable bit on the binary. @@ -70,7 +70,7 @@ The token must have the `Issues` and `Pull requests` scopes enabled for read and To run application locally you can launch run it as any other dotnet application. To set up its dependencies (database) locally you can run docker-compose: ``` -docker-compose up -d +docker compose -f docker-compose-debug.yml up -d ``` This will run local postgres to which labeller will try attach upon launching and when running integration tests. @@ -117,12 +117,4 @@ Now we need set up the repository. Create new repository or use an existing one. 3. Copy your smee.io url into Payload URL field 4. Select content-type ```application/json``` 5. Input any "secret" word or phrase into the Secret field. -6. In the block 'Which events would you like to trigger this webhook?' select 'Let me select individual events' and check the events as listed in the Usage section. - -### Debugging behaviour in container - -To debug app behaviour in container environment you can use docker-compose-debug.yaml (it will pick latest version of labeller app from image repository): -``` -docker compose -f docker-compose-debug.yml up -d -``` -Or build Dockerfile yourself to try out your local code. \ No newline at end of file +6. In the block 'Which events would you like to trigger this webhook?' select 'Let me select individual events' and check the events as listed in the Usage section. \ No newline at end of file diff --git a/SS14.Labeller.sln b/SS14.Labeller.sln index 5d91970..8defc13 100644 --- a/SS14.Labeller.sln +++ b/SS14.Labeller.sln @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject docker-compose-debug.yml = docker-compose-debug.yml docker-compose.yml = docker-compose.yml + Dockerfile = Dockerfile README.md = README.md EndProjectSection EndProject diff --git a/SS14.Labeller/Program.cs b/SS14.Labeller/Program.cs index f9d3802..d271379 100644 --- a/SS14.Labeller/Program.cs +++ b/SS14.Labeller/Program.cs @@ -7,8 +7,6 @@ using SS14.Labeller.Middlewares; using SS14.Labeller.Models; -[module:DapperAot] - namespace SS14.Labeller; public class Program diff --git a/SS14.Labeller/SS14.Labeller.csproj b/SS14.Labeller/SS14.Labeller.csproj index c999288..bcd6b89 100644 --- a/SS14.Labeller/SS14.Labeller.csproj +++ b/SS14.Labeller/SS14.Labeller.csproj @@ -4,9 +4,6 @@ enable enable true - true - true - $(InterceptorsPreviewNamespaces);Dapper.AOT true @@ -16,7 +13,6 @@ - diff --git a/docker-compose-debug.yml b/docker-compose-debug.yml index da0313b..e275c87 100644 --- a/docker-compose-debug.yml +++ b/docker-compose-debug.yml @@ -1,12 +1,13 @@ services: - ss14-labeller: - image: ghcr.io/space-wizards/ss14.labeller:latest - container_name: ss14-labeller - restart: unless-stopped - ports: - - "5000:5000" + postgres: + image: postgres:16-alpine + container_name: postgresql + restart: always environment: - ASPNETCORE_URLS: http://+:5000 + POSTGRES_DB: labeller + POSTGRES_USER: labeller_user + POSTGRES_PASSWORD: example + ports: + - "5432:5432" volumes: - - ./appsettings.json:/app/appsettings.json - - ./data:/app/data \ No newline at end of file + - ./build/database-init/init.sql:/docker-entrypoint-initdb.d/init.sql diff --git a/docker-compose.yml b/docker-compose.yml index 06c255c..da0313b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,12 @@ services: - postgres: - image: postgres:16-alpine - container_name: postgresql - restart: always - environment: - POSTGRES_DB: labeller - POSTGRES_USER: labeller_user - POSTGRES_PASSWORD: example + ss14-labeller: + image: ghcr.io/space-wizards/ss14.labeller:latest + container_name: ss14-labeller + restart: unless-stopped ports: - - "5432:5432" + - "5000:5000" + environment: + ASPNETCORE_URLS: http://+:5000 volumes: - - ./build/database-init/init.sql:/docker-entrypoint-initdb.d/init.sql \ No newline at end of file + - ./appsettings.json:/app/appsettings.json + - ./data:/app/data \ No newline at end of file From 56a8487cf2e33f4da65cd7cbdb26e72cb9a266e6 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Mon, 8 Sep 2025 21:19:13 +0300 Subject: [PATCH 18/26] refactor: removed aot related attribute --- .../Database/Migrations/001_CreateTable_DiscourseTopics.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs b/SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs index d5ff73d..255a04f 100644 --- a/SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs +++ b/SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs @@ -1,10 +1,8 @@ using FluentMigrator; -using System.Diagnostics.CodeAnalysis; namespace SS14.Labeller.Database.Migrations; [Migration(20250818113000)] -[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] public class CreateTableDiscourseTopics : Migration { public const string TableName = "discussions"; From 55235e8cb3883b8a7f1863a24aa7aca631561c90 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Mon, 29 Sep 2025 21:46:38 +0300 Subject: [PATCH 19/26] feat: use ef core migrations --- .../GitHubApi/GithubRetryHandlerTests.cs | 16 ++--- SS14.Labeller/Database/CustomDbContext.cs | 19 +++++ SS14.Labeller/Database/DatabaseMigration.cs | 12 ++-- .../DiscourseEntitiesContextConfiguration.cs | 21 ++++++ .../Database/Entities/DiscourseTopicEntity.cs | 12 ++++ SS14.Labeller/Database/Entities/EntityBase.cs | 6 ++ .../Database/IContextConfiguration.cs | 14 ++++ .../001_CreateTable_DiscourseTopics.cs | 32 --------- ...921_CreateTableDiscourseTopics.Designer.cs | 71 +++++++++++++++++++ ...250929183921_CreateTableDiscourseTopics.cs | 61 ++++++++++++++++ .../CustomDbContextModelSnapshot.cs | 68 ++++++++++++++++++ SS14.Labeller/Registry.cs | 21 +++--- SS14.Labeller/SS14.Labeller.csproj | 9 ++- 13 files changed, 304 insertions(+), 58 deletions(-) create mode 100644 SS14.Labeller/Database/CustomDbContext.cs create mode 100644 SS14.Labeller/Database/DiscourseEntitiesContextConfiguration.cs create mode 100644 SS14.Labeller/Database/Entities/DiscourseTopicEntity.cs create mode 100644 SS14.Labeller/Database/Entities/EntityBase.cs create mode 100644 SS14.Labeller/Database/IContextConfiguration.cs delete mode 100644 SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs create mode 100644 SS14.Labeller/Database/Migrations/20250929183921_CreateTableDiscourseTopics.Designer.cs create mode 100644 SS14.Labeller/Database/Migrations/20250929183921_CreateTableDiscourseTopics.cs create mode 100644 SS14.Labeller/Database/Migrations/CustomDbContextModelSnapshot.cs diff --git a/SS14.Labeller.Tests/GitHubApi/GithubRetryHandlerTests.cs b/SS14.Labeller.Tests/GitHubApi/GithubRetryHandlerTests.cs index 9ee15ab..0b5f298 100644 --- a/SS14.Labeller.Tests/GitHubApi/GithubRetryHandlerTests.cs +++ b/SS14.Labeller.Tests/GitHubApi/GithubRetryHandlerTests.cs @@ -36,7 +36,7 @@ public void Setup() public void SendAsync_SuccessfulRequest() { // Arrange - _mockInnerHandler.Send(Arg.Any(), Arg.Any()) + _mockInnerHandler.SendMock(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK })); var handler = new GithubRetryHandler(_mockInnerHandler, _config, _logger); @@ -46,14 +46,14 @@ public void SendAsync_SuccessfulRequest() var result = httpClient.SendAsync(_httpRequestMessage, default).Result; // Assert - Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); + Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); } [Test] public void SendAsync_NetworkErrorRetries() { // Arrange - _mockInnerHandler.Send(Arg.Any(), Arg.Any()) + _mockInnerHandler.SendMock(Arg.Any(), Arg.Any()) .Returns( _=> throw new HttpRequestException(HttpRequestError.ConnectionError), _=> Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK }) @@ -66,7 +66,7 @@ public void SendAsync_NetworkErrorRetries() var result = httpClient.SendAsync(_httpRequestMessage, default).Result; // Assert - Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); + Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); } [Test] @@ -84,7 +84,7 @@ public void SendAsync_CalculateNextRequestTimeWithRateLimits() var response2 = new HttpResponseMessage(HttpStatusCode.OK); - _mockInnerHandler.Send(Arg.Any(), Arg.Any()) + _mockInnerHandler.SendMock(Arg.Any(), Arg.Any()) .Returns(response1, response2); var handler = new GithubRetryHandler(_mockInnerHandler, _config, _logger); @@ -103,7 +103,7 @@ public void SendAsync_CalculateNextRequestTimeWithRateLimits() public void SendAsync_MaxRetryExceeded() { // Arrange - _mockInnerHandler.Send(Arg.Any(), Arg.Any()) + _mockInnerHandler.SendMock(Arg.Any(), Arg.Any()) .ThrowsAsync(new HttpRequestException(HttpRequestError.ConnectionError)); _gitHubConfig.MaxRetryAttempt = 2; @@ -125,10 +125,10 @@ public class MockHttpMessageHandler : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - return Send(request, cancellationToken); + return SendMock(request, cancellationToken); } - public virtual Task Send(HttpRequestMessage request, CancellationToken cancellationToken) + public virtual Task SendMock(HttpRequestMessage request, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/SS14.Labeller/Database/CustomDbContext.cs b/SS14.Labeller/Database/CustomDbContext.cs new file mode 100644 index 0000000..e3ba1a5 --- /dev/null +++ b/SS14.Labeller/Database/CustomDbContext.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; + +namespace SS14.Labeller.Database; + +/// Custom db-context that can be used in reusable way. Consumes all configurations that DI Container will pass. +public class CustomDbContext(DbContextOptions options, IEnumerable configurations) + : DbContext(options) +{ + private readonly IEnumerable _configurations = configurations ?? throw new ArgumentNullException(nameof(configurations)); + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + foreach (var config in _configurations) + { + config.Apply(modelBuilder); + } + } +} \ No newline at end of file diff --git a/SS14.Labeller/Database/DatabaseMigration.cs b/SS14.Labeller/Database/DatabaseMigration.cs index 83612b0..71f9930 100644 --- a/SS14.Labeller/Database/DatabaseMigration.cs +++ b/SS14.Labeller/Database/DatabaseMigration.cs @@ -1,4 +1,4 @@ -using FluentMigrator.Runner; +using Microsoft.EntityFrameworkCore; namespace SS14.Labeller.Database; @@ -15,10 +15,12 @@ public static void MigrateDatabase(IServiceProvider sp) /// Update the database private static void UpdateDatabase(IServiceProvider serviceProvider) { - // Instantiate the runner - var runner = serviceProvider.GetRequiredService(); + var contextFactory = serviceProvider.GetRequiredService>(); + using var context = contextFactory.CreateDbContext(); + var db = context.Database; - // Execute the migrations - runner.MigrateUp(); + db.EnsureCreated(); + + db.Migrate(); } } diff --git a/SS14.Labeller/Database/DiscourseEntitiesContextConfiguration.cs b/SS14.Labeller/Database/DiscourseEntitiesContextConfiguration.cs new file mode 100644 index 0000000..4914970 --- /dev/null +++ b/SS14.Labeller/Database/DiscourseEntitiesContextConfiguration.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using SS14.Labeller.Database.Entities; + +namespace SS14.Labeller.Database; + +public class DiscourseEntitiesContextConfiguration : IContextConfiguration +{ + public const string TableName = "discussions"; + + public const string SchemaName = "discourse"; + + /// + public void Apply(ModelBuilder modelBuilder) + { + var ent = modelBuilder.Entity() + .ToTable(TableName, SchemaName); + ent.HasIndex(x => x.RepoOwner); + ent.HasIndex(x => x.RepoName); + ent.HasIndex(x => x.IssueNumber); + } +} \ No newline at end of file diff --git a/SS14.Labeller/Database/Entities/DiscourseTopicEntity.cs b/SS14.Labeller/Database/Entities/DiscourseTopicEntity.cs new file mode 100644 index 0000000..1f21709 --- /dev/null +++ b/SS14.Labeller/Database/Entities/DiscourseTopicEntity.cs @@ -0,0 +1,12 @@ +namespace SS14.Labeller.Database.Entities; + +public class DiscourseTopicEntity : EntityBase +{ + public required string RepoOwner { get; set; } + + public required string RepoName{ get; set; } + + public required int IssueNumber{ get; set; } + + public required int TopicId { get; set; } +} \ No newline at end of file diff --git a/SS14.Labeller/Database/Entities/EntityBase.cs b/SS14.Labeller/Database/Entities/EntityBase.cs new file mode 100644 index 0000000..f74902a --- /dev/null +++ b/SS14.Labeller/Database/Entities/EntityBase.cs @@ -0,0 +1,6 @@ +namespace SS14.Labeller.Database.Entities; + +public class EntityBase +{ + public int Id { get; set; } +} \ No newline at end of file diff --git a/SS14.Labeller/Database/IContextConfiguration.cs b/SS14.Labeller/Database/IContextConfiguration.cs new file mode 100644 index 0000000..c7be526 --- /dev/null +++ b/SS14.Labeller/Database/IContextConfiguration.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace SS14.Labeller.Database; + +/// +/// Marker for types that can configure DbContext. Usually application will be one main DbContext, +/// for which every available implementation will be executed. +/// Interface that will help to apply all ef model configuration without interacting with one certain db-context. +/// +public interface IContextConfiguration +{ + /// Applies configuration to model. + void Apply(ModelBuilder modelBuilder); +} \ No newline at end of file diff --git a/SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs b/SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs deleted file mode 100644 index 255a04f..0000000 --- a/SS14.Labeller/Database/Migrations/001_CreateTable_DiscourseTopics.cs +++ /dev/null @@ -1,32 +0,0 @@ -using FluentMigrator; - -namespace SS14.Labeller.Database.Migrations; - -[Migration(20250818113000)] -public class CreateTableDiscourseTopics : Migration -{ - public const string TableName = "discussions"; - public const string SchemaName = "discourse"; - - public override void Up() - { - Create.Schema(SchemaName); - - Create.Table(TableName) - .InSchema(SchemaName) - .WithColumn("id").AsInt32().PrimaryKey().Identity() - .WithColumn("repo_owner").AsString().NotNullable() - .WithColumn("repo_name").AsString().NotNullable() - .WithColumn("issue_number").AsInt32().NotNullable() - .WithColumn("topic_id").AsInt32().NotNullable(); - - Create.Index("discussion_repo_owner_ix").OnTable(TableName).InSchema(SchemaName).OnColumn("repo_owner"); - Create.Index("discussion_issue_number_ix").OnTable(TableName).InSchema(SchemaName).OnColumn("issue_number"); - Create.Index("discussion_repo_name_ix").OnTable(TableName).InSchema(SchemaName).OnColumn("repo_name"); - } - - public override void Down() - { - // no-op - } -} \ No newline at end of file diff --git a/SS14.Labeller/Database/Migrations/20250929183921_CreateTableDiscourseTopics.Designer.cs b/SS14.Labeller/Database/Migrations/20250929183921_CreateTableDiscourseTopics.Designer.cs new file mode 100644 index 0000000..10a4832 --- /dev/null +++ b/SS14.Labeller/Database/Migrations/20250929183921_CreateTableDiscourseTopics.Designer.cs @@ -0,0 +1,71 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SS14.Labeller.Database; + +#nullable disable + +namespace SS14.Labeller.Database.Migrations +{ + [DbContext(typeof(CustomDbContext))] + [Migration("20250929183921_CreateTableDiscourseTopics")] + partial class CreateTableDiscourseTopics + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SS14.Labeller.Database.Entities.DiscourseTopicEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IssueNumber") + .HasColumnType("integer") + .HasColumnName("issue_number"); + + b.Property("RepoName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("repo_name"); + + b.Property("RepoOwner") + .IsRequired() + .HasColumnType("text") + .HasColumnName("repo_owner"); + + b.Property("TopicId") + .HasColumnType("integer") + .HasColumnName("topic_id"); + + b.HasKey("Id") + .HasName("pk_discussions"); + + b.HasIndex("IssueNumber") + .HasDatabaseName("ix_discussions_issue_number"); + + b.HasIndex("RepoName") + .HasDatabaseName("ix_discussions_repo_name"); + + b.HasIndex("RepoOwner") + .HasDatabaseName("ix_discussions_repo_owner"); + + b.ToTable("discussions", "discourse"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SS14.Labeller/Database/Migrations/20250929183921_CreateTableDiscourseTopics.cs b/SS14.Labeller/Database/Migrations/20250929183921_CreateTableDiscourseTopics.cs new file mode 100644 index 0000000..7d99d81 --- /dev/null +++ b/SS14.Labeller/Database/Migrations/20250929183921_CreateTableDiscourseTopics.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SS14.Labeller.Database.Migrations +{ + /// + public partial class CreateTableDiscourseTopics : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "discourse"); + + migrationBuilder.CreateTable( + name: "discussions", + schema: "discourse", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + repo_owner = table.Column(type: "text", nullable: false), + repo_name = table.Column(type: "text", nullable: false), + issue_number = table.Column(type: "integer", nullable: false), + topic_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_discussions", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_discussions_issue_number", + schema: "discourse", + table: "discussions", + column: "issue_number"); + + migrationBuilder.CreateIndex( + name: "ix_discussions_repo_name", + schema: "discourse", + table: "discussions", + column: "repo_name"); + + migrationBuilder.CreateIndex( + name: "ix_discussions_repo_owner", + schema: "discourse", + table: "discussions", + column: "repo_owner"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "discussions", + schema: "discourse"); + } + } +} diff --git a/SS14.Labeller/Database/Migrations/CustomDbContextModelSnapshot.cs b/SS14.Labeller/Database/Migrations/CustomDbContextModelSnapshot.cs new file mode 100644 index 0000000..10711c2 --- /dev/null +++ b/SS14.Labeller/Database/Migrations/CustomDbContextModelSnapshot.cs @@ -0,0 +1,68 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SS14.Labeller.Database; + +#nullable disable + +namespace SS14.Labeller.Database.Migrations +{ + [DbContext(typeof(CustomDbContext))] + partial class CustomDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SS14.Labeller.Database.Entities.DiscourseTopicEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IssueNumber") + .HasColumnType("integer") + .HasColumnName("issue_number"); + + b.Property("RepoName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("repo_name"); + + b.Property("RepoOwner") + .IsRequired() + .HasColumnType("text") + .HasColumnName("repo_owner"); + + b.Property("TopicId") + .HasColumnType("integer") + .HasColumnName("topic_id"); + + b.HasKey("Id") + .HasName("pk_discussions"); + + b.HasIndex("IssueNumber") + .HasDatabaseName("ix_discussions_issue_number"); + + b.HasIndex("RepoName") + .HasDatabaseName("ix_discussions_repo_name"); + + b.HasIndex("RepoOwner") + .HasDatabaseName("ix_discussions_repo_owner"); + + b.ToTable("discussions", "discourse"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SS14.Labeller/Registry.cs b/SS14.Labeller/Registry.cs index 36c4ee6..5ad3348 100644 --- a/SS14.Labeller/Registry.cs +++ b/SS14.Labeller/Registry.cs @@ -7,7 +7,7 @@ using SS14.Labeller.Labelling; using SS14.Labeller.Repository; using System.Net.Http.Headers; -using FluentMigrator.Runner; +using Microsoft.EntityFrameworkCore; using Polly; using Polly.Extensions.Http; @@ -80,18 +80,19 @@ public static void RegisterDependencies(this IServiceCollection service, IConfig .ToDictionary(x => x.CanHandleType) ); + var connectionString = configuration.GetConnectionString("Default") ?? throw new InvalidOperationException( - "Failed to find 'Default' connection string " + "Failed to find 'Default' connection string " + "from application configuration for database initialization." - ); - - service.AddFluentMigratorCore() - .ConfigureRunner(rb => rb - .AddPostgres() - .WithGlobalConnectionString(connectionString) - .ScanIn(typeof(DatabaseMigration).Assembly).For.All()) - .AddLogging(lb => lb.AddFluentMigratorConsole()); + ); + + service.AddPooledDbContextFactory( + optsBuilder => optsBuilder.UseNpgsql(connectionString) + .UseSnakeCaseNamingConvention() + ); + + service.AddSingleton(); } private static IAsyncPolicy GetDiscourseRetryPolicy(IServiceProvider sp) diff --git a/SS14.Labeller/SS14.Labeller.csproj b/SS14.Labeller/SS14.Labeller.csproj index b66db92..b566643 100644 --- a/SS14.Labeller/SS14.Labeller.csproj +++ b/SS14.Labeller/SS14.Labeller.csproj @@ -8,13 +8,16 @@ - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + From 94125c0524d66fb1527b922634f12d7a2ceda300 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Mon, 29 Sep 2025 23:43:09 +0300 Subject: [PATCH 20/26] fix: removed migration being executed twice --- .github/workflows/build-test.yml | 4 ++-- SS14.Labeller/Database/DatabaseMigration.cs | 7 ++----- SS14.Labeller/appsettings.json | 2 +- docker-compose-debug.yml | 4 ++-- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 654b957..718869d 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -14,8 +14,8 @@ jobs: postgres: image: postgres:16 # Or your desired PostgreSQL version env: - POSTGRES_USER: labeller_user - POSTGRES_PASSWORD: example + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres POSTGRES_DB: labeller ports: - 5432:5432 # Expose the PostgreSQL port diff --git a/SS14.Labeller/Database/DatabaseMigration.cs b/SS14.Labeller/Database/DatabaseMigration.cs index 71f9930..430885d 100644 --- a/SS14.Labeller/Database/DatabaseMigration.cs +++ b/SS14.Labeller/Database/DatabaseMigration.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; namespace SS14.Labeller.Database; @@ -17,10 +18,6 @@ private static void UpdateDatabase(IServiceProvider serviceProvider) { var contextFactory = serviceProvider.GetRequiredService>(); using var context = contextFactory.CreateDbContext(); - var db = context.Database; - - db.EnsureCreated(); - - db.Migrate(); + context.Database.Migrate(); } } diff --git a/SS14.Labeller/appsettings.json b/SS14.Labeller/appsettings.json index 049921c..5afedd4 100644 --- a/SS14.Labeller/appsettings.json +++ b/SS14.Labeller/appsettings.json @@ -1,5 +1,5 @@ { "ConnectionStrings": { - "Default": "Host=localhost;Port=5432;Username=labeller_user;Password=example;Database=labeller;" + "Default": "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=labeller;" } } \ No newline at end of file diff --git a/docker-compose-debug.yml b/docker-compose-debug.yml index e275c87..6ed1706 100644 --- a/docker-compose-debug.yml +++ b/docker-compose-debug.yml @@ -5,8 +5,8 @@ services: restart: always environment: POSTGRES_DB: labeller - POSTGRES_USER: labeller_user - POSTGRES_PASSWORD: example + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres ports: - "5432:5432" volumes: From 038e404acf4fee479254373e585ecca19e21f4d6 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Mon, 29 Sep 2025 23:46:18 +0300 Subject: [PATCH 21/26] fix: pipeline fixed (please?) --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 718869d..6d1b4ed 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -37,7 +37,7 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Create default database run: | - psql -h localhost -U labeller_user -d postgres -c "SELECT 'CREATE DATABASE labeller' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'labeller');" + psql -h localhost -U postgres -d postgres -c "SELECT 'CREATE DATABASE labeller' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'labeller');" env: PGPASSWORD: example - name: SS14.Labeller.Tests From 6fc6fcff57e15b3bc942e80f9a9da70f6da3b0ec Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Mon, 29 Sep 2025 23:50:01 +0300 Subject: [PATCH 22/26] fix: fix pipeline (pretty please) --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 6d1b4ed..237d2b2 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -37,7 +37,7 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Create default database run: | - psql -h localhost -U postgres -d postgres -c "SELECT 'CREATE DATABASE labeller' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'labeller');" + psql -h localhost -U postgres -d labeller -W postgres -c "SELECT 'CREATE DATABASE labeller' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'labeller');" env: PGPASSWORD: example - name: SS14.Labeller.Tests From 128ac4c9afa7aba8b0db9f5ec5883cf3390c68a7 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Mon, 29 Sep 2025 23:54:18 +0300 Subject: [PATCH 23/26] fix: fix pipeline prettypretty please --- .github/workflows/build-test.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 237d2b2..db9a7b3 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -35,10 +35,5 @@ jobs: run: dotnet restore - name: Build run: dotnet build --configuration Release --no-restore - - name: Create default database - run: | - psql -h localhost -U postgres -d labeller -W postgres -c "SELECT 'CREATE DATABASE labeller' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'labeller');" - env: - PGPASSWORD: example - name: SS14.Labeller.Tests run: dotnet test SS14.Labeller.Tests/SS14.Labeller.Tests.csproj -v n \ No newline at end of file From 528b3cf05b1be1bb5e55aba16567032bcce6f4e7 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Tue, 30 Sep 2025 00:07:35 +0300 Subject: [PATCH 24/26] refactor: fix deploy pipeline using NAOT --- .github/workflows/deploy.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5b5717e..26e999c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,14 +32,9 @@ jobs: with: dotnet-version: '9.0.x' - - name: Install NativeAOT Dependencies + - name: Publish app run: | - sudo apt-get update - sudo apt-get install -y clang zlib1g-dev - - - name: Publish NativeAOT app - run: | - dotnet publish ./SS14.Labeller -c Release -r ${{ matrix.rid }} --self-contained true /p:PublishAot=true -o publish + dotnet publish ./SS14.Labeller -c Release -r ${{ matrix.rid }} --self-contained true -o publish - name: Prepare release zip run: | From 75c14bede3bfcf2ff3af3df93fb88a8c6eb310c8 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Thu, 9 Oct 2025 14:44:41 +0300 Subject: [PATCH 25/26] fix: registered github retry handler --- .../GitHubApi/GithubRetryHandlerTests.cs | 28 +++++++++---------- .../DiscourseApi/DummyDiscourseClient.cs | 4 ++- SS14.Labeller/GitHubApi/GithubRetryHandler.cs | 2 +- SS14.Labeller/Registry.cs | 1 + 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/SS14.Labeller.Tests/GitHubApi/GithubRetryHandlerTests.cs b/SS14.Labeller.Tests/GitHubApi/GithubRetryHandlerTests.cs index 0b5f298..cfdced9 100644 --- a/SS14.Labeller.Tests/GitHubApi/GithubRetryHandlerTests.cs +++ b/SS14.Labeller.Tests/GitHubApi/GithubRetryHandlerTests.cs @@ -36,37 +36,37 @@ public void Setup() public void SendAsync_SuccessfulRequest() { // Arrange - _mockInnerHandler.SendMock(Arg.Any(), Arg.Any()) + _mockInnerHandler.Send(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK })); - var handler = new GithubRetryHandler(_mockInnerHandler, _config, _logger); + var handler = new GithubRetryHandler(_config, _logger) { InnerHandler = _mockInnerHandler }; var httpClient = new HttpClient(handler); // Act var result = httpClient.SendAsync(_httpRequestMessage, default).Result; // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); } [Test] public void SendAsync_NetworkErrorRetries() { // Arrange - _mockInnerHandler.SendMock(Arg.Any(), Arg.Any()) + _mockInnerHandler.Send(Arg.Any(), Arg.Any()) .Returns( - _=> throw new HttpRequestException(HttpRequestError.ConnectionError), - _=> Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK }) + _ => throw new HttpRequestException(HttpRequestError.ConnectionError), + _ => Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK }) ); - var handler = new GithubRetryHandler(_mockInnerHandler, _config, _logger); + var handler = new GithubRetryHandler(_config, _logger) { InnerHandler = _mockInnerHandler }; var httpClient = new HttpClient(handler); // Act var result = httpClient.SendAsync(_httpRequestMessage, default).Result; // Assert - Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); } [Test] @@ -84,10 +84,10 @@ public void SendAsync_CalculateNextRequestTimeWithRateLimits() var response2 = new HttpResponseMessage(HttpStatusCode.OK); - _mockInnerHandler.SendMock(Arg.Any(), Arg.Any()) + _mockInnerHandler.Send(Arg.Any(), Arg.Any()) .Returns(response1, response2); - var handler = new GithubRetryHandler(_mockInnerHandler, _config, _logger); + var handler = new GithubRetryHandler(_config, _logger) { InnerHandler = _mockInnerHandler }; var httpClient = new HttpClient(handler); // Act @@ -103,12 +103,12 @@ public void SendAsync_CalculateNextRequestTimeWithRateLimits() public void SendAsync_MaxRetryExceeded() { // Arrange - _mockInnerHandler.SendMock(Arg.Any(), Arg.Any()) + _mockInnerHandler.Send(Arg.Any(), Arg.Any()) .ThrowsAsync(new HttpRequestException(HttpRequestError.ConnectionError)); _gitHubConfig.MaxRetryAttempt = 2; - var handler = new GithubRetryHandler(_mockInnerHandler, _config, _logger); + var handler = new GithubRetryHandler(_config, _logger) { InnerHandler = _mockInnerHandler }; var httpClient = new HttpClient(handler); // Act & Assert @@ -125,10 +125,10 @@ public class MockHttpMessageHandler : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - return SendMock(request, cancellationToken); + return Send(request, cancellationToken); } - public virtual Task SendMock(HttpRequestMessage request, CancellationToken cancellationToken) + public virtual Task Send(HttpRequestMessage request, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/SS14.Labeller/DiscourseApi/DummyDiscourseClient.cs b/SS14.Labeller/DiscourseApi/DummyDiscourseClient.cs index 5ce1ff4..98babff 100644 --- a/SS14.Labeller/DiscourseApi/DummyDiscourseClient.cs +++ b/SS14.Labeller/DiscourseApi/DummyDiscourseClient.cs @@ -2,7 +2,9 @@ namespace SS14.Labeller.DiscourseApi; -public class DummyDiscourseClient : IDiscourseClient +#pragma warning disable CS9113 // Parameter is unread. +public class DummyDiscourseClient(HttpClient _) : IDiscourseClient +#pragma warning restore CS9113 // Parameter is unread. { public Task CreateTopic(int category, string body, string title, CancellationToken ct) => Task.FromResult(new DiscourseCreatedPost() diff --git a/SS14.Labeller/GitHubApi/GithubRetryHandler.cs b/SS14.Labeller/GitHubApi/GithubRetryHandler.cs index 0f9b248..d85db53 100644 --- a/SS14.Labeller/GitHubApi/GithubRetryHandler.cs +++ b/SS14.Labeller/GitHubApi/GithubRetryHandler.cs @@ -15,7 +15,7 @@ namespace SS14.Labeller.GitHubApi; ///
Rate limit information /// /// This was designed for the 2022-11-28 version of the API. -public sealed class GithubRetryHandler(HttpMessageHandler innerHandler, IOptionsMonitor githubConfig, ILogger logger) : DelegatingHandler(innerHandler) +public sealed class GithubRetryHandler(IOptionsMonitor githubConfig, ILogger logger) : DelegatingHandler { private const int MaxWaitSeconds = 32; diff --git a/SS14.Labeller/Registry.cs b/SS14.Labeller/Registry.cs index 5ad3348..ff04414 100644 --- a/SS14.Labeller/Registry.cs +++ b/SS14.Labeller/Registry.cs @@ -44,6 +44,7 @@ public static void RegisterDependencies(this IServiceCollection service, IConfig client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", githubConfig.Token); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); }).AddHttpMessageHandler(); + service.AddTransient(); var discourseStartupConfig = new DiscourseConfig(); configuration.Bind(DiscourseConfig.Name, discourseStartupConfig); From 33e8fb4518e106fe569c6be5a9b6146eeb362ca9 Mon Sep 17 00:00:00 2001 From: "pa.pecherskij" Date: Mon, 20 Oct 2025 16:10:37 +0300 Subject: [PATCH 26/26] refactor: use aspnet runtime image instead of copy self-contained runtime during build --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e3519b6..395cc26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,9 +18,9 @@ RUN echo "Building for platform: $TARGETPLATFORM" \ "linux/arm64") export RID=linux-arm64 ;; \ *) echo "Unsupported TARGETPLATFORM: $TARGETPLATFORM" && exit 1 ;; \ esac \ - && dotnet publish -c Release -r $RID --self-contained true -o /app + && dotnet publish -c Release -r $RID -o /app -FROM debian:bookworm-slim AS final +FROM mcr.microsoft.com/dotnet/aspnet:9.0.10-bookworm-slim AS final WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \