In this post, I will show how to build a GitHub App. I will start with a basic Node.js
and gradually turn it into a GitHub App. In later posts I will show other aspects of building and deploying GitHub Apps.
What is a GitHub App
A GitHub App is a Node.js
App that can interact with the GitHub API. Or, at least, this is the way I like to understand it.
A GitHub App is a type of integration that you can build to interact with and extend the functionality of GitHub. You can build a GitHub App to provide flexibility and reduce friction in your processes, without needing to sign in a user or create a service account.
GitHub Apps can be installed directly on organizations and personal accounts and granted access to specific repositories. They come with built-in webhooks and narrow, specific permissions.
Once you have written the code for your GitHub App, your app needs to run somewhere. If your app is a website or web app, you might host your app on a server like Azure App Service. If your app is a client-side app, it might run on a user’s device. For example, you might deploy your App as an Azure Function App which will use the supplied GitHub webhook to consume various GitHub API events.
GitHub Apps run persistently on a server or compute infrastructure that you provide or run on a user device. They can react to GitHub webhook events as well as events from outside the GitHub ecosystem. They are a good option for operations that span multiple repositories or organizations, or for providing hosted services to other organizations. A GitHub App is the best choice when building a tool with functions that occur primarily outside of GitHub or require more execution time or permissions than what a GitHub Actions workflow is allotted.
What tools do I need?
Note: I am running Ubuntu
22.04
as I think I get a betterNode.js
development experience, so if you are running directly on Windows the versions of the following could be different.
- Your computer or codespace should use Node.js version
v18.17.1
or greater. For more information, see Node.js. - npm version
9.8.1
or greater
Build the App
- Let’s start by building and running a basic hello world
Node.js
app so we can see the fundamentals of how we get from a basicNode.js
app to a full GitHub App. - Start by creating a file called
app.js
and put the following code inside it:
var msg = 'Hello World';
console.log(msg);
- then run
node app.js
in your terminal and you should seeHello World
written to the console. If you don’t please have a look at this VSCode post to get help https://code.visualstudio.com/docs/nodejs/nodejs-tutorial - So now we have our first
Node.js
app but we need to get it to the point where it becomes a GitHub App. For that we will need some dependencies. Let’s start with creating apackage.json
file and populating it. - To do this we can run
npm init
and this will ask us a few questions to get ourpackage.json
file populated. After I rannpm init
and answered the questions I got a fully populatedpackage.json
file as follows:
{
"name": "node_js_github_app",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"server": "node app.js",
"lint": "standard"
},
"description": "This is my my Node.js app that will become a GitHub App",
"main": "app.js",
"repository": {
"type": "git",
"url": "git+https://Github.com/russellmccloy/node_js_github_app.git"
},
"keywords": [
"node",
"Github",
"app",
"azure"
],
"author": "Russell McCloy",
"license": "ISC",
"bugs": {
"url": "https://Github.com/russellmccloy/node_js_github_app/issues"
},
"homepage": "https://Github.com/russellmccloy/node_js_github_app#readme"
}
- After that, run
npm install
to create apackage-lock.json
file.package-lock.json
is a file that is automatically generated by npm when a package is installed. It records the exact version of every installed dependency, including its sub-dependencies and their versions.
- Run
node app.js
in your terminal again to make sure you still get the Hello world output to ensure we didn’t break anything before continuing. - now we need to add some dependencies to get our App closer to being a GitHub App.
- At the end of our
package.json
file I added the following (add a new comma before it or you will break yourjson
):
"devDependencies": {
"smee-client": "^1.2.3",
"standard": "^17.0.0",
"vite": "^4.1.0"
},
"dependencies": {
"dotenv": "^16.0.3",
"octokit": "^2.0.14"
}
Now my package.json
file looks like the following:
{
"name": "node_js_github_app",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"server": "node app.js",
"lint": "standard"
},
"description": "This is my my Node.js app that will become a GitHub App",
"main": "app.js",
"repository": {
"type": "git",
"url": "git+https://github.com/russellmccloy/node_js_github_app.git"
},
"keywords": [
"node",
"github",
"app",
"azure"
],
"author": "Russell McCloy",
"license": "ISC",
"bugs": {
"url": "https://github.com/russellmccloy/node_js_github_app/issues"
},
"homepage": "https://github.com/russellmccloy/node_js_github_app#readme",
"devDependencies": {
"standard": "^17.0.0",
"vite": "^4.1.0"
},
"dependencies": {
"dotenv": "^16.0.3",
"octokit": "^2.0.14",
"smee-client": "^1.2.3"
}
}
- run
npm install
again so ourpackage-lock.json
file gets updated with the new dependencies.
Regarding the dependencies I added
smee-client
- is a Webhook payload delivery service. This is useful when we are developing locally and need to receive webhook payloads. once we deploy our App to somewhere like an Azure Function App we wont need smee any more.standard
- I think this is a bunch of the most common dependencies most developers might want with a Node App.vite
- is a build tool that aims to provide a faster and leaner development experience for modern web projects. https://vitejs.dev/guide/. I am not sure I need this but I don’t want to remove it as I don’t want to risk breaking things as I have done many times today!dotenv
- is a zero-dependency module that loads environment variables from a .env file into process.env. most apps need environment settings so this make sense to load this one.octokit
- The all-batteries-included GitHub SDK for Browsers, Node.js, and Deno.
Getting back to building our GitHub App
- Firstly, you will need to register a new GitHub App. Please following these instructions: Register a GitHub App. the following list of instructions also relate to registering your GitHub App.
- You will need setup a repository that the GitHub App will act on. I created this repository: https://Github.com/russellmccloy/node_js_github_app_working_repo
- On the above mentioned repository you will need to install the GitHub app. follow these instructions https://docs.Github.com/en/apps/using-Github-apps/installing-your-own-Github-app
- Make sure you opt to create a
README
file when you set it up as we will enter this value in the Homepage URL: https://Github.com/russellmccloy/node_js_github_app_working_repo/blob/main/README.md when registering your GitHub App. - As we are not going to have anywhere, initially, to host our GitHub App (GitHub Apps are not hosted in GitHub. They could be hosted in Azure as a Function App for example.) we will use https://smee.io/ which is a Webhook payload delivery service.
- Go into smee and click on Start a new channel and then copy this url (https://smee.io/my_channel_id) into the Webhook URL field in the GitHub App registration page.
- Enter a Webhook secret into the registration page. This needs to match the
WEBHOOK_SECRET
value in your.env
file mentioned further down this page. - In Repository Permissions go and select what you want.
-
I selected Pull Requests - Read / Write:
- in the Subscribe to Events section I selected
Pull request
- Save your GitHub App registration.
- Now go and select to generate a private key. This will download a private key file to your machine. I saved this to
c:\node-js-Github-app.2023-08-14.private-key.pem
but this is probably not the best place to save this. -
Now in root of your
Node.js
project create a.env
file and add the following:APP_ID="375863" PRIVATE_KEY_PATH='c:\app-az-func-app.2023-08-10.private-key.pem' WEBHOOK_SECRET="<MY_SECRET_GET_YOUR_OWN>"
You will now need more code now as we are getting closer to becoming a GitHub App
As we now have most of the parts setup we will need more code to receive the events that GitHub emits.
Add the following code to your app.js
import dotenv from 'dotenv'
import fs from 'fs'
import http from 'http'
import { Octokit, App } from 'octokit'
import { createNodeMiddleware } from '@octokit/webhooks'
// Load environment variables from .env file
dotenv.config()
// Set configured values
const appId = process.env.APP_ID
const privateKeyPath = process.env.PRIVATE_KEY_PATH
const privateKey = fs.readFileSync(privateKeyPath, 'utf8')
const secret = process.env.WEBHOOK_SECRET
const enterpriseHostname = process.env.ENTERPRISE_HOSTNAME
const messageForNewPRs = fs.readFileSync('./message.md', 'utf8')
// Create an authenticated Octokit client authenticated as a GitHub App
const app = new App({
appId,
privateKey,
webhooks: {
secret
},
...(enterpriseHostname && {
Octokit: Octokit.defaults({
baseUrl: `https://${enterpriseHostname}/api/v3`
})
})
})
// Optional: Get & log the authenticated app's name
const { data } = await app.octokit.request('/app')
// Read more about custom logging: https://Github.com/octokit/core.js#logging
app.octokit.log.debug(`Authenticated as '${data.name}'`)
// Subscribe to the "pull_request.opened" webhook event
app.webhooks.on('pull_request.opened', async ({ octokit, payload }) => {
console.log(`Received a pull request event for #${payload.pull_request.number}`)
try {
await octokit.rest.issues.createComment({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: payload.pull_request.number,
body: messageForNewPRs
})
} catch (error) {
if (error.response) {
console.error(`Error! Status: ${error.response.status}. Message: ${error.response.data.message}`)
} else {
console.error(error)
}
}
})
// Optional: Handle errors
app.webhooks.onError((error) => {
if (error.name === 'AggregateError') {
// Log Secret verification errors
console.log(`Error processing request: ${error.event}`)
} else {
console.log(error)
}
})
// Launch a web server to listen for GitHub webhooks
const port = process.env.PORT || 3000
const path = '/api/webhook'
const localWebhookUrl = `http://localhost:${port}${path}`
// See https://Github.com/octokit/webhooks.js/#createnodemiddleware for all options
const middleware = createNodeMiddleware(app.webhooks, { path })
http.createServer(middleware).listen(port, () => {
console.log(`Server is listening for events at: ${localWebhookUrl}`)
console.log('Press Ctrl + C to quit.')
})
How to run locally and debug
Debugging
- Go to your Run and Debug section in VSCode
- Click on
create a launch.json file
- Choose
Node.js
from the dropdown -
You
launch.json
file should now look like the following:{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "<node_internals>/**" ], "program": "${file}" } ] }
Now just hit F5
and the debugger will start:
Running without debugging
If you just want to run your app and watch what is going on in the Console then just run:
npm run server
And you should see the following output:
> node_js_github_app@0.0.1 server
> node app.js
Authenticated as 'node-js-github-app'
Server is listening for events at: http://localhost:3000/api/webhook
Press Ctrl + C to quit.
Note: you need to be listening to the web hook proxy endpoint so run the following in a new / different terminal window in VSCode:
npx smee -u https://smee.io/<XXXXXXXXX> -t http://localhost:3000/api/webhook
When you run this you will see the following:
Forwarding https://smee.io/8YB3XiGOLHR5SGh to http://localhost:3000/api/webhook
Connected https://smee.io/8YB3XiGOLHR5SGh
Now, when you raise a new Pull Request in the Repository that you installed the gitHub App into, the smee client
Webhook payload delivery service will receive the webhook event from GitHub and your code will detect this as it is listening to smee
A message will be create in the pull request using this file
here is the pull request showing the Message:
Best Practice
Best practices for creating a GitHub App
What are all the GitHub events I can consume
There are heaps of GitHub events you can consume. Here is a list:
That’s all for now.