GitHub Issuer

If you didn’t happen to read the last post I’ll give you a brief overview. I’m trying out a new system to tackle my project load and my TODO list.

The problem is that the current setup is very manual. Each “thing”, or project, I want to do gets its own repository with its own automated kanban board. I open an issue in the repo, assign it to the project board and “bam”, new task. But, I then need to add that issue to my master “TODO” repo by hand.

We can’t have that! It makes the system more work than it should be. That means we won’t use it.

But, we have a solution! GitHub repositories can have webhooks enabled. So, on every new issue, a POST request with the issue details goes to our webhook. That’s where Issuer is going to come in. It will live on a server waiting for incoming POST events. Once one comes it will use the GitHub API to create an issue in the TODO repo.


Lets Go

OK! Let’s jump right in and see what we are dealing with. If you read the second or third articles in the Uploader series then parts of the base code will look similar. We’re using a very standard approach to a Go web server.

package main

import (
  "encoding/json"
  "fmt"
  "log"
  "net/http"
)

The GitHub issue payload is “massive” compared to what we actually need from it. So our Payload struct is only going to include exactly what we need to create a new issue.

type Payload struct {
  Action string `json:"action"`
  Issue  struct {
    URL           string `json:"url"`
    RepositoryURL string `json:"repository_url"`
    Number        int    `json:"number"`
    Title         string `json:"title"`
    Body          string `json:"body"`
  } `json:"issue"`
  Repository struct {
    Name     string `json:"name"`
  } `json:"repository"`
}

Hello! This isn’t needed but I like to have it as a small health check. In a more robust application you may want to return some sort of metrics from the server. Then if I wanted I could point Uptime Robot at / to alert if its down.

func status(res http.ResponseWriter, req *http.Request) {
  fmt.Fprintf(res, "Hello!")
}

I’ll cover how to enable webhooks in a repo and how we can test a bit later on. For now, let’s take a closer look at our webhook handler function. We begin by setting up our Payload variable and defer closing the request body. We then use json.NewDecoder() to get ready and decode our JSON payload. If decode.Decoder() returns an error we’ll first return a 400 to the originating server. We then log out the error to the console and then return from the function.

func handleWebhook(res http.ResponseWriter, req *http.Request) {
  var Payload Payload
  defer req.Body.Close()

  decoder := json.NewDecoder(req.Body)
  err := decoder.Decode(&Payload)
  if err != nil {
    http.Error(res, "bad request: "+err.Error(), 400)
    log.Printf("bad request: %v", err.Error())
    return
  }

Assuming all has gone well we print the webhook details in to the console.

  log.Printf("%#v\n", Payload.Repository.Name)
  log.Printf("%#v\n", Payload.Issue.URL)
  log.Printf("%#v\n", Payload.Issue.Title)
  log.Printf("%#v\n", Payload.Issue.Body)
  log.Printf("%#v\n", Payload.Issue.Number)
  log.Printf("%#v\n", Payload.Issue.RepositoryURL)
}

For this basic example, our main consists of two handlers and http.ListenAndServe(). We may want to extend this at some point to allow the use of alternative HTTP clients. To keep the code simple for now we’ll leave it.

func main() {
  log.Println("Issuer")

  http.HandleFunc("/", status)
  http.HandleFunc("/webhook", handleWebhook)
  http.ListenAndServe(":3000", nil)
}

Next time

When next we meet we’ll be adding the ability to create an issue based on the payload received. If that goes well - we’ll even add the code to add issues to the TODO project. 🎉


You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.

{% github shindakun/atlg %}


Enjoy this post?
How about buying me a coffee?