Validating new github issues with Azure Functions

Using Azure Functions and github webhooks to check if the issue template was used to relieve some pain of frustrated repository owners.

Posted by Sven-Michael Stübe on January 12, 2017

In 2016 github added a feature, that allows users to create templates for new issues and pull requests. This is nice, because if the maintainers can help users to provide all necessary data without asking for them several times. Unfortunately, some people just delete all of the template and start typing. In this blog post I’ll show my solution for this problem.

Functionality

When a user opens a new issue, github sends the issue information to a Azure Function web hook. The function analyses the issue data and compares the issue text with the issue template. After computing the matching quote, it adds an comment using the github API.

Setup

Azure Function

Creation

Creating a new Azure Function is very easy.

  • open https://portal.azure.com/
  • add a Function app and open it
  • click New Function
  • select scenario Webhook + API
  • select language C#
  • click Create this function

If you need a more detailed tutorial, click here.

Configuration

The github API for .NET is implemented in Octokit and it’s available on NuGet. To add the NuGet package to your Function:

  • click View files
  • click Add
  • name it project.json
  • paste the following code and save it
{
    "frameworks": {
        "net46": {
            "dependencies": {
                "Octokit": "0.23.0"
            }
        }
    }
}

Github

Webhook

Now, you have to configure github to call your webhook when a new issue was opened.

  • Open the settings of the repository that you want to monitor.
  • click Webhooks
  • click Add Webhook
  • enter the Azure Function url into Payload URL
  • enter the GitHub Secret into Secret
  • select Let me select individual events
  • select Issues

To verify your setup, you can open an issue. If you look at the log output of your function, you’ll see something like:

2017-01-11T22:22:42.222 C# HTTP trigger function processed a request.

Pro tip: Before opening the issue, add this line to your function:

log.Info($"Data: {data}");

The request body will be written to the log. You can then copy it and paste it into the Test section and every time you press Run the “real” request will be processed. So you don’t need to open a new issue on github to test you function.

OAuth API token

The github API requires an access token. Create a new one following these steps:

  • open https://github.com/
  • log in and open your settings
  • click Personal access tokens
  • click Generate new token
  • enter a name and select public_repo
  • click Generate token
  • save the generated token

Issue template

If your repository doesn’t contain an issue template ISSUE_TEMPLATE.md, add one. Click here for more information on issue templates. Additionally add the file ISSUE_TEMPLATE_CHECK.md. This file contains the lines that should be included in the issue in the given order. I’ve chosen the headings and some bold bullet points.

## Steps to reproduce
## Expected behavior
## Actual behavior
### Crashlog
## Configuration
**Version of the Plugin:**
**Platform:**
**Device:**

Let's code!

Now, everything is setup correctly and you can code (or copy paste) your Azure Function. The code is available on github.

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    dynamic data = await req.Content.ReadAsAsync<object>();
    log.Info($"Data. {data}");
    await ProcessIssueAsync(data);  
   
    return req.CreateResponse(HttpStatusCode.OK);
}

private static async Task ProcessIssueAsync(dynamic data)
{
    if(data?.action != "opened")
        return;

    var creator = (string)data.issue.user.login;
    var owner = (string)data.repository.owner.login;
    var repository = (string)data.repository.name;    
    var repositoryId = (long)data.repository.id;
    var branch = (string)data.repository.default_branch; 

    var issueLines = GetLines((string)data.issue.body);
    var templateLines = (await GetTemplateElements(owner, repository, branch)).ToArray();

    var matchingQuote = CheckIssueWithTemplate(issueLines, templateLines);
    var message = GetMessage(creator, matchingQuote);
    await CreateCommentAsync(repositoryId, (int)data.issue.number, message);
}

First, call ProcessIssueAsync from your Run function. ProcessIssueAsync implements the workflow of the issue. You need to check if the action is "opened", because github sends all issue related events to your webhook. The function then:

  • reads some properties from the request
  • splits the issue text into lines
  • gets all lines of ISSUE_TEMPLATE_CHECK.md
  • checks the issueLines using the templateLines and calculates a matching quote in percent
  • generates a message based on the matching quote
  • creates a new comment on the issue

The format of the issue request is documented here.

CheckIssueWithTemplate

CheckIssueWithTemplate counts the lines of templateLines that occur in issueLines and calculates a quote that expresses how well the check condition was satisfied. You can implement any logic you want. It just has to project two string arrays to a number between 0 (=issue template wasn’t used at all) and 1 (=100% sure that the issue template was used).

private static double CheckIssueWithTemplate(string[] issueLines, string[] templateLines)
{
    if (templateLines.Length == 0)
        return 1;

    var templateList = templateLines.ToList();
    foreach (var issueLine in issueLines)
    {
        var found = templateList.FirstOrDefault(tpl => issueLine.StartsWith(tpl));

        if (!string.IsNullOrEmpty(found))
        {
            templateList.Remove(found);
        }
    }
    var matches = templateLines.Length - templateList.Count;
    return matches / (double)templateLines.Length;
}

GetMessage

The GetMessage generates a praisingly message if the user used the template and a sad one if he probably didn’t. You can add your on text and thresholds if you want.

static string GetMessage(string userName, double matchingQuote)
{
    string message;
    if (matchingQuote > 0.9)
    {
        message = "Thanks for using the issue template :kissing_heart:\n" +
                    "I appreciate it very much. I'm sure, the maintainers of this repository will answer, soon.";
    }
    else
    {
        message = $"It seems like ({(1 - matchingQuote):P}) you haven't used our issue template :cry: " +
                    $"I think it is very frustrating for the repository owners, if you ignore them.\n\n" +
                    $"If you think it's fine to make an exception, just ignore this message.\n" +
                    $"**But if you think it was a mistake to delete the template, please close the issue and create a new one.**\n\n" +
                    $"Thanks!";
    }                       

    return $"Hi @{userName},\n\n" +
            $"I'm the friendly issue checker.\n" +
            message;
}

CreateCommentAsync Last you have just to send the comment to github. The API makes this very easy. The developers of github (and ofc. the contributers) have done a great job! You have to replace the token with your own one.

private static async Task CreateCommentAsync(long repositoryId, int issueNumber, string message)
{
    var client = new GitHubClient(new ProductHeaderValue("github-issue-checker"));
    var tokenAuth = new Credentials("<github OAuth token>");
    client.Credentials = tokenAuth;
    var comment = await client.Issue.Comment.Create(repositoryId, issueNumber, message);
}

Result

will result in:

A nice looking issue will be commented like:

Possible additions

I think you are now aware of the possibilities that the connection of github webhooks, Azure Functions and the github API offers you. The Function can be easily enhanced with features like

  • close issues automatically if match qoute is smaller than 10%
  • save issue opener to database and block them after 3 bad issues
  • make the matching algorithm CheckIssueWithTemplate more intelligent
  • make the messages configurable like ISSUE_TEMPLATE_CHECK.md

Tweet me your best idea and win a like :)

Found a typo? Send me a pull request!