← Back

Build your own Probot

A lot of people look at Probot and wonder how they can extend it - add custom routes, integrate it with other platforms, control its startup. I firmly believe that too many features and options can make a framework unwieldy, so rather than show where all of those things can fit in Probot itself, we’re going to take a look at building our own Probot.

Interestingly, the API design (started by @bkeepers) looks and feels a lot like a chatbot. I’m hoping that from this post, you (yes you!) will get an understanding of how Probot integrates with GitHub and why it feels easy to use, and then you’ll be able to bring those patterns to your own projects.

I’m going to leave the Why we built Probot until the end because while it is interesting and enlightening, we want to build stuff!

A couple of important notes:

  • Unless specifically stated, all code is pseudo-code, not copied directly from Probot. I’ll be oversimplifying some code so that we don’t have to think about edge-cases and complications.
  • Many parts of Probot have been extracted into smaller modules (shoutout @gr2m). We’ll talk about how they work, but you’ll probably just want to use them directly.
  • There’s a lot of code here - if you’re looking at something and wanting for some more explanation, please tell me!

Probot is an opinionated Express server

At its core, Probot uses Express to run a Node.js HTTP server. When an event happens on GitHub that your Probot app is configured to care about, GitHub sends HTTP POST requests (webhooks) to a special “webhook endpoint,” containing information in a JSON payload about what event was triggered. You can imagine code like this being a central part of the Probot framework (I’ll link to actual code shortly):

const express = require('express')
const app = express()

app.post('/', (req, res) => {
  // We got a webhook! Now run the Probot app's code.
})

When a Probot server receives a Webhook, it does a few things before actually running your code:

Probot webhook handling flow

First, it verifies the webhook signature; along with the JSON payload, GitHub sends an X-GitHub-Secret header. The value of the header is a combination of a secret key and the contents of the payload itself. GitHub and your Probot app both have the secret key (Probot uses the WEBHOOK_SECRET environment variable), so when the two services generate the header they should match exactly. If they don’t, Probot ignores the request.

This is a security measure to ensure that random POST requests aren’t acted upon - only GitHub can trigger your app. This logic is now abstracted in a separate module that Probot uses, @octokit/webhooks, for convenience and reusability. Here’s a contrived example of what Probot does internally:

app.post('/', (req, res) => {
  // Grab the header that we should be able to create ourselves
  const signatureHeader = req.get('X-Hub-Signature')
  // Verify the webhook payload
  const isValidWebhook = webhooks.verify(req.body, secretHeader)
  // -> true/false
})

Once the webhook verification is complete, Probot emits an event through its internal EventEmitter:

const express = require('express')
const EventEmitter = require('events')
const Webhooks = require('@octokit/webhooks')
const app = express()

const events = new EventEmitter()
const webhooks = new Webhooks({ secret: process.env.WEBHOOK_SECRET })

app.post('/', <