Monitoring Github events with Webhooks and Google Cloud Functions
Tags: cloud, gcp, node, tutorial
Published: 2020-03-31 15:32:50

In this post we are going create a Google Cloud Function and a Github Webhook to automate the deletion of inappropriate comments on Github issues. We'll create a webhook that will call an HTTP function every time an issue comment is created, edited, or deleted. In order to follow along with this tutorial you will need a Google Cloud Platform account and a Github account. 

###Table of Contents
  * [What is a Google Cloud Function?](#function)
  * [Developing our Function in the Console](#console)
  * [Developing our Function Locally](#local)
  * [Creating our Webhook](#hook)
  * [Generating a Personal Access Token](#token)
  * [Testing our Function](#testing)
  * [Deploying our Function](#deploy)
  * [Wrapping up](#finish)

###What is a Google Cloud Function?<a name="function"></a>

Google Cloud functions are single-purpose, serverless functions that can run on demand in your cloud environment in response to events. Events include HTTP events, Cloud Pub/Sub events, and Cloud Storage events. At the time of this writing there are also a few more events currently in beta. You then create a trigger in response to the event that is emitted, and bind that trigger to a function.  

Cloud Functions are useful for situations where you don't want to spin up a full server to execute some logic. All of the infrastructure and software are managed by Google so that all you have to do is write the code. This is often referred to as Functions as a Service, or FaaS, and is not unique to Google. AWS Lambda and Azure Functions are just two of the many competitors in this space. 

###Developing our Function in the Console<a name="console"></a>

There are two ways we could go about developing our function, in the console or locally in our development environment. First, I'll demonstrate how you would go about setting it up in the console, and afterward we will actually develop in our local environment. 

Open up your cloud console and select a project or create a new one. Next, select Cloud Functions in the compute section of the navigation menu. Enable the API if it is not already enabled. You will also need to make sure you setup a billing account for the project to use Cloud Functions. Once you click on create function you'll see the following.

![Options for creating cloud function in GCP console](https://dev-to-uploads.s3.amazonaws.com/i/r2n3tzby2hhkzkf938rx.png)

I gave this function the name test, left the memory allocation on the default of 256 MiB, and we are using the HTTP trigger type. We are also allowing unauthenticated invocations.

Next we have the advanced options. Choose the region closest to you to reduce latency. You get charged only while your function is running to the nearest 100 milliseconds. You can also set the timeout and the maximum function instances that you want running. This is useful because you can put a limit on how much your function can scale out, otherwise your function can scale out to as many instances as necessary. You will also need to select a service account that the function will assume as its identity.

![Advanced options for creating cloud function in GCP console](https://dev-to-uploads.s3.amazonaws.com/i/96l4ywbryvf5sxpqilmu.png)

It's nice to have all the options right in front of you, but writing code in the browser like that is definitely not ideal. 

###Developing our Function Locally<a name="local"></a>

To develop locally we are going to use the Functions Framework. This will allow us to spin up a server for our function and invoke that function in response to a request. To get started, create a new folder that will have your function and run `npm init`. Next, run `npm install @google-cloud/functions-framework node-fetch` and add the following script to your `package.json`:


```javascript
  "scripts": {
    "start": "functions-framework --target=deleteGithubComment"
  }
```
We'll need node-fetch to make a DELETE request to our github comment. Create an index.js file and add the following contents to it:

```javascript
const fetch = require('node-fetch')

const inappropriateWords = ['enter','words','to','check','for']

exports.deleteGithubComment = async (req, res) => {
  const { repository_url, title } = req.body.issue
  const {
    id,
    body,
    user: { login },
  } = req.body.comment

  const bodyArray = body.split(' ')

  const url = `${repository_url}/issues/comments/${id}`
  const headers = {
    Authorization: `Token ${process.env.TOKEN}`,
  }

  let removeComment = false
  bodyArray.forEach((word) => {
    if (inappropriateWords.includes(word)) {
      removeComment = true
    }
  })

  if (removeComment) {
    try {
      await fetch(url, {
        method: 'DELETE',
        headers,
      })
      return res.status(200).json({
        user: login,
        message: `Removed inappropriate comment on issue "${title}."`,
        comment: body,
        deleted_message: body,
      })
    } catch (err) {
      return res.status(400).json({
        user: null,
        message: 'Error removing inappropriate comment.',
        comment: body,
        deleted_message: null,
      })
    }
  }

  return res.status(200).json({
    user: login,
    message: `No need to remove comment. Maybe you can log this information.`,
    comment: body,
    deleted_message: null,
  })
}

```
Our function is written in Node.js. Node cloud functions use express under the hood, so you will have the familiar request and response arguments. When this function is called, it checks the contents of the comment, and checks each word against an array of inappropriate words. To keep the tutorial family friendly, I removed the words I used and added a placeholder. If you wanted to get more advanced, you could use Google's AI services to better understand the meaning of the comment and maybe catch things that a simple word check would miss. Since this is just meant to get you started I will not be doing that.

If it finds a word in your array of inappropriate words, it will send a DELETE request to github to remove the comment using node-fetch. This requires getting a token from Github which we will cover in the next section. Before moving onto the next section, run `npm start` so the functions framework can start up a server for our function. This should start up on `http://localhost:8080`.

###Creating our Webhook<a name="hook"></a>

Creating our webhook requires a publicly accessible URL, but because we haven't deployed our function yet, we don't have one. In order to get around this, we are going to install an npm package called ngrok which will create a tunnel to expose our localhost to the internet. Run `npm install -g ngrok` and once that is done, run `ngrok http 8080`. 

![ngrok output](https://dev-to-uploads.s3.amazonaws.com/i/53t5k7wxybev3lqt6100.png)

Login to your Github account and select a repository that you want to use this function. Go to settings and select Webhooks.

![Github webhooks settings](https://dev-to-uploads.s3.amazonaws.com/i/zogjuz8fk30oavcv0f1j.png)

Click on create webhook and fill out the form like I have done. Notice how I am using the the URL provided by ngrok which will tunnel to our localhost.

![Create webhook options](https://dev-to-uploads.s3.amazonaws.com/i/w5la5dsdbic24op76tme.png)

Click on Add Webhook and you're all set.

###Generating a Personal Access Token<a name="token"></a>

Go to your user settings and then click on Developer Settings at the bottom. 

![Developer settings](https://dev-to-uploads.s3.amazonaws.com/i/gu2pnzrf3sv6k8lcwoqm.png)

Select Personal access tokens and click on generate token. Enter some kind of descriptive note and select the `repo` checkbox.

![Options for generating token](https://dev-to-uploads.s3.amazonaws.com/i/nmm0kzjbs0vtnnzze4ij.png)

Click on generate token and you'll be given your token. Make sure to copy it because you'll never be able to see it again after you leave the page.

###Testing our Function<a name="testing"></a>

Head back to your code and create a `.env.yaml` file and add the token like so:

```yaml
TOKEN: add-your-token-here
```
In order to get around installing the `dotenv` package for testing and uninstalling it for deployment, just replace 

```javascript
const headers = {
  Authorization: `Token ${process.env.TOKEN}`,
}
```
with the following:

```javascript
const headers = {
  Authorization: `Token your-token`,
}
```
We will change this back before we deploy our function.

Now that you have everything set up you can test your function. Create a new issue in your Github repo and add a comment that shouldn't be removed by your function. Afterward, add a comment that is included in your inappropriate words array. You should see it get added, and once you refresh the page it should be deleted. You can use this setup to test other webhooks or functions that you have created. 

###Deploying our Function<a name="deploy"></a>

Now that we have tested our function locally, we are going to deploy our function to Google Cloud. First, DO NOT forget to remove your token from your code and replace it with `process.env.TOKEN`. Next, from the root of your function directory run the following command:

```
gcloud functions deploy deleteGithubComment \
  --region us-east1 \
  --runtime nodejs10 \
  --trigger-http \
  --entry-point=deleteGithubComment \
  --env-vars-file .env.yaml \
  --allow-unauthenticated
```

This will deploy your function to the region us-east1 (you should change the region to the one closest to you) with the name `deleteGithubComment`, that uses the nodejs 10 runtime, and declares it as an HTTP function. The entry point is the function in your index.js file that you want to deploy and we are letting Google Cloud know that we have environment variables that are scoped to this function. We are also allowing unauthenticated function invocations but because this function requires information from Github, nothing will happen if you just go to the URL provided by the function.

Deploying will take up to two minutes but afterwards in the output you should see a URL for your function. The only thing left to do is to go back to Github and replace the ngrok URL in your webhook with the URL for your function. Now that you're deployed, test everything out once more to make sure it's still working. The first time you hit the endpoint, you'll experience what is called a cold start. If your function hasn't been run in awhile or is running for the first time, it will take a second or two for the server to spin up. After your function is called the server should stay active for some time before it's spun back down, meaning much faster response times.

###Wrapping up<a name="finish"></a>

Cloud functions and Github webhooks can both be really powerful and you are only limited by your imagination on what you can do with them. I'd encourage you to take what you learned from this article and apply it to something else. Maybe you already have an application that could use some one-off logic wrapped up in a cloud function. Or maybe you want to automate some other aspect of Github, like emailing any user who creates a pull request with information about your project. 

If anything was unclear or something isn't working right, leave me a comment down below or message me and I'll do my best to help you out. If you would like to get in touch for any reason, feel free to connect with me on LinkedIn, follow me on Twitter, or send me an email. Thanks for reading!