Recently I wanted to build a GitHub webhook using C# Azure Functions, but could only find an example template using Node.js. Here, I set out to create a webhook to check the overall status of a build. This is something I was doing around 10 years ago with Team Foundation Server, so thought it would be fun to get this up and running with GitHub Actions.

Check Suites

Looking at the available webhook events there wasn’t an obvious event I could subscribe to get what I wanted. Currently, there isn’t an OTB event for when workflows complete. This has been asked a couple of times on the GitHub community - so I hope will be addressed soon. In addition, there are some marketplace solutions for Webhook Actions: Webhook Action and Workflow Webhook Action which you can use anywhere within your workflows to trigger a webhook.

Meanwhile, I settled on the check suite event subscription since it returns the summary conclusion for all the check runs that are part of a check suite. By default, GitHub creates a check suite automatically when code is pushed to the repository.

C# Azure Function webhook

To create the endpoint that GitHub calls, I wanted to use a lightweight serverless approach. Using Visual Studio you can use the C# Azure Function HTTP Trigger template to get up and running quickly. The below code extract shows how the received json payload can be deserialized and interrogated.

public static class GitHubWebhook
{
    [FunctionName("GitHubWebhook")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, 
        ILogger log)
    {
        log.LogInformation("C# Webhook for GitHub.");
        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        CheckSuiteStatus suiteStatus = JsonConvert.DeserializeObject<CheckSuiteStatus>(requestBody);
        string responseMessage = $"The GitHub Check Suite status for {suiteStatus.check_suite.id} {suiteStatus.action} with {suiteStatus.check_suite.conclusion}";
        return new OkObjectResult(responseMessage);            
    }
}

An extract of the application/json payload GitHub sends for the check suites event can be seen below:

{
  "action": "completed",
  "check_suite": {
    "id": 941142538,
    "head_branch": "master",
    "status": "completed",
    "conclusion": "success",
    "app": {
      "id": 15368,
      "slug": "github-actions",
      "name": "GitHub Actions",
      "description": "Automate your workflow from idea to production",
      "external_url": "https://help.github.com/en/actions",
      "html_url": "https://github.com/apps/github-actions",
    }
  }
}

For an example of the full payload go here: webhook payload example.

Debugging with Tunnel Relay

Whilst debugging, GitHub needs a public addressable endpoint to invoke. Obviously this is a problem if you’re hosting on localhost, so you can use a tool like ngrok. However, I’ve chosen to use the relatively new (and free) Tunnel Relay tool which creates a secure tunnel using Azure Relay with a static address 😀

In the screenshot above you can see the Azure Function 200 result that has been sent back to GitHub. We don’t expect GitHub to actually do anything with this response - but we can use it to check our Azure Function is working ok.

Workflow Manual Triggers

To make the development cycle quicker, I used a manual trigger using the new workflow_dispatch. Adding the following lines to an existing workflow, means that you can manually invoke it:

so my workflow yaml now looks like this:

name: .NET Core

on:
  workflow_dispatch:
    inputs:
      logLevel:
        description: 'Log level'     
        required: true
        default: 'warning'

So you can now trigger it from the Actions tab without having to perform any commits:

Securing the Webhook

We want to ensure our trigger can only get called by GitHub and not some other spurious source. So we set a secret in the Webhooks Manage page:

GitHub will use this secret to create a hash signature of the entire payload and pass the signature within the X-Hub-Signature request header. Within our C# Azure Function we also need to store the same environment setting as Secret so we can compute a hash on the payload and check that it matches the one we’ve been sent from GitHub. If running locally, you can set this in the the local.settings.json file eg:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "Secret": ""
  }
}

For more details on how GitHub recommends you secure webhooks see Securing your webhooks. To implement this, I’ve used a simple Validate function:

public static bool Validate (string signature, string body, string secret)
{
    string expectedSignature = "sha1=" + HMACSHA256(secret, body);
    return expectedSignature.Equals(signature);
}

Publish to Azure Functions

Here I’ve used the [HttpTrigger(AuthorizationLevel.Function)], so we need to grab the Function access key from the Azure portal:

This now forms part of the ?code= querystring of the webhook Payload URL in the GitHub Webhook Manage page - which we now need to update from our development Tunnel Relay address. The URL should look something like this: https://{yourfunc}.azurewebsites.net/api/GitHubWebhook?code=abcdefg123456==

Testing

Finally, to ensure everything is hooked up and all the configuration is correct - let’s manually trigger a workflow using workflow_dispatch and monitor the Azure Function at the same time.

Full code here: GitHub Webhook Playground. This particular webhook doesn’t actually do anything useful, other than interrogate the check suite conclusion and return a message back in the response payload to GitHub. I plan to use this same mechanism to integrate GitHub into SmartThings, which I’ll cover in a later post.