diff --git a/README.md b/README.md index f37e511..39c3948 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,30 @@ Then edit `appsettings.json` and fill in your actual values: | `GitHubApp.TeamName` | Team slug used for review workflow (for example: `captains`). Review labels are updated only when review is submitted by a member of this team. | | `Kestrel.Endpoints.Http.Url` | The address and port the server listens on (default: `http://0.0.0.0:3456`) | -### 2. Run +### 2. Configure GitHub App permissions and webhook events + +Your GitHub App must be configured with the following permissions: + +**Organization permissions** + +- Members: **Read-only** + +**Repository permissions** + +- Pull requests: **Read and write** +- Issues: **Read and write** +- Contents: **Read-only** + +Your GitHub App must also subscribe to these webhook events: + +- Issue comments +- Issues +- Pull request review comments +- Pull request review threads +- Pull request reviews +- Pull requests + +### 3. Run #### Option A: Run directly @@ -72,7 +95,7 @@ docker run -d \ ghcr.io/openruyi/abaci-bot:latest ``` -### 3. Build Docker Image Locally (Optional) +### 4. Build Docker Image Locally (Optional) If you want to build the image yourself: diff --git a/Services/GitHubService.cs b/Services/GitHubService.cs index 5cdfdb6..90bbcad 100644 --- a/Services/GitHubService.cs +++ b/Services/GitHubService.cs @@ -139,4 +139,11 @@ public async Task> GetTeamMembersAsync( var members = await _client.Organization.Team.GetAllMembers(teamSlug.Id); return members.Select(m => m.Login.ToLowerInvariant()).ToHashSet(); } + + // Add more GitHub API methods as needed. + public async Task GetPullRequestAsync(string owner, string repo, int prNumber) + { + await EnsureAuthenticatedAsync(); + return await _client.PullRequest.Get(owner, repo, prNumber); + } } diff --git a/Services/GitHubWebhookProcessor.cs b/Services/GitHubWebhookProcessor.cs index 4a5959a..370ffad 100644 --- a/Services/GitHubWebhookProcessor.cs +++ b/Services/GitHubWebhookProcessor.cs @@ -2,6 +2,7 @@ using Octokit.Webhooks.Events; using Octokit.Webhooks.Events.PullRequest; using Octokit.Webhooks.Events.PullRequestReview; +using Octokit.Webhooks.Events.IssueComment; namespace abaci_bot.Services; @@ -27,6 +28,8 @@ protected override async ValueTask ProcessPullRequestWebhookAsync( var repo = pullRequestEvent.Repository.Name; var headSha = pullRequestEvent.PullRequest.Head.Sha; var isDraft = pullRequestEvent.PullRequest.Draft; + var currentLabels = pullRequestEvent.PullRequest.Labels.Select(l => l.Name).ToList(); + string prTitle = pullRequestEvent.PullRequest.Title; // Handle PR when opened or synchronized (new commit) if (action == PullRequestAction.Opened || @@ -51,7 +54,7 @@ protected override async ValueTask ProcessPullRequestWebhookAsync( // Then, let's analyze the content of the PR and add labels accordingly. await AnalyzeFilesAndLabelPR(owner, repo, prNumber, headSha); - if (isDraft) + if (isDraft || prTitle.StartsWith("WIP", StringComparison.OrdinalIgnoreCase)) { // For draft PR, add "Workflow: In Dev" label to indicate it's still in development, // and remove "Workflow: Ready For Review" label if exists. @@ -62,7 +65,10 @@ protected override async ValueTask ProcessPullRequestWebhookAsync( { // For non-draft PR, add "Workflow: Ready For Review" label and remove "Workflow: In Dev" label if exists. await _github.RemoveLabelAsync(owner, repo, prNumber, "Workflow: In Dev"); - await _github.AddLabelsAsync(owner, repo, prNumber, "Workflow: Ready For Review"); + if (!currentLabels.Contains("Workflow: In Review")) + { + await _github.AddLabelsAsync(owner, repo, prNumber, "Workflow: Ready For Review"); + } } } @@ -103,6 +109,38 @@ protected override async ValueTask ProcessPullRequestReviewWebhookAsync( } } + // When a new comment is created on the PR, if the commenter is a captain, we can also add "Workflow: In Review" label. + protected override async ValueTask ProcessIssueCommentWebhookAsync( + WebhookHeaders headers, + IssueCommentEvent issueCommentEvent, + IssueCommentAction action, + CancellationToken cancellationToken = default) + { + if (action == IssueCommentAction.Created) + { + if (issueCommentEvent.Issue.PullRequest != null) + { + var prNumber = (int)issueCommentEvent.Issue.Number; + var owner = issueCommentEvent.Repository.Owner.Login; + var repo = issueCommentEvent.Repository.Name; + var captains = await _github.GetTeamMembersAsync(owner, _config["GitHubApp:TeamName"]!); + string sender = issueCommentEvent.Sender.Login.ToLowerInvariant(); + var pullRequest = await _github.GetPullRequestAsync(owner, repo, prNumber); + var prTitle = issueCommentEvent.Issue.Title; + + // Only when the commenter is a captain, the PR is not in draft, + // and the title doesn't start with "WIP", we consider it as "In Review" and add the label. + if (captains.Contains(sender) && + !pullRequest.Draft && + !prTitle.StartsWith("WIP", StringComparison.OrdinalIgnoreCase)) + { + await _github.RemoveLabelAsync(owner, repo, prNumber, "Workflow: Ready For Review"); + await _github.AddLabelsAsync(owner, repo, prNumber, "Workflow: In Review"); + } + } + } + } + private async Task AnalyzeUserAndLabelPR(string owner, string repo, int prNumber, string? userEmail) { var domain = ExtractEmailDomain(userEmail); @@ -173,9 +211,11 @@ private async Task AnalyzeFilesAndLabelPR(string owner, string repo, int prNumbe } } - // TODO: What if, LTS. - labelsToAdd.Add("Target: Rolling"); - + if (file.FileName.StartsWith("SPECS/")) + { + // TODO: What if, LTS. + labelsToAdd.Add("Target: Rolling"); + } } if (labelsToAdd.Any())