About Github webhooks

Github Webhooks allow you to build or set up integrations, such as GitHub Apps or OAuth Apps, which subscribe to certain events on GitHub.com. When one of those events is triggered,a HTTP POST payload to the webhook’s configured URL will be sent. Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. You’re only limited by your imagination!

The most recent time I was working on a project where I needed to consume webhook events to trigger custom logic. Of course I chose Go, since Go’s standard library provides great functionality for HTTP client and server implementations out of the box.

There is also a nice Go library for accessing the GitHub API with google/go-github.

For Typescript there is also a great tooling to consume Github events, unfortunately I haven’t found something similar for Go. And that’s why I wrote a toolset for consuming Github events (which you can find here: cbrgm/githubevents) for Go last weekend and got inspired by octokit/webhook.js.

So today I want to show how to consume Github Webhook events with Go.

Consuming Github webhook events in Go

Webhooks are a great thing. In general, webhook receivers are used to register to certain events, which then perform one or more actions, depending on the type of event.

In Go we use two modules for this:

Here’s a simple example how to start a webhook server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
  "fmt"
  "github.com/cbrgm/githubevents/githubevents"
  "github.com/google/go-github/v43/github"
  "net/http"
)

func main() {
    // create a new event handler
    handle := githubevents.New("secretkey")
	
    // add callbacks
    handle.OnIssueCommentCreated(
      func(deliveryID string, eventName string, event *github.IssueCommentEvent) error {
          fmt.Printf("%s made a comment!", *event.Sender.Login)
          return nil
      }, 
    ) 
	
    // add a http handleFunc
    http.HandleFunc("/hook", func(w http.ResponseWriter, r *http.Request) {
        err := handle.HandleEventRequest(r)
        if err != nil {
            fmt.Println("error")
        }
    })
	
    // start the server listening on port 8080
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

In this example we create a new webhook handler, create a callback function that is triggered in the example when creating a comment in a Github issue. Then we create an http.HandleFunc that takes an http.Request and passes it to the EventHandler. Then we start the web server listening on port 8080. We now have a running webhook receiver for Github events in less than 30 lines of Go code!

You can use services like ngrok to expose your local port 8080 to the world. Enter the public domain name as the webhook endpoint. You can install webhooks on an organization or on a specific repository. To set up a webhook, go to the settings page of your repository or organization. From there, click Webhooks, then Add webhook. Alternatively, you can choose to build and manage a webhook through the Webhooks API.

Let’s walk through some patterns how we can structure a Go project which subscribes to multiple Github Events

Pass the EventHandler to function

Instead of defining all callbacks in a main() function, we can pass the githubevents.EventHandler as a pointer to functions. This way event subscriptions can be decomposed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
	"fmt"
	"github.com/cbrgm/githubevents/githubevents"
	"net/http"
)

func main() {
	handle := githubevents.New("")

	// pass the eventHandler to funcs that define callbacks
	newPing(handle)
	newPong(handle)

	http.HandleFunc("/hook", func(w http.ResponseWriter, r *http.Request) {
		err := handle.HandleEventRequest(r)
		if err != nil {
			fmt.Println("error")
		}
	})

	if err := http.ListenAndServe(":8080", nil); err != nil {
		panic(err)
	}
}

func newPing(handle *githubevents.EventHandler) {
	handle.OnBeforeAny(
		func(deliveryID string, eventName string, event interface{}) error {
			fmt.Println("ping!")
			return nil
		},
	)
}

func newPong(handle *githubevents.EventHandler) {
	handle.OnBeforeAny(
		func(deliveryID string, eventName string, event interface{}) error {
			fmt.Println("pong!")
			return nil
		},
	)
}

Use Go modules as “plugins”

It is also possible to define e.g. packages which return a githubevents.EventHandleFunc as importable modules. This way we can easily import modules in form of plugins and subscribe them to events.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"fmt"
	"github.com/cbrgm/githubevents/examples/simple-http-server-packages/plugins"
	"github.com/cbrgm/githubevents/githubevents"
	"net/http"
)

func main() {
	handle := githubevents.New("")

	// return handleFuncs from other packages
	// and use them ad "plugins"
	handle.OnIssueCommentCreated(
		plugins.NewResponder("ping!"),
		plugins.NewResponder("pong!"),
	)

	http.HandleFunc("/hook", func(w http.ResponseWriter, r *http.Request) {
		err := handle.HandleEventRequest(r)
		if err != nil {
			fmt.Println("error")
		}
	})

	if err := http.ListenAndServe(":8080", nil); err != nil {
		panic(err)
	}
}

A responder plugin could look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package plugins

import (
	"fmt"
	"github.com/cbrgm/githubevents/githubevents"
	"github.com/google/go-github/v43/github"
)

func NewResponder(msg string) githubevents.IssueCommentEventHandleFunc {
	// do some configuration here
	// ...
	return func(deliveryID string, eventName string, event *github.IssueCommentEvent) error {
		fmt.Printf("commenting %s", msg)
    // write a response...
		return nil
	}
}

Good to know

The nice thing about the toolset is that you can also register callbacks to all events or when errors occur.

OnBeforeAny for example registers callbacks which are triggered before any event. Registered callbacks are executed in parallel in separate Goroutines.

1
2
3
4
5
6
7
8
9
handle := githubevents.New("secretkey")
handle.OnBeforeAny(
    func(deliveryID string, eventName string, event interface{}) error {
        fmt.Printf("%s event received!", eventName)
        // do something
        return nil
    },
)
// ...

OnError registers callbacks which are triggered whenever an error occurs. These callbacks can be used for additional error handling, debugging or logging purposes.

1
2
3
4
5
6
7
8
9
handle := githubevents.New("secretkey")
handle.OnError(
	func(deliveryID string, eventName string, event interface{}, err error) error {
		fmt.Printf("received error %s", err)
		// additional error handling ...
		return err
	}, 
)
// ...

Conclusion

In summary, consuming Github events in Go is a great thing thanks to the great standard library, great client support for the Github API and type safety. I hope I gave you some useful examples of how to execute your own logic in response to consuming Github events. I would be interested to hear how you get on with the githubevents toolset and look forward to getting feedback from you!