Receiving and Processing Github API Events

Intro

Github the is the most widely used code repository around, one of the best features is that almost everything you can do to a code repository is exposed thru apis and events. In this article, we will investigate how to receive those events, parse them and act on them using the api. In another article (Part 2) we will investigate the Jenkins API and show how you can trigger a jenkins job after receiving an event from github and wait for a response to provide Github an actual status.

Github setup (Required stuff)

First let’s create a personal token that we can use to access the APIs

  1. Login to your github account and go to your settings page
  2. Click on the menu item on the left that says: “Personal Access Token” and then click on “Generate new token”
  3. Give your token and name and click the repo selection.
  4. Once you see the token, copy and paste it somewhere that you can get it back since github will never show it to you again.

Second let’s setup a github repository to use in our tests.

  1. Create a new repository called “github-events”. Once this is done, you should be at the main page of your repository
  2. Go to the repo’s settings page and then webhooks and services
  3. Click ‘Add webhook’
  4. Payload URL is where you want to receive the events that you will be selecting next. (we will be using ngrok (check it out here) to get these - this is a great way of getting around firewalls, VPNs and other bothersome things), Enter ‘https://xyz.ngrok.io/events'
    • start ngrok with the command: ‘ngrok http -subdomain=xyz 8081’
  5. Content-Type is fine as application/json
  6. Remove the Secret (Secret is a way to validate the events you receive from github)
  7. Click: “Send me everything”
  8. Make sure active is checked and ress ‘Add Hook’

Go Code (The fun part)

At this point all the pre-requisites are ready to go and github is actually sending events to your chosen url and you can see them coming in the ngrok web interface at http://localhost:4040/. Now we can start GO coding…

Here is the front information for our code package, imports and variables. Note that we declare the github api token and a client connection variables to be able to respond to any github events that come out way.

package main
 
import (
    "log"
    "net/http"
 
    "golang.org/x/oauth2"
    "github.com/google/go-github/github"
)
 
var (
    githubToken = "c149XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX31da6"
 
    client *github.Client
)

In the ground work for our code we need to establish a connection to github using our API key and the simple api code from “github.com/google/go-github/github”. Once this is done we can establish the http server to receive the events (http).

func main() {
    // Setup the token for github authentication
    ts := oauth2.StaticTokenSource(
     &oauth2.Token{AccessToken: githubToken},		//pfortin-urbn token
    )
    tc := oauth2.NewClient(oauth2.NoContext, ts)
    
    // Get a client instance from github
    client = github.NewClient(tc)
    // Github is now ready to receive information from us!!!
    
    //Setup the serve route to receive guthub events
    http.HandleFunc("/event", EventHandler)
 
    // Start the server
    log.Println("Listening...")
    log.Fatal(http.ListenAndServe(":8081", nil))
}

Finally here is the skeleton of the Github Event handler that will parse the events and respond accordingly. At the moment all it does is print the request body so we can see that we are receiving events from github.

//This is the event handler for github events.
func EventHandler(w http.ResponseWriter, r *http.Request) {
    log.Printf("Message: %+v\n", r.Body)
}

Now let’s build up that event handler code. First we need to determine what type of event we have received in case we have registered for more than 1 type. Event types from github come in a request header called ‘X-GitHub-Event’, there is a full list of the types on the Github API site but for now we only will care about receiving and acting upon the ‘pull_request’ events. So the code below extract the header and then checks for the ‘pull_request’ event type and then pulls out the json message that contains all the information from the request body. Then we log that information to the logs and exit. Notice that all other events are ignored and simply logged as not supported.

//This is the event handler for github events.
func EventHandler(w http.ResponseWriter, r *http.Request) {
    event_type := r.Header.Get("X-GitHub-Event")

    if event_type == "pull_request" {
        pr_event := new(github.PullRequestEvent)
        json.NewDecoder(r.Body).Decode(pr_event)

        log.Printf("Event Type: %s, Created by: %s\n", event_type, pr_event.PullRequest.Base.User.Login)
        log.Printf("Message: %s\n", r.Body)

        log.Println("Handler exiting...")
    } else {
        log.Printf("Event %s not supported yet.\n", event_type)
    }
}

A Pull Request comes with many states (open, close, etc,..) but in our case we are not interested in all of them we simply want to handle the open state (when a PR is first opened or re-opened). So we can add to the code above by cheking the state.

//This is the event handler for github events.
func EventHandler(w http.ResponseWriter, r *http.Request) {
    event_type := r.Header.Get("X-GitHub-Event")

    if event_type == "pull_request" {
            pr_event := new(github.PullRequestEvent)
            if pr_event.PullRequest.State == "open" {
                json.NewDecoder(r.Body).Decode(pr_event)
    
                log.Printf("Event Type: %s, Created by: %s\n", event_type, pr_event.PullRequest.Base.User.Login)
                log.Printf("Message: %s\n", r.Body)
    
                log.Println("Handler exiting...")
            } else {
                log.Println("PR state not open or reopen")
            }
    } else {
        log.Printf("Event %s not supported yet.\n", event_type)
    }
}

Finally let’s use that github client we created above to tell github that we are working on that event and the PR should be put in a “freeze” state until we are done and they released after we are done. (In this article we will simply wait for a delay time and then release the PR, in the next article we will send a request to jenkins to start a job and wait for it to complete.

Add these variables to your variable declaration above.

    pending     = "pending"
    success     = "success"
    failure     = "error"
    targetUrl   = "https://xyz.ngrok.com/status"
     pendingDesc = "Build/testing in progress, please wait."
    successDesc = "Build/testing successful."
    failureDesc = "Build or Unit Test failed."
    appName     = "TEST-CI"

Now let’s put the final piece together by creating the statuses and sending them to github..

//This is the event handler for github events.
func EventHandler(w http.ResponseWriter, r *http.Request) {
    event_type := r.Header.Get("X-GitHub-Event")

    if event_type == "pull_request" {
            pr_event := new(github.PullRequestEvent)
            if *pr_event.PullRequest.State == "open" {
                json.NewDecoder(r.Body).Decode(pr_event)

                log.Printf("Event Type: %s, Created by: %s\n", event_type, pr_event.PullRequest.Base.User.Login)
                log.Printf("Message: %s\n", r.Body)

                // Create the 'pending' status and send it
                status1 := &github.RepoStatus{State: &pending, TargetURL: &targetUrl, Description: &pendingDesc, Context: &appName}
                client.Repositories.CreateStatus(*pr_event.PullRequest.Base.User.Login, *pr_event.PullRequest.Base.Repo.Name, *pr_event.PullRequest.Head.SHA, status1)

                // sleep for 30 seconds
                time.Sleep(30 * time.Second)


                // Create the 'success' status and send it
                log.Println("Returning Success")
                status2 := &github.RepoStatus{State: &success, TargetURL: &targetUrl, Description: &successDesc, Context: &appName}
                client.Repositories.CreateStatus(*pr_event.PullRequest.Base.User.Login, *pr_event.PullRequest.Base.Repo.Name, *pr_event.PullRequest.Head.SHA, status2)


                log.Println("Handler exiting...")
            } else {
                log.Println("PR state not open or reopen")
            }
    } else {
        log.Printf("Event %s not supported yet.\n", event_type)
    }
}

The full code to this article can be found on my Gist at: https://gist.github.com/p4tin/25fd1360d31c2f85871e4a2c820a9aeb

Conclusion

In this article we received events on a PR from github and sent a pending status to github on that PR and then just waited a few seconds and sent a success status. In a follow up article, we will take those PR open events and after sending the initial “pending” status to github, we will call jenkins, start a specific job, wait for it to complete and after getting the status back from jenkins we will send the appropriate status back to github to indicate success or failure.

You can see the full source code to my gitkins project at https://github.com/p4tin/gitkins/, and I encourage you to handle new events/statuses and submit PRs so the project can grow.

Happy GOing!!!

*** Sign up for my email list to keep in touch with all the interesting new happenings in the go community with the GolangNewsFeed

comments powered by Disqus