Back in 2014, I wrote a post describing a simple payload server for GitHub webhooks, using Python 3. That server could be deployed to any VPS listening on a custom port.

Now it's 2019, and deploying servers to VPSs doesn't make me feel hip enough. All the cool kids are into serverless now, so I decided to rewrite the same payload server in Go and deploy it as a Google Cloud Function. This brief post can serve as a basic tutorial on how to do it.

I assume you already have a GCP account (there's a free tier), and the gcloud command-line tool is configured to authenticate with your account and project name.

You can see the full code here, but this is the important part:

func Payload(w http.ResponseWriter, r *http.Request) {
  body, err := ioutil.ReadAll(r.Body)
  if err != nil {
    log.Println(err)
    return
  }
  log.Println("Header:\n---------")
  fmt.Println(r.Header)

  if !validateSignature(body, r) {
    m := "Signature validation failed"
    log.Println(m)
    w.Write([]byte(m))
    return
  }

  fmt.Println("Body:\n---------")
  log.Println(string(body))
}

Payload is a standard http.HandlerFunc, and its signature should be familiar to anyone who has written HTTP servers in Go. GitHub sends its payload as a POST request (hence our reading from r.Body) with some special headers for validation. The validation code runs a SHA1 HMAC to ensure that GitHub knows a secret key shared with the application (this helps keep intruders away from your payload server).

The full code sample has this validation code, as well as a simple unit test for Payload. It doesn't actually attempt to create a properly signed message, but checks that Payload is alive and returns a valid HTTP response. In general, it is highly recommended to unit-test these handlers locally, because cloud function deployment takes many seconds and isn't very convenient for short edit-test cycles.

To deploy this function, we'll go to the directory where payloadserver.go lives, and run:

$ gcloud functions deploy payloadserver \
      --entry-point Payload \
      --runtime go111 \
      --trigger-http \
      --set-env-vars HOOK_SECRET_KEY=<your secret key>

This prints out a URL for your function; it looks something like:

httpsTrigger:
  url: https://<region>-<project-name>.cloudfunctions.net/payloadserver

Which is what you'll point the GitHub webhook to. Configure the webhook to send all events, and then test it by creating or modifying some issue in your repository. The webhook management page on GitHub should now show the event in "Recent deliveries". You can also check the logs of your cloud function, either from the GCP control panel, or from the command-line:

$ gcloud functions logs read payloadserver

If everything ran successfully, you'll see the headers and the body of the payload emitted to the log. That's it!

Jokes about hipness aside, once you get the hang of it cloud functions seem like a particularly easy way to deploy simple web servers and apps for specific needs. There's a lot of powerful functionality behind the simple facade - for example, resources will automatically scale with the load. That said, be aware of the costs if you're doing it for anything high-volume.