DEV Community

John Papa for Microsoft Azure

Posted on

Automate Your Replies to GitHub Issues with Serverless

Have you ever contributed to an OSS project on GitHub, perhaps creating an issue, and instantly received a reply? Wouldn't it be nice if that reply to your issue thanked you? This article walks through one way you can automate creating a holiday themed "thank you" replies to all issues created in one or more of your GitHub repositories.

Here is a link to the source code in case you want a reference.

diagram

This article is part of #25DaysOfServerless. New challenges will be published every day from Microsoft Cloud Advocates throughout the month of December. Find out more about how Microsoft Azure enables your Serverless functions. Have an idea or a solution?

One possible solution to make this a "Happy Holiday" story is to use Azure Functions to listen to GitHub via webhooks and then respond by creating a comment on the issue. Let's explore what you'll need first and then walk through the steps you can use to try this for yourself.

The Approach

Okay, let's step back a moment and examine what you're about to do. When an issue is created in GitHub, you want to thank the issue's creator for reporting the issue.

You need to know when that issue is created, so you'll ask GitHub to alert us when this happens. GitHub alerts us via a webhook. The webhook is triggered by the issue being created and then makes an HTTP request to a URL, which you can define, passing along the payload of the issue that was created. So where does that GitHub webhook HTTP request go? It will call a function that you create with Azure Functions.

You'll create a function that accepts the webhooks request and inspects its payload. You'll parse out the creator and other details in the payload and format a response to the issue. Now that you have the data you need to create the comment on the issue, you need a way to talk back to the same issue and create the comment. Then you'll call the GitHub API to create a comment on the issue, using a token that allows the function to make the call.

You're communicating between a GitHub repository and Azure Functions, so you'll need to make sure you have the right permissions to make these HTTP requests between them. This is pretty straightforward, as you'll see through the solution below.

Here is a summary of what you're going to do:

  • Create a webhook on one of our GitHub repositories that fires when an issue is created
  • Create an Azure Function that accepts HTTP request that the webhook makes
  • Generate a personal access token that the function can use when talking to the GitHub API
  • Make an HTTP request from the function to the GitHub API to create the comment

Now that you've laid out what you going to do, let's go put it all together.

Resources and Tools πŸš€

Starting with the right tools can make all the difference in the world. Let's start by using Visual Studio Code and the Azure Functions extension. This combination is, in my opinion, the best way to get started and efficiently create Azure Functions.

If you do not have an Azure account, you'll want to sign up for a free trial of Azure from this link here.

Install the Right Tools

Create the function

You'll start by creating the Azure Function app in Visual Studio Code.

Create the Azure Function project

  1. Create a new folder and name it thank-you
  2. Open VS Code and open the thank-you folder
  3. Press the F1 key to open the Command Palette.
  4. Enter and select Azure Functions: Create New Project
  5. If prompted to create the project in the thank-you folder, press Yes
  6. Select TypeScript for the new project's language
  7. Select HTTP Trigger as the template for the function
  8. Name the function SayThankYou
  9. Choose Anonymous as the authorization level

VS Code will now create the function project thank-you and your SayThankYou f

Test the Function

Let's install the dependencies for the project

  1. Open the Command Palette by pressing F1
  2. Enter and select Terminal: Create new integrated terminal
  3. Enter npm install in the terminal, to install the dependencies for the project
  4. Press F5 to run the function
  5. Go to a browser and enter http://localhost:7071/api/saythankyou?name=Colleen into the address bar

The function will respond to the browser by saying Hello Colleen

To stop the function app from running:

  1. Open the Command Palette by pressing F1
  2. Enter and select Debug: Disconnect

The Trigger is a webhook

You need to tell the function that it is triggered by a GitHub webhook.

  1. Open the function.json file
  2. modify the JSON to add ", webHookType": "github" after the "methods": ["get", "post"],

What's the Payload?

You need to know the shape of the payload that the function will receive from the webhook. You can find the payload shape for the IssuesEvent in the GitHub documentation here.

You'll reference this payload information as you read the payload in the function.

Before you proceed, let's replace the code in the function with the following starter code.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';

const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise<void> {
  const { body: payload } = req;

  let body = 'Nothing to see here';

  context.res = { status: 200, body };
};

The function gathers the payload from the request, and then always responds with the same body and status. You'll refactor this to read the payload from the webhook next.

Reading the Payload

You want to create a message that thanks the issue creator. Some information you may want to gather are the issue creator's username, the issue number, the repository name, the owner of the repository, and the action that occurred to trigger the webhook.

Using the IssuesEvent GitHub documentation here as a reference, you can write the following code to gather these values from the payload.

// Gather the data from the payload from the webhook
const repo = payload.repository.name;
const owner = payload.repository.owner.login;
const issue_number = payload.issue.number;
const user = payload.issue.user.login;
const action = payload.action;

Now your function will look like the following code.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';

const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise<void> {
  const { body: payload } = req;

  // Gather the data from the payload from the webhook
  const repo = payload.repository.name;
  const owner = payload.repository.owner.login;
  const issue_number = payload.issue.number;
  const user = payload.issue.user.login;
  const action = payload.action;

  let body = 'Nothing to see here';

  context.res = { status: 200, body };
};

Crafting the Response

Now that you can read the payload, you want to craft the message that you'll use to create the comment on the issue. You only want to write the comment if the event that triggered the webhook was creating an issue. You will know it came from an issue event once you create the webhook. You don't want to respond to editing or deleting of the issue, so well look at the action to see if the issue event was caused by it being opened.

The following code will create a message only if the issue was opened (created).

let body = 'Nothing to see here';
if (action === 'opened') {
  body = `Thank you @${user} for creating this issue!\n\nHave a Happy Holiday season!`;
  context.log(body);
}

Your function should now look like the following code.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';

const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise<void> {
  const { body: payload } = req;

  const repo = payload.repository.name;
  const owner = payload.repository.owner.login;
  const issue_number = payload.issue.number;
  const user = payload.issue.user.login;
  const action = payload.action;

  let body = 'Nothing to see here';
  if (action === 'opened') {
    body = `Thank you @${user} for creating this issue!\n\nHave a Happy Holiday season!`;
    context.log(body);
  }

  context.res = { status: 200, body };
};

Generate a Personal Access Token

Before we start writing the code to create the comment, we'll need to generate a personal access token from GitHub so we can let our function talk to GitHub.

You only want to token to have access to public repositories, so be sure to only select the public_repo scope.

  1. Follow these steps to generate a personal access token. Only select public_repo when you are asked to select the scopes for the token.
  2. Copy the token to your clipboard

The token is a secret and should not be pasted into our code or stored in a repository. Azure Functions allows for secrets and environment variables to be set in the local.settings.json file. This file is in the .gitignore file by default, so it only lives on your local computer. Next you will add a setting to this file for your token.

The local.settings.json file is just for when you run the function locally on your computer. When it is time to push this function to Azure, you will see how to push these settings, as well.

  1. Open the local.settings.json file in your function project.
  2. Create a key in the Values section named githubKey
  3. Paste the token as the value

Your local.settings.json should look like the following code, except with your token.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "githubKey": "YOUR_TOKEN_GOES_HERE"
  }
}

Creating the Comment in the GitHub repository

Now that you have the token and the message that you want to write to a new comment in the repository, you need to tell GitHub to create a comment on the issue. There are a few ways to do this. Some common approaches are to either make a HTTP request directly to the GitHub API using an HTTP library like axios, or you can use a library that abstracts and simplifies the HTTP request. Let's choose the latter.

The library Octokit/rest.js is a GitHub REST API client for JavaScript. It exposes an API that will make it easy to create the comment. Let's install @octokit/rest next.

  1. Open a terminal in your project folder
  2. Run npm install @octokit/rest

You will want to import Octokit and IssuesCreateCommentParams, from the library.

import * as Octokit from '@octokit/rest';
import { IssuesCreateCommentParams } from '@octokit/rest';

Create the comment by crafting an object of type IssuesCreateCommentParams.

const comment: IssuesCreateCommentParams = {
  repo,
  owner,
  issue_number,
  body
};

Now it is time to use the library to create the comment. The important API to create the comment is the asynchronous function octokit.issues.createComment(). But before you can call it you need to pass a personal access token from your function to GitHub's API, via Octokit. You can retrieve the token from the local.settings.json file by referencing process.env.githubKey.

We can write the following function to grab the personal access token, pass it to Octokit, and create the comment.

async function createComment(comment: IssuesCreateCommentParams) {
  const auth = process.env.githubKey;
  const octokit = new Octokit({ auth });
  const response = await octokit.issues.createComment(comment);
  return response;
}

Now we can call that function right after we craft the comment.

if (payload.action === 'opened') {
  body = `Thank you @${user} for creating this issue!\n\nHave a Happy Holiday season!`;
  const comment: IssuesCreateCommentParams = {
    repo,
    owner,
    issue_number,
    body
  };
  await createComment(comment);
}

Your final function

Your function code should is now complete and should look like the following code.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';
import * as Octokit from '@octokit/rest';
import { IssuesCreateCommentParams } from '@octokit/rest';

const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise<void> {
  const { body: payload } = req;

  const repo = payload.repository.name;
  const owner = payload.repository.owner.login;
  const issue_number = payload.issue.number;
  const user = payload.issue.user.login;
  const action = payload.action;

  let body = 'Nothing to see here';

  if (action === 'opened') {
    body = `Thank you @${user} for creating this issue!\n\nHave a Happy Holiday season!`;
    const comment: IssuesCreateCommentParams = {
      repo,
      owner,
      issue_number,
      body
    };
    await createComment(comment);
  }

  context.res = { status: 200, body };
};

async function createComment(comment: IssuesCreateCommentParams) {
  const auth = process.env.githubKey;
  const octokit = new Octokit({ auth });
  const response = await octokit.issues.createComment(comment);
  return response;
}

export default httpTrigger;

Your function is now complete!

Push Your Function to Azure

Next you want to push your function to Azure. You'll need an Azure account first. If you do not have one, you can create an Azure free trial here.

  1. Open the Command Palette F1
  2. Enter and select Azure Functions: Create Function App in Azure
  3. Enter a globally unique identifier (a name)
  4. If prompted, select an OS
  5. Select Node.js 10.x
  6. Select the region to create your function app

You are now creating your function in Azure. VS Code will alert you when it is ready.

Once your app has been created in Azure, you must push your settings in your local.settings.json file to the app in Azure.

  1. Open the Command Palette F1
  2. Enter and select Azure Functions: Upload Local Settings
  3. Select your Function app, when prompted

You just created your function and pushed it to Azure. The next step is to create the webhook for one of your GitHub repositories.

Setting up a webhook

You want to set up a webhook for one of your repositories that will trigger each time an issue is created in that repository.

There are two steps in setting up this webhook. The first step is to tell the webhook what events should trigger it. The second step is to tell the webhook which URL it should post the HTTP request to. Let's walk through setting up your webhook.

Create the webhook

Let's create the webhook in one of your existing repositories.

  1. Using your web browser, sign in to your GitHub account.
  2. Navigate to one of your repositories.
  3. Select the Settings tab.
  4. Select webhooks from the menu on the left.
  5. Press the Add webhook button to create a new webhook for this repository
  6. Set the content type to applicaiton/json

Next you'll need the URL to your function app. The URL can be found in VS Code.

  1. Go to the Azure Function extension
  2. Expand your Function app and the Functions node
  3. Right click on your *SayThankYou& function
  4. Select Copy Function Url
  5. Go back to your browser where you are setting up the webhook
  6. Paste your Url into the Payload URL field
  7. Select the individual events checkbox
  8. Select the Issues checkbox
  9. Save the webhook

Now your webhook is ready to trigger the calls whenever something happens to an Issue. The webhook will post the payload to your function, your function will read the payload, craft a message, and then use the GitHub API to create a comment on the issue!

Try it

All that is left to do is to see if it works. You should go to your repository and create and issue. The webhook will trigger an HTTP request and pass a payload to your function. Your function will write a new comment back to your issue and you should see it momentarily.

Success!

Show me the Code

You can try this all from scratch by following these steps. You can also skip straight to the code solution here.

If you get stuck, please open an issue in the repo.

Next Steps πŸƒ

Learn more about serverless with Free Training!

Resources

I recommend these resources as they are great at helping explain all of the key elements.

Additional Resources ⭐️

Some additional awesome serverless resources, in general, are as follows.


Want to submit your solution to this challenge? Build a solution locally and then submit an issue. If your solution doesn't involve code you can record a short video and submit it as a link in the issue desccription. Make sure to tell us which challenge the solution is for. We're excited to see what you build! Do you have comments or questions? Add them to the comments area below.


Watch for surprises all during December as we celebrate 25 Days of Serverless. Stay tuned here on dev.to as we feature challenges and solutions! Sign up for a free account on Azure to get ready for the challenges!

Top comments (1)

Collapse
 
new_blicio_us profile image
New.blicio.us

Thanks, I could actually use this, and it gives me a reason to use my Azure account again.