Quickstart: Building Your First GitHub App

Introduction

This guide will help you build a GitHub App and run it on a server. The app you'll build will add a label to any new issue that's opened in the repository the app is installed on. After completing this guide, you'll have the tools you need to enhance your workflow with GitHub Apps.

This project will walk you through the following:

  • Registering a GitHub App
  • Using Smee to receive webhook payloads
  • Running a simple web server via Sinatra
  • Programming your app to listen for events
  • Authenticating as a GitHub App
  • Authenticating as an installation
  • Using the Octokit.rb library to do REST API operations

Note: This guide demonstrates the app development process using the Ruby programming language. However, there are many flavors of Octokit. If you prefer JavaScript, you can use Probot and Node.js to develop GitHub Apps.

Once you've worked through the steps, you'll be ready to develop other kinds of integrations using the full suite of GitHub APIs. You can check out successful examples of apps on GitHub Marketplace and Works with GitHub.

Prior knowledge needed

You may find it helpful to have a basic understanding of the following:

But you can follow along at any experience level. We'll link out to information you need along the way!

One-time setup

You'll need to complete the following tasks as part of the one-time setup for this project:

Once you've completed these steps, you can start building your app!

Download app boilerplate

Download the app boilerplate code to your local machine and save it somewhere for later.

You'll be customizing this code in "Building the app." For now, just remember where you download it.

Start a new Smee channel

To help GitHub send webhooks to your local machine without exposing it to the internet, you can use a tool called Smee. First, go to https://smee.io and click Start a new channel. (If you're already comfortable with other tools that expose your local machine to the internet like (https://dashboard.ngrok.com/get-started) or localtunnel, feel free to use those).

The Smee new channel button

Starting a new Smee channel creates a unique domain where GitHub can send webhook payloads. You'll need to know this domain for the next step. Here is an example of a unique domain at https://smee.io/qrfeVRbFbffd6vD:

A Smee unique channel

Next, open up your Terminal, and follow the steps on the page under "Use the CLI" to install and run the Smee command-line client:

  1. Install the client:

    npm install --global smee-client
    
  2. Run the client (replacing https://smee.io/qrfeVRbFbffd6vD with your own domain):

    smee -u https://smee.io/qrfeVRbFbffd6vD
    

    You should see output like the following:

    Forwarding https://smee.io/qrfeVRbFbffd6vD to http://127.0.0.1:3000/
    Connected https://smee.io/qrfeVRbFbffd6vD
    

The smee -u <unique_channel> command tells Smee to forward all webhook events that are sent to that channel to the Smee client running on your computer. Because of this, your machine does not need to be open to the public internet to receive webhooks from GitHub. You can also open that Smee URL in your browser to inspect webhook payloads as they come in. Note: You'll also be connecting your web server to port 3000.

Note: You only need to follow the steps under "Use the CLI" for this example. Don't follow the other instructions on the Smee page.

We recommend leaving this Terminal window open and keeping Smee connected while you complete the rest of the steps in this guide. Although you can disconnect and reconnect the Smee client without losing your unique domain (unlike ngrok), you may find it easier to leave it connected and do other command-line tasks in a different Terminal window.

If you don't yet have a GitHub account, now is a great time to join. Don't forget to verify your email before continuing!

Register a new app with GitHub

To register a new app, visit the app settings page in your GitHub profile, and click New GitHub App.

GitHub website, showing the **New App**

You'll see a form where you can enter details about your app. See "Creating a GitHub App" for general information about the fields on this page. For the purposes of this guide, you'll need to enter specific data in a few fields:

Note: You can always update these settings later to point to a hosted server.

  • For the "Homepage URL", use the domain issued by Smee. For example:

    Form with Smee domain filled in for homepage URL

  • For the "Webhook URL", again use the domain issued by Smee. For example:

    Form with Smee domain filled in for webhook URL

  • For the "Webhook secret", create a password to secure your webhook endpoints. This should be something that only you (and GitHub, via this form) know.

    Form with webhook secret filled in

  • On the Permissions & Webhooks page, you can specify a set of permissions for your app, which determines how much data your app has access to. For now, leave this page with its default values. You'll update these permissions later.

  • At the bottom of the Permissions & Webhooks page, specify whether this is a private app or a public app. This refers to who can install it: just you, or anyone in the world? For now, leave the app as private by selecting Only on this account.

    GitHub App privacy

Click Create GitHub App to create your app!

Save your private key and App ID

After you create your app, you'll be taken back to the app settings page. You have two more things to do here:

  • Generate a private key for your app. This is necessary to authenticate your app later on. Scroll down on the page and click Generate a private key. Save the resulting PEM file (called something like private-key.pem) in a directory where you can find it again.

    The private key generation dialog

  • Note the app ID GitHub has assigned your app. You'll need this to prepare your runtime environment.

    Your app's ID number

Prepare the runtime environment

To keep your information secure, we recommend putting all your app-related secrets in your computer's memory where your app can find them, rather than putting them directly in your code. You can do this by creating a few environment variables:

  • Start with the app ID you noted in the previous section. In a new Terminal tab (leave Smee running in the other one!), enter the following command, but replace 12345 with your own app ID:

    export GITHUB_APP_IDENTIFIER=12345
    
  • Next, add your webhook secret:

    export GITHUB_WEBHOOK_SECRET="your secret here"
    
  • Finally, add the private key you generated and saved previously. Because the key is stored in a file, you can process it using awk and store it in memory in a way your app can read. For example, if you saved your private key as private-key.pem, you would enter:

    export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' path/to/your/private-key.pem`
    

Start the server

Your nascent app doesn't do anything yet, but at this point, you can get it running on the server.

In the same Terminal window where you entered the environment variable commands, cd into the directory where you downloaded the boilerplate app code. The Ruby code in this repository will start up a Sinatra web server. This code has a few dependencies. You can install these by running:

gem install bundler

Followed by:

bundle install

With the dependencies installed, you can start the server:

ruby server.rb

You should see a response like:

== Sinatra (v2.0.3) has taken the stage on 3000 for development with backup from Puma
Puma starting in single mode...
* Version 3.11.2 (ruby 2.4.0-p0), codename: Love Song
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://localhost:3000
Use Ctrl-C to stop

If you see an error, make sure you're running the server in the same window where you entered the export commands above. If not, run those commands again before starting the server.

Once the server is running, you can test it by going to http://localhost:3000 in your browser. If the app works as expected, you'll see a helpful error page:

Sinatra's 404 error page

This is good! Even though it's an error page, it's a Sinatra error page, which means your app is connected to the server as expected. You're seeing this message because you haven't given the app anything else to show.

Install the app on your account

You can test that the server is listening to your app by installing the app on your GitHub account and checking the output in Terminal.

To install the app, visit the app settings page, choose your app, and click Install App in the sidebar. Next to your username, click Install.

You'll be asked whether to install the app on all repositories or just a selection. If you don't want to install the app on all of your repositories, that's okay! You may want to create a sandbox repository for testing purposes and install your app there.

App installation permissions

After you click Install, look at the output in your Terminal. You should see something like this:

D, [2018-06-29T15:45:43.773077 #30488] DEBUG -- : ---- recevied event integration_installation
D, [2018-06-29T15:45:43.773141 #30488] DEBUG -- : ----         action created
192.30.252.44 - - [29/Jun/2018:15:45:43 -0400] "POST / HTTP/1.1" 200 2 0.0067
D, [2018-06-29T15:45:43.833016 #30488] DEBUG -- : ---- recevied event installation
D, [2018-06-29T15:45:43.833062 #30488] DEBUG -- : ----         action created
192.30.252.39 - - [29/Jun/2018:15:45:43 -0400] "POST / HTTP/1.1" 200 2 0.0019

This is good news! It means your app received a notification that it was installed on your GitHub account. If you see something like this, your app is running on the server as expected. 🙌

Building the app

If you're wondering where the Terminal output above is coming from, it's written in the app boilerplate code in server.rb. Open this file in a text editor. It's time to dig into the code and customize your app!

server.rb contains app boilerplate code that has not yet been customized. In this file, you'll see some placeholder code for handling webhook events, and some other code for initializing a Octokit.rb client. The remainder of this guide customizes this code so that the app automatically adds a needs-response label to issues that are opened in a repository the app is installed on.

Note: server.rb contains many code comments that complement this guide and explain additional technical details. You may find it helpful to read through the comments in that file now, before continuing with this section, to get an overview of how the code works.

The final customized code that you'll create by the end of this guide is provided in advanced_server.rb. We recommend waiting until the end to look at it, though!

The following sections will walk you through these procedures:

Update App permissions

When you first registered your app, you accepted the default permissions, which means your app doesn't have access to most operations. For this example, your app will need permission to read issues and write labels. To update its permissions, return to the app settings page, choose your app, and click Permissions & Webhooks in the sidebar.

  • In the permissions section, find Issues, and select Read & Write in the Access dropdown next to it. The description says this option grants access to both issues and labels, which is just what you need.
  • In the events section, subscribe to Issues events.

Save your changes!

Important step: Check your email and follow the link to accept the new permissions. Any time you change your app's permissions or webhooks, users who have installed the app (including yourself!) will need to accept the new permissions before the changes take effect.

Great! Your app has permission to do the tasks you want it to do. Now you can add the code to make it work.

Add event handling

The first thing your app needs to do is listen for new issues that are opened.

To do this, you can use the IssuesEvent type, which is triggered when certain issue-related actions occur. You can filter this event type for the specific action you want in your code. In server.rb, the event handling boilerplate starts after this line:

post '/' do

The / refers to the route of the webhook URL you entered when you registered your app. The post method allows the web server to receive events that take place on GitHub at that route.

If you scroll down a bit, you'll see some placeholder code ready to be customized:

case request.env['HTTP_X_GITHUB_EVENT']
when :the_event_that_i_care_about
  # Add code here to handle the event that you care about!
  handle_the_event_that_i_care_about(payload)
end

A bit further down, server.rb includes a section (under helpers do) where you can add helper methods. You should see one placeholder method definition in this section:

def handle_the_event_that_i_care_about(payload)
  logger.debug 'Handling the event that we care about!'
  true
end

First, in the case statement, update the when condition so that it runs when an issues event with an opened action is received. For example:

when 'issues'
  if payload['action'] === 'opened'
    handle_issue_opened_event(payload)
  end
end

Next, update the handle_the_event_that_i_care_about method definition to match whatever you named it above. Something like this:

def handle_issue_opened_event(payload)
  logger.debug 'An issue was opened!'
  true
end

Note: This method receives a JSON-formatted event payload as an argument. This means you can parse the payload in the method and drill down to any specific data you need. You may find it helpful to inspect the full payload at some point: try changing logger.debug 'An issue was opened!' to logger.debug payload. The payload structure you see should match what's shown in the IssuesEvent docs.

Great! It's time to test the changes.

Note: You'll need to restart the Sinatra server before you can test changes. Enter Ctrl-C to stop the server, and then run ruby server.rb again. If you don't want to do this every time you change your app code, you can look into reloading.

In your browser, visit the repository where you installed your app. Open a new issue in this repository. The issue can say anything you like. It's just for testing.

When you look back at your Terminal, you should see a message in the output that says, An issue was opened! Congrats! You've added an event handler to your app. 💪

Get started with Octokit.rb

Okay, your app can tell when issues are opened. Now you want it to add the label needs-response to any newly opened issue in a repository the app is installed in.

Before the label can be added anywhere, you'll need to create the custom label in your repository. You'll only need to do this one time. For the purposes of this guide, create the label manually via the GitHub website. In your repository, click Issues, then Labels, then click New label. Name the new label needs-response.

Tip: Wouldn't it be great if your app could create the label programmatically? It can! Try adding the code to do that on your own after you finish the steps in this guide.

Now that the label exists, you can program your app to use the REST API to add the label to any newly opened issue.

For this task, you'll want to use the Octokit.rb Ruby library. But here's a minor bump in the road: doing anything interesting with this library will require you, or rather, your app, to authenticate.

Authenticate as a GitHub App

If you look in server.rb, you'll see it initializes an Octokit client:

@client ||= Octokit::Client.new(bearer_token: jwt)

The code above this line generates a JSON Web Token (JWT) and uses it (along with your app's private key) to initialize an authenticated Octokit.rb client. For more information about authenticating using a JWT, see "Authenticating as a GitHub App."

Great—the client is already authenticated as a GitHub App! Unfortunately, that's not enough to do much with the API. According to the docs:

Authenticating as a GitHub App lets you do a couple of things:

  • You can retrieve high-level management information about your GitHub App.
  • You can request access tokens for an installation of the app.

The second item hints at the missing piece of the puzzle: to perform actions via the API, your app needs to authenticate as an installation. Let's do that!

Note: An installation refers to any user or organization account that has installed the app. Even if someone installs the app on more than one repository, it only counts as one installation because it's within the same account.

Authenticate as an installation

The docs show a cURL you can run to get an installation access token, but you're using Octokit.rb. The corresponding Octokit.rb method is create_app_installation_access_token.

This method accepts two arguments:

  • Installation (integer): The ID of a GitHub App installation
  • Options (hash, defaults to {}): A customizable set of options

You can omit the options hash because it will default to {}. But how do you get the installation ID?

You may remember inspecting the full event payload earlier. If you still have that log message around, you can see that the payload includes an installation object with an id. Don't do anything with that specific value. Instead, write your method so it parses the payload and gets the ID programmatically. This way your code will still work if your app is installed on other accounts.

Once you can get the ID, you can call create_app_installation_access_token(installation_id) on the JWT-authenticated client to generate an access token for each installation. If you look back at the docs, you can see the response includes two fields: token and expired_at. You can select for just the token field in your code.

Armed with all of the above information, you can write a new helper method in server.rb. Try adding the following code above the helper method you added previously (def handle_issue_opened_event(payload)):

def authenticate_installation(payload)
  installation_id = payload['installation']['id']
  installation_token = @client.create_app_installation_access_token(installation_id)[:token]
  @bot_client ||= Octokit::Client.new(bearer_token: installation_token)
end

You'll want to call this helper method when the issues event is triggered. You can do this by adding authenticate_installation(payload) to your when 'issues' block:

when 'issues'
  authenticate_installation(payload)
  if payload['action'] === 'opened'
    handle_issue_opened_event(payload)
  end
end

Now when an issues event is triggered, your code calls the authenticate_installation helper method, which returns a @bot_client instance variable.

With a @bot_client instance variable ready to use, you can run API operations!

Add label handling

Congrats—you've made it to the final step: adding label handling to your app.

In the Octokit.rb docs, find the list of label methods. The method you'll want to use is add_labels_to_an_issue.

Back in server.rb, update the method you defined previously:

def handle_issue_opened_event(payload)
  logger.debug 'An issue was opened!'
  true
end

The add_labels_to_an_issue docs show you'll need to pass three arguments to this method:

  • Repo (string in "owner/name" format)
  • Issue number (integer)
  • Labels (array)

You can parse the payload to get both the repo and the issue number. Since the label name will always be the same (needs-response), you can pass it as a hardcoded string. Putting these pieces together, your updated method might look like this:

def handle_issue_opened_event(payload)
  repo = payload['repository']['full_name']
  issue_number = payload['issue']['number']
  @bot_client.add_labels_to_an_issue(repo, issue_number, ['needs-response'])
end

Try opening a new issue in your test repository and see what happens! If nothing happens right away, try refreshing.

You won't see much in the Terminal, but you should see that a bot user has added a label to the issue.

Note: When GitHub Apps take actions via the API, such as adding labels, GitHub.com shows these actions as being performed by bot accounts. For more information, see "Machine vs. bot accounts."

If so, congrats! You've successfully built a working app! 🎉

You can see the final code in advanced_server.rb in the app boilerplate repository.

See "Next steps" for ideas about where you can go from here.

Troubleshooting

Here are a few common problems and some suggested solutions. If you run into any other trouble, you can ask for help or advice in the GitHub Community Forum: https://github.community/t5/GitHub-Quickstart-Guides/bd-p/GitHubQuickstartGuides

  • Q: When I try to install the Smee command-line client, I get the following error:

    npm: command not found
    

    A: Looks like you don't have npm installed. The best way to install it is to download the Node.js package at https://nodejs.org and follow the installation instructions for your system. npm will be installed alongside Node.js.

  • Q: When I run the server, I get the following error:

    server.rb:38:in `initialize': Neither PUB key nor PRIV key: header too long (OpenSSL::PKey::RSAError)
    

    A: You probably haven't set up your private key environment variable quite right. Try running this command:

    env | grep GITHUB_PRIVATE_KEY
    

    If your key is stored correctly, you will see:

    GITHUB_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----
    

    If you don't see that result, you may have mistyped either the path to your private key or its filename when you added it to your environment. Double-check the location and filename of your key.

  • Q: When I run the server, it crashes with this error:

    Octokit::Unauthorized ... 401 - Bad credentials`
    

    A: You may be authenticated as a GitHub App but not as an installation. Make sure you follow all the steps under "Authenticate as an installation," and use the @bot_client instance variable (authenticated with an installation access token) for your API operations, not the @client instance variable (authenticated with a JWT). The @client can only retrieve high-level information about your app and obtain installation access tokens. It can't do much else in the API.

  • Q: My server isn't listening to events! The Smee client is running in a Terminal window, and I'm sending events on GitHub.com by opening new issues, but I don't see any output in the Terminal window where I'm running the server.

    A: You may not have the correct Smee domain in your app settings. Visit your app settings page and double-check the fields shown in "Register a new app with GitHub." Make sure the domain in those fields matches the domain you used in your smee -u <unique_channel> command in "Start a new Smee channel."

  • Q: My app doesn't work! I opened a new issue, but even after refreshing, no label has been added to it.

    A: Make sure all of the following are true:

Conclusion

After walking through this guide, you've learned the basic building blocks for developing GitHub Apps! To review, you:

  • Registered a new GitHub App
  • Used Smee to receive webhook payloads
  • Ran a simple web server via Sinatra
  • Programmed your app to listen for events
  • Used the Octokit.rb library to do REST API operations
  • Authenticated as a GitHub App
  • Authenticated as an installation

Next steps

Here are some ideas for what you can do next:

  • Rewrite your app using GraphQL!
  • Rewrite your app in Node.js using Probot!
  • Have the app check whether the needs-response label already exists on the issue, and if not, add it.
  • When the bot successfully adds the label, show a message in the Terminal. (Hint: compare the needs-response label ID with the ID of the label in the payload as a condition for your message, so that the message only displays when the relevant label is added and not some other label.)
  • Add a landing page to your app and hook up a Sinatra route for it.
  • Move your code to a hosted server (like Heroku). Don't forget to update your app settings with the new domain.
  • Share your project or get advice in the Community Forum: https://github.community/t5/GitHub-Quickstart-Guides/bd-p/GitHubQuickstartGuides
  • Have you built a shiny new app you think others might find useful? Add it to GitHub Marketplace!