Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:

Expand Down
7 changes: 7 additions & 0 deletions Services/GitHubService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,11 @@ public async Task<HashSet<string>> 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<PullRequest> GetPullRequestAsync(string owner, string repo, int prNumber)
{
await EnsureAuthenticatedAsync();
return await _client.PullRequest.Get(owner, repo, prNumber);
}
}
50 changes: 45 additions & 5 deletions Services/GitHubWebhookProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 ||
Expand All @@ -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.
Expand All @@ -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");
}

}
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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())
Expand Down
Loading