├── .gitignore ├── package.json ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | private-key.pem 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "github-app": "^3.0.0", 5 | "github-webhook-handler": "^0.6.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var createHandler = require('github-webhook-handler'); 3 | 4 | var handler = createHandler({ 5 | path: '/', 6 | secret: process.env.WEBHOOK_SECRET || 'development' 7 | }); 8 | 9 | http.createServer(function (req, res) { 10 | handler(req, res, function (err) { 11 | res.statusCode = 404 12 | res.end('no such location') 13 | }); 14 | }).listen(process.env.PORT || 7777); 15 | 16 | var fs = require('fs'); 17 | var createApp = require('github-app'); 18 | 19 | var app = createApp({ 20 | id: process.env.APP_ID, 21 | cert: process.env.PRIVATE_KEY || fs.readFileSync('private-key.pem') 22 | }); 23 | 24 | handler.on('error', function (err) { 25 | console.error('Error:', err.message) 26 | }); 27 | 28 | handler.on('issues', function (event) { 29 | console.log("EVENT", event); 30 | if (event.payload.action === 'opened') { 31 | var installation = event.payload.installation.id; 32 | app.asInstallation(installation).then(function (github) { 33 | github.issues.createComment({ 34 | owner: event.payload.repository.owner.login, 35 | repo: event.payload.repository.name, 36 | number: event.payload.issue.number, 37 | body: 'Welcome to the robot uprising.' 38 | }); 39 | }); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building a GitHub App in Node.js 2 | 3 | [GitHub Apps](https://developer.github.com/apps/) are a new way to extend GitHub. They can be installed directly on organizations and user accounts and granted access to specific repositories. They come with granular permissions and built-in webhooks. Apps are first class actors within GitHub. 4 | 5 | This tutorial will walk you through creating an app with Node.js that comments on any new issue that is opened on a GitHub repository. 6 | 7 | ## Receiving webhooks 8 | 9 | Usually an app will be responding to a [webhook](https://developer.github.com/webhooks/) from GitHub. Webhooks are fired for almost every significant action that users take on GitHub, whether it's pushes to code, opening or closing issues, opening or merging pull requests, or commenting on a discussion. 10 | 11 | The [github-webhook-handler] module makes it easy to add support for webhooks to Node.js web servers. Start by installing it in your project: 12 | 13 | ``` 14 | $ npm install --save github-webhook-handler 15 | ``` 16 | 17 | [github-webhook-handler] includes an example in the `README`, so we'll will use a truncated version of that, starting with creating a webhook handler and listening for an event: 18 | 19 | ```js 20 | var createHandler = require('github-webhook-handler'); 21 | 22 | var handler = createHandler({ 23 | path: '/', 24 | secret: 'myhashsecret' 25 | }); 26 | 27 | handler.on('issues', function (event) { 28 | console.log('Received an issue event for %s action=%s: #%d %s', 29 | event.payload.repository.name, 30 | event.payload.action, 31 | event.payload.issue.number, 32 | event.payload.issue.title) 33 | }); 34 | ``` 35 | 36 | `createHandler` returns a middleware that handles all the logic of receiving and verifying webhook requests from GitHub. It has an `on` method that can be used to listen to any of the GitHub event types. The [`issues`](https://developer.github.com/v3/activity/events/types/#issuesevent) webhook event is fired whenever an issue is opened, closed, edited, assigned, labeled, etc. 37 | 38 | In order to receive these webhooks from GitHub, our app needs an HTTP server. We are going to use the builtin `http` module, but `handler` also works as a [middleware](http://expressjs.com/en/guide/using-middleware.html) in [express](http://expressjs.com/). 39 | 40 | ```js 41 | var http = require('http'); 42 | 43 | http.createServer(function (req, res) { 44 | handler(req, res, function (err) { 45 | res.statusCode = 404 46 | res.end('no such location') 47 | }); 48 | }).listen(7777); 49 | ``` 50 | 51 | Now we have a node server that can receive webhooks, so our app knows when something interesting happens on GitHub. But as they say, knowing is only half the battle. 52 | 53 | ## Integrating with GitHub 54 | 55 | A GitHub App is a first-class actor on GitHub, like a user (e.g. [@defunkt](https://github.com/defunkt)) or a organization (e.g. [@github](https://github.com/github)). That means it can be given access to repositories and perform actions through the API like [commenting on an issue](https://developer.github.com/v3/issues/comments/#create-a-comment) or [creating a status](https://developer.github.com/v3/repos/statuses/#create-a-status). The app is given access to a repository or repositories by being "installed" on a user or organization account. 56 | 57 | Unlike a user, an app doesn't sign in through the website (it is a robot, after all). Instead, it authenticates by signing a token with a private key, and then requesting an access token to perform actions on behalf of a specific installation. The [docs](https://developer.github.com/apps/building-integrations/setting-up-and-registering-github-apps/about-authentication-options-for-github-apps/) cover this in more detail, but I'm skimming over it because we don't really need to know the implementation details. 58 | 59 | The [github-app] module handles all the authentication details for us. It returns and instance of the [github][node-github] Node.js module, which wraps the [GitHub API](https://developer.github.com/v3/) and allows you to do almost anything programmatically that you can do through a web browser. Install it on your project with: 60 | 61 | ``` 62 | $ npm install --save github-app 63 | ``` 64 | 65 | Configuring the app requires the `id` of the app and the private key certificate, which we'll get later. 66 | 67 | ```js 68 | var createApp = require('github-app'); 69 | 70 | var app = createApp({ 71 | id: process.env.APP_ID, 72 | cert: require('fs').readFileSync('private-key.pem') 73 | }); 74 | ``` 75 | 76 | An app can request to authenticate as a specific installation by calling `app.asInstallation(id)` and passing the _installation_ id as an argument. Every webhook payload from an installation includes the id, so for now we don't have to worry about how to get it or where to save it. 77 | 78 | Our app is complete by updating the webhook handler to authenticate as the installation and then perform an action using the [github][node-github] API client. 79 | 80 | ```js 81 | handler.on('issues', function (event) { 82 | if (event.payload.action === 'opened') { 83 | var installation = event.payload.installation.id; 84 | 85 | app.asInstallation(installation).then(function (github) { 86 | github.issues.createComment({ 87 | owner: event.payload.repository.owner.login, 88 | repo: event.payload.repository.name, 89 | number: event.payload.issue.number, 90 | body: 'Welcome to the robot uprising.' 91 | }); 92 | }); 93 | } 94 | }); 95 | ``` 96 | 97 | ## Testing it out 98 | 99 | [register a new app](https://github.com/settings/apps/new) 100 | 101 | [Configuring Your Server](https://developer.github.com/webhooks/configuring/) 102 | 103 | ngrok 104 | 105 | ``` 106 | ngrok http 7777 107 | ``` 108 | 109 | ## Next Steps 110 | 111 | This example is about the simplest app imaginable, and it isn't terribly useful. 112 | 113 | Apps don't have to wait for a webhook to take action. 114 | 115 | ```js 116 | handler.on('installation', function (event) { 117 | if (event.payload.action == 'created') { 118 | console.log("App installed", event.payload.installation); 119 | } else if (event.payload.action == 'deleted') { 120 | console.log("App uninstalled", event.payload.installation); 121 | } 122 | }); 123 | ``` 124 | 125 | https://developer.github.com/apps/ 126 | 127 | [github-webhook-handler]: https://github.com/rvagg/github-webhook-handler 128 | [node-github]: https://github.com/mikedeboer/node-github 129 | [github-app]: https://github.com/probot/github-app 130 | --------------------------------------------------------------------------------