Writing GitHub bots in .NET

For a while now the Octokit libraries for .NET have lagged behind the JavaScript libraries, especially when it comes to webhooks. Unfortunately, I needed a GitHub webhook client for an internal project, so I had to write my own. It wasn’t too much extra effort to open source it, and thus Octokit.Webhooks was born!

I wanted to give a quick example of how to get up and running with Octokit.Webhooks, and what better way than to write a small GitHub bot?

Setup

For this project, I’m going to be using .NET 6.0 and ASP.NET’s new minimal APIs to simplify setup. From a terminal I’m going to create a new web API project:

1dotnet new webapi --output octokit-webhooks-sample

The default template is set up to be a weather forecast API, but I can simplify it a bit more for this sample. My Program.cs looks like this:

1var builder = WebApplication.CreateBuilder(args);
2
3var app = builder.Build();
4
5app.Run();

Next up I’m going to install the Octokit.Webhooks.AspNetCore package:

1dotnet add package Octokit.Webhooks.AspNetCore

This package consumes the Octokit.Webhooks package, which contains core functionality like deserializers and processors, and adds ASP.NET Core specific code, like automatic API endpoint mapping and shared secret verification.

Handling webhooks

Now that I’ve got my project set up, I need to create my own processor to handle incoming webhooks. Octokit.Webhooks ships with an abstract class called WebhookEventProcessor that does all the heavy lifting of deserializing incoming webhooks. All I need to do is to create my own class that inherits from it, and write some logic to act on the webhook events.

 1using Octokit.Webhooks;
 2using Octokit.Webhooks.Events;
 3using Octokit.Webhooks.Events.IssueComment;
 4
 5public sealed class MyWebhookEventProcessor : WebhookEventProcessor
 6{
 7
 8  private readonly ILogger<MyWebhookEventProcessor> logger;
 9
10  public MyWebhookEventProcessor(ILogger<MyWebhookEventProcessor> logger)
11  {
12    this.logger = logger;
13  }
14
15  protected override Task ProcessIssueCommentWebhookAsync(WebhookHeaders headers, IssueCommentEvent issueCommentEvent, IssueCommentAction action)
16  {
17    this.logger.LogInformation(issueCommentEvent.Comment.Body);
18    return Task.CompletedTask;
19  }
20}

I created a small class MyWebhookEventProcessor that inherits from WebhookEventProcessor, and has an override for ProcessIssueCommentWebhookAsync that logs out the comment body. I also get the headers and the action passed to this method, so I could write a switch case and have different handling for created, edited, and deleted actions, but this is enough for now.

I also need to hook up MyWebhookEventProcessor in my startup class.

 1using Octokit.Webhooks;
 2using Octokit.Webhooks.AspNetCore;
 3
 4var builder = WebApplication.CreateBuilder(args);
 5
 6builder.Services.AddSingleton<WebhookEventProcessor, MyWebhookEventProcessor>();
 7
 8var app = builder.Build();
 9
10app.UseRouting();
11
12app.UseEndpoints(endpoints =>
13{
14  endpoints.MapGitHubWebhooks();
15});
16
17app.Run();

This is enough to tell ASP.NET to hook up dependency injection for MyWebhookEventProcessor, enable routing. It will also automatically add a route to handle incoming GitHub webhooks. By default it’s exposed at /api/github/webhooks, but you can use any route you’d like. MapGitHubWebhooks also accepts a shared secret which allows you to verify the content signature of GitHub webhooks.

That’s all the code required on my side. Now I just need to expose my service to the internet, and configure GitHub to start sending me webhooks.

GitHub webhook configuration

For GitHub to be able to send me webhooks, my service needs to be publicly accessible to the internet. I recently discovered a neat little service to do this with nothing more than ssh: localhost.run.

If I run my app with dotnet run then I can find the port that it’s running on:

1info: Microsoft.Hosting.Lifetime[14]
2      Now listening on: http://localhost:5002

And using localhost.run I can create a tunnel for that port:

1$ ssh -R 80:localhost:5002 [email protected]
2
3...
4
5b49b69845954b1.lhrtunnel.link tunneled with tls termination, https://b49b69845954b1.lhrtunnel.link

Now on GitHub if I visit the settings for a repository, and go to webhooks, I can create a new webhook configuration using that domain name.

Creating a new GitHub Webhook

Now all I need to do is create a comment on an issue in the same repository….

A test issue comment

And it’ll get logged in my terminal!

1info: MyWebhookEventProcessor[0]
2      Test comment

Making it interactive

Logging things to the terminal is great and all, but to make it a bot it should really do something. For that I’ll need the Octokit package:

1dotnet add package Octokit

And I’ll use it in MyWebhookEventProcessor :

 1private readonly GitHubClient client;
 2
 3public MyWebhookEventProcessor(ILogger<MyWebhookEventProcessor> logger)
 4{
 5  this.logger = logger;
 6  this.client = new GitHubClient(new ProductHeaderValue("octokit-webhooks-sample"))
 7  {
 8    Credentials = new Credentials("...")
 9  };
10}

For this example I’m using a personal access token. You can create your own here. If I were deploying this as a production service, I would probably use something a bit more robust, like a GitHub App.

1protected override async Task ProcessIssueCommentWebhookAsync(WebhookHeaders headers, IssueCommentEvent issueCommentEvent, IssueCommentAction action)
2{
3  this.logger.LogInformation(issueCommentEvent.Comment.Body);
4  await this.client.Issue.Comment.Create(
5    repositoryId: issueCommentEvent.Repository.Id,
6    number: (int)issueCommentEvent.Issue.Number,
7    newComment: "Hello, world!"
8  );
9}

I need to do that cast from long to int because Octokit still has an open PR to convert all its IDs to long. I’ve also got to add the async modifier to my method, so I can await the issue comment creation method in Octokit.

Once I’ve done all that, I can create an issue comment on my repository on GitHub and my “bot” will reply!

A reply from the bot

What next?

If you find the library useful or interesting, give it a star on GitHub. And create an issue or pull request if you find find a bug or have a feature request.

If you want to create a fully-fledged bot, check out the GitHub documentation on creating a GitHub app. It’s the next progression from PATs, and allows you to more easily share your bot.

comments powered by Disqus