Comparing version 0.9.1 to 0.10.0
{ | ||
"opts": { | ||
"readme": "docs/plugins.md", | ||
"template": "node_modules/minami" | ||
@@ -5,0 +4,0 @@ }, |
#!/usr/bin/env node | ||
require('dotenv').config(); | ||
require('dotenv').config() | ||
const pkgConf = require('pkg-conf'); | ||
const program = require('commander'); | ||
const pkgConf = require('pkg-conf') | ||
const program = require('commander') | ||
const {findPrivateKey} = require('../lib/private-key'); | ||
const {findPrivateKey} = require('../lib/private-key') | ||
program | ||
.usage('[options] <plugins...>') | ||
.usage('[options] <apps...>') | ||
.option('-a, --app <id>', 'ID of the GitHub App', process.env.APP_ID) | ||
@@ -16,12 +16,13 @@ .option('-s, --secret <secret>', 'Webhook secret of the GitHub App', process.env.WEBHOOK_SECRET) | ||
.option('-P, --private-key <file>', 'Path to certificate of the GitHub App', findPrivateKey) | ||
.option('-w, --webhook-path <path>', 'URL path which receives webhooks. Ex: `/webhook`', process.env.WEBHOOK_PATH) | ||
.option('-t, --tunnel <subdomain>', 'Expose your local bot to the internet', process.env.SUBDOMAIN || process.env.NODE_ENV !== 'production') | ||
.parse(process.argv); | ||
.parse(process.argv) | ||
if (!program.app) { | ||
console.warn('Missing GitHub App ID.\nUse --app flag or set APP_ID environment variable.'); | ||
program.help(); | ||
console.warn('Missing GitHub App ID.\nUse --app flag or set APP_ID environment variable.') | ||
program.help() | ||
} | ||
if (!program.privateKey) { | ||
program.privateKey = findPrivateKey(); | ||
program.privateKey = findPrivateKey() | ||
} | ||
@@ -31,14 +32,14 @@ | ||
try { | ||
const setupTunnel = require('../lib/tunnel'); | ||
const setupTunnel = require('../lib/tunnel') | ||
setupTunnel(program.tunnel, program.port).then(tunnel => { | ||
console.log('Listening on ' + tunnel.url); | ||
console.log('Listening on ' + tunnel.url) | ||
}).catch(err => { | ||
console.warn('Could not open tunnel: ', err.message); | ||
}); | ||
console.warn('Could not open tunnel: ', err.message) | ||
}) | ||
} catch (err) { | ||
console.warn('Run `npm install --save-dev localtunnel` to enable localtunnel.'); | ||
console.warn('Run `npm install --save-dev localtunnel` to enable localtunnel.') | ||
} | ||
} | ||
const createProbot = require('../'); | ||
const createProbot = require('../') | ||
@@ -49,16 +50,17 @@ const probot = createProbot({ | ||
cert: program.privateKey, | ||
port: program.port | ||
}); | ||
port: program.port, | ||
webhookPath: program.webhookPath | ||
}) | ||
pkgConf('probot').then(pkg => { | ||
const plugins = require('../lib/plugin')(probot); | ||
const requestedPlugins = program.args.concat(pkg.plugins || []); | ||
const plugins = require('../lib/plugin')(probot) | ||
const requestedPlugins = program.args.concat(pkg.plugins || []) | ||
// If we have explicitly requested plugins, load them; otherwise use autoloading | ||
if (requestedPlugins.length > 0) { | ||
plugins.load(requestedPlugins); | ||
plugins.load(requestedPlugins) | ||
} else { | ||
plugins.autoload(); | ||
plugins.autoload() | ||
} | ||
probot.start(); | ||
}); | ||
probot.start() | ||
}) |
#!/usr/bin/env node | ||
// Usage: bin/simulate issues path/to/payload plugin.js | ||
// Usage: bin/simulate issues path/to/payload app.js | ||
require('dotenv').config({silent: true}); | ||
require('dotenv').config({silent: true}) | ||
const path = require('path'); | ||
const program = require('commander'); | ||
const {findPrivateKey} = require('../lib/private-key'); | ||
const path = require('path') | ||
const program = require('commander') | ||
const {findPrivateKey} = require('../lib/private-key') | ||
program | ||
.usage('[options] <event-name> <path/to/payload.json> [path/to/plugin.js...]') | ||
.usage('[options] <event-name> <path/to/payload.json> [path/to/app.js...]') | ||
.option('-a, --app <id>', 'ID of the GitHub App', process.env.APP_ID) | ||
.option('-P, --private-key <file>', 'Path to certificate of the GitHub App', findPrivateKey) | ||
.parse(process.argv); | ||
.parse(process.argv) | ||
const eventName = program.args[0]; | ||
const payloadPath = program.args[1]; | ||
const eventName = program.args[0] | ||
const payloadPath = program.args[1] | ||
if (!eventName || !payloadPath) { | ||
program.help(); | ||
program.help() | ||
} | ||
const payload = require(path.join(process.cwd(), payloadPath)); | ||
const payload = require(path.join(process.cwd(), payloadPath)) | ||
const createProbot = require('../'); | ||
const createProbot = require('../') | ||
@@ -30,9 +30,9 @@ const probot = createProbot({ | ||
cert: findPrivateKey() | ||
}); | ||
}) | ||
const plugins = require('../lib/plugin')(probot); | ||
const plugins = require('../lib/plugin')(probot) | ||
plugins.load(program.args.slice(2)); | ||
plugins.load(program.args.slice(2)) | ||
probot.logger.debug('Simulating event', eventName); | ||
probot.receive({event: eventName, payload}); | ||
probot.logger.debug('Simulating event', eventName) | ||
probot.receive({event: eventName, payload}) |
#!/usr/bin/env node | ||
const semver = require('semver'); | ||
const version = require('../package').engines.node; | ||
const semver = require('semver') | ||
const version = require('../package').engines.node | ||
if (!semver.satisfies(process.version, version)) { | ||
console.log(`Node.js version ${version} is required. You have ${process.version}.`); | ||
process.exit(1); | ||
console.log(`Node.js version ${version} is required. You have ${process.version}.`) | ||
process.exit(1) | ||
} | ||
@@ -16,2 +16,2 @@ | ||
.command('simulate', 'simulate a webhook being delivered') | ||
.parse(process.argv); | ||
.parse(process.argv) |
# Changelog | ||
## 0.10.0 (2017-08-24) | ||
Enhancements: | ||
- Configure the path that the GitHub webhook handler listens on with `WEBHOOK_PATH` environment variable or the `--webhook-path` or `-w` command line options ([#203](https://github.com/probot/probot/pull/203)) | ||
- `robot.on` can now take an array of events ([#212](https://github.com/probot/probot/pull/212)): | ||
```js | ||
robot.on(['issues.opened', 'issues.closed'], async context => { | ||
robot.log(context); | ||
}); | ||
``` | ||
Breaking Changes: | ||
- Previously, `context.config(path, default)` would raise an error if the config file does not exist in the repository. Now, it will return the `default` from [`context.config`](https://probot.github.io/api/latest/Context.html#config) even if the file doesn't exist, or `null` if a default is not set. ([#198](https://github.com/probot/probot/pull/198), [#201](https://github.com/probot/probot/pull/201)) | ||
- Rename `SENTRY_URL` to `SENTRY_DSN`, because consistency ([#194](https://github.com/probot/probot/pull/194)) | ||
- The second event argument to `robot.on` that was deprecated in 0.7.0 was removed. ([e9355539](https://github.com/probot/probot/commit/e93555397e21d23b0ec2fc56816a5337c3cf7bc4)) | ||
- The `robot` property of the object returned from the Probot constructor (`const probot = require('probot')(); probot.robot`) was removed. ([dc33e4be](https://github.com/probot/probot/commit/dc33e4be6ffeafd36011ddefd5216bfe3bd508d3)) | ||
Noteworthy Community Updates: | ||
- There's a new [Slack channel for Probot](https://probot-slackin.herokuapp.com/). Come chat with us! ([#217](https://github.com/probot/probot/pull/216)) | ||
- [Standard](https://standardjs.com/) is the new lint sheriff in town and recommended for all Probot apps. ([#219](https://github.com/probot/probot/pull/219)) | ||
- Welcome to [@hiimbex](https://github.com/hiimbex), [@JasonEtco](https://github.com/JasonEtco), and [@anglinb](https://github.com/anglinb) as maintainers! | ||
- Update terminology to use "Probot app" instead of "Probot plugin", and "Probot extension" instead of "Probot helper" ([#210](https://github.com/probot/probot/pull/210)) | ||
[View full changelog](https://github.com/probot/probot/compare/v0.9.1...v0.10.0) | ||
## 0.9.1 (2017-08-09) | ||
@@ -4,0 +40,0 @@ |
@@ -1,4 +0,4 @@ | ||
# Best practices for Probot plugins | ||
Plu# Best Practices | ||
First and foremost, your plugin must obey the [The Three Laws of Robotics](https://en.wikipedia.org/wiki/Three_Laws_of_Robotics): | ||
First and foremost, your app must obey the [The Three Laws of Robotics](https://en.wikipedia.org/wiki/Three_Laws_of_Robotics): | ||
@@ -9,3 +9,3 @@ > 1. A robot may not injure a human being or, through inaction, allow a human being to come to harm. | ||
Now that we agree that nobody will get hurt, here are some tips to make your plugin more effective. | ||
Now that we agree that nobody will get hurt, here are some tips to make your app more effective. | ||
@@ -22,3 +22,3 @@ **Contents:** | ||
Think about how how people will experience the interactions with your plugin. | ||
Think about how how people will experience the interactions with your app. | ||
@@ -40,11 +40,11 @@ ### Avoid the uncanny valley | ||
Being installed on an account is sufficient permission for actions in response to a user action, like replying on a single issue. But a plugin _must_ have explicit permission before performing bulk actions, like labeling all open issues. | ||
Being installed on an account is sufficient permission for actions in response to a user action, like replying on a single issue. But a app _must_ have explicit permission before performing bulk actions, like labeling all open issues. | ||
For example, the [stale](https://github.com/probot/stale) plugin will only scan a repository for stale issues and pull requests if `.github/stale.yml` exists in the repository. | ||
For example, the [stale](https://github.com/probot/stale) app will only scan a repository for stale issues and pull requests if `.github/stale.yml` exists in the repository. | ||
### Include "dry run" functionality | ||
A dry run is when a plugin, instead of actually taking an action, only logs what actions it would have taken if it wasn't a dry run. A plugin _must_ offer a dry run feature if it does anything destructive and _should_ offer a dry run feature in all cases. | ||
A dry run is when a app, instead of actually taking an action, only logs what actions it would have taken if it wasn't a dry run. A app _must_ offer a dry run feature if it does anything destructive and _should_ offer a dry run feature in all cases. | ||
For example, the [stale](https://github.com/probot/stale) plugin will perform a dry run if there is no `.github/stale.yml` file in the repository. | ||
For example, the [stale](https://github.com/probot/stale) app will perform a dry run if there is no `.github/stale.yml` file in the repository. | ||
@@ -55,10 +55,10 @@ ## Configuration | ||
Plugins _should_ provide sensible defaults for all settings. | ||
Apps _should_ provide sensible defaults for all settings. | ||
### Provide full configuration | ||
Plugins _should_ allow all settings to customized for each installation. | ||
Apps _should_ allow all settings to customized for each installation. | ||
### Store configuration in the repository | ||
Any configuration _should_ be stored in the repository. Unless the plugin is using files from an established convention, the configuration _should_ be stored in the `.github` directory. See the [API docs for `context.config`](https://probot.github.io/probot/latest/Context.html#config). | ||
Any configuration _should_ be stored in the repository. Unless the app is using files from an established convention, the configuration _should_ be stored in the `.github` directory. See the [API docs for `context.config`](https://probot.github.io/probot/latest/Context.html#config). |
@@ -1,14 +0,18 @@ | ||
# Deploy | ||
--- | ||
next: docs/best-practices.md | ||
--- | ||
Every plugin can either be deployed as a stand-alone bot, or combined with other plugins in one deployment. | ||
# Deployment | ||
> **Heads up!** Note that most [plugins in the @probot organization](https://github.com/search?q=topic%3Aprobot-plugin+org%3Aprobot&type=Repositories) have an official hosted app that you can use for your open source project. Use the hosted instance if you don't want to deploy your own. | ||
Every app can either be deployed stand-alone, or combined with other apps in one deployment. | ||
> **Heads up!** Note that most [apps in the @probot organization](https://github.com/search?q=topic%3Aprobot-app+org%3Aprobot&type=Repositories) have an official hosted app that you can use for your open source project. Use the hosted instance if you don't want to deploy your own. | ||
**Contents:** | ||
1. [Create the GitHub App](#create-the-github-app) | ||
1. [Deploy the plugin](#deploy-the-plugin) | ||
1. [Deploy the app](#deploy-the-app) | ||
1. [Heroku](#heroku) | ||
1. [Now](#now) | ||
1. [Combining plugins](#combining-plugins) | ||
1. [Combining apps](#combining-apps) | ||
@@ -20,6 +24,6 @@ ## Create the GitHub App | ||
1. [Create a new GitHub App](https://github.com/settings/apps/new) with: | ||
- **Homepage URL**: the URL to the GitHub repository for your plugin | ||
- **Webhook URL**: Use `https://example.com/` for now, we'll come back in a minute to update this with the URL of your deployed plugin. | ||
- **Webhook Secret**: Generate a unique secret with `openssl rand -base64 32` and save it because you'll need it in a minute to configure your deployed plugin. | ||
- **Permissions & events**: See `docs/deploy.md` in the plugin for a list of the permissions and events that it needs access to. | ||
- **Homepage URL**: the URL to the GitHub repository for your app | ||
- **Webhook URL**: Use `https://example.com/` for now, we'll come back in a minute to update this with the URL of your deployed app. | ||
- **Webhook Secret**: Generate a unique secret with `openssl rand -base64 32` and save it because you'll need it in a minute to configure your deployed app. | ||
- **Permissions & events**: See `docs/deploy.md` in the app for a list of the permissions and events that it needs access to. | ||
@@ -30,5 +34,5 @@ 1. Download the private key from the app. | ||
## Deploy the plugin | ||
## Deploy the app | ||
To deploy a plugin to any cloud provider, you will need 3 environment variables: | ||
To deploy a app to any cloud provider, you will need 3 environment variables: | ||
@@ -51,3 +55,3 @@ - `APP_ID`: the ID of the app, which you can get from the [app settings page](https://github.com/settings/apps). | ||
1. Clone the plugin that you want to deploy. e.g. `git clone https://github.com/probot/stale` | ||
1. Clone the app that you want to deploy. e.g. `git clone https://github.com/probot/stale` | ||
@@ -69,3 +73,3 @@ 1. Create the Heroku app with the `heroku create` command: | ||
1. Deploy the plugin to heroku with `git push`: | ||
1. Deploy the app to heroku with `git push`: | ||
@@ -79,15 +83,15 @@ $ git push heroku master | ||
1. Your plugin should be up and running! To verify that your plugin | ||
1. Your app should be up and running! To verify that your app | ||
is receiving webhook data, you can tail your app's logs: | ||
$ heroku config:set LOG_LEVEL=trace | ||
$ heroku logs --tail | ||
$ heroku config:set LOG_LEVEL=trace | ||
$ heroku logs --tail | ||
### Now | ||
Zeit [Now](http://zeit.co/now) is a great service for running Probot plugins. After [creating the GitHub App](#create-the-github-app): | ||
Zeit [Now](http://zeit.co/now) is a great service for running Probot apps. After [creating the GitHub App](#create-the-github-app): | ||
1. Install the now CLI with `npm i -g now` | ||
1. Clone the plugin that you want to deploy. e.g. `git clone https://github.com/probot/stale` | ||
1. Clone the app that you want to deploy. e.g. `git clone https://github.com/probot/stale` | ||
@@ -102,7 +106,7 @@ 1. Run `now` to deploy, replacing the `APP_ID` and `WEBHOOK_SECRET` with the values for those variables, and setting the path for the `PRIVATE_KEY`: | ||
Your plugin should be up and running! | ||
Your app should be up and running! | ||
## Combining plugins | ||
## Combining apps | ||
To deploy a bot that includes multiple plugins, create a new app that has the plugins listed as dependencies in `package.json`: | ||
To deploy a bot that includes multiple apps, create a new app that has the apps listed as dependencies in `package.json`: | ||
@@ -128,1 +132,9 @@ ```json | ||
``` | ||
## Error tracking | ||
Probot comes bundled with a client for the [Sentry](https://github.com/getsentry/sentry) exception tracking platform. To enable Sentry: | ||
1. [Create a Sentry.io Account](https://sentry.io/signup/) (with [10k events/month free](https://sentry.io/pricing/)) or [host your own instance](https://github.com/getsentry/sentry) (Students can get [extra Sentry credit](https://education.github.com/pack)) | ||
2. Follow the setup instructions to find your DSN. | ||
3. Set the `SENTRY_DSN` environment variable with the DSN you retrieved. |
@@ -1,13 +0,67 @@ | ||
# Development | ||
--- | ||
next: docs/webhooks.md | ||
--- | ||
To run a plugin locally, you'll need to create a GitHub App and configure it to deliver webhooks to your local machine. | ||
# Developing an App | ||
1. Make sure you have a recent version of [Node.js](https://nodejs.org/) installed | ||
To develop a Probot app, you will first need a recent version of [Node.js](https://nodejs.org/) installed. Probot uses the `async/await` keywords, so Node.js 7.6 is the minimum required version. | ||
## Generating a new app | ||
[create-probot-app](https://github.com/probot/create-probot-app) is the best way to start building a new app. It will generate a new app with everything you need to get started and run your app in production. | ||
To get started, install the module from npm: | ||
``` | ||
$ npm install -g create-probot-app | ||
``` | ||
Next, run the app: | ||
``` | ||
$ create-probot-app my-first-app | ||
``` | ||
This will ask you a series of questions about your app, which should look something like this: | ||
``` | ||
Let's create a Probot app! | ||
? Package name: my-first-app | ||
? Description of app: A "Hello World" GitHub App built with Probot | ||
? Author's full name: Brandon Keepers | ||
? Author's email address: bkeepers@github.com | ||
? Homepage: | ||
? GitHub user or org name: bkeepers | ||
? Repository name: my-first-app | ||
created file: my-first-app/.env.example | ||
created file: my-first-app/.gitignore | ||
created file: my-first-app/.travis.yml | ||
created file: my-first-app/LICENSE | ||
created file: my-first-app/README.md | ||
created file: my-first-app/app.json | ||
created file: my-first-app/index.js | ||
created file: my-first-app/package-lock.json | ||
created file: my-first-app/package.json | ||
created file: my-first-app/docs/deploy.md | ||
Finished scaffolding files! | ||
Installing Node dependencies! | ||
Done! Enjoy building your Probot app! | ||
``` | ||
The most important files note here are `index.js`, which is where the code for your app will go, and `package.json`, which makes this a standard [npm module](https://docs.npmjs.com/files/package.json). | ||
## Configure a GitHub App | ||
To run your app in development, you will need to configure a GitHub App to deliver webhooks to your local machine. | ||
1. [Create a new GitHub App](https://github.com/settings/apps/new) with: | ||
- **Webhook URL**: Set to `https://example.com/` and we'll update it in a minute. | ||
- **Webhook Secret:** `development` | ||
- **Permissions & events** needed will depend on how you use the bot, but for development it may be easiest to enable everything. | ||
1. Download the private key and move it to the project directory | ||
1. Edit `.env` and set `APP_ID` to the ID of the app you just created. | ||
1. Run `$ npm start` to start the server, which will output `Listening on https://yourname.localtunnel.me`; | ||
- **Permissions & events** is located lower down the page and will depend on what data you want your app to have access to. Note: if, for example, you only enable issue events, you will not be able to listen on pull request webhooks with your app. However, for development we recommend enabling everything. | ||
1. Download the private key and move it to your project's directory. | ||
1. Edit `.env` and set `APP_ID` to the ID of the app you just created. The App ID can be found in your app settings page here <img width="1048" alt="screen shot 2017-08-20 at 8 31 31 am" src="https://user-images.githubusercontent.com/13410355/29496168-044b9a48-8582-11e7-8be4-39cc75090647.png"> | ||
1. Run `$ npm start` to start the server, which will output `Listening on https://yourname.localtunnel.me`. | ||
1. Update the **Webhook URL** in the [app settings](https://github.com/settings/apps) to use the `localtunnel.me` URL. | ||
@@ -17,9 +71,20 @@ | ||
Whenever you come back to work on the app after you've already had it running once, you should only need to run `$ npm start`. | ||
## Running the app | ||
Optionally, you can also run your plugin through [nodemon](https://github.com/remy/nodemon#nodemon) which will listen on any files changes in your local development environment and automatically restart the server. After installing nodemon, you can run `nodemon --exec "npm start"` and from there the server will automatically restart upon file changes. | ||
Once you've set the `APP_ID` of your GitHub app in `.env` and downloaded the private key, you're ready to run your app. | ||
``` | ||
$ npm start | ||
> probot run ./index.js | ||
Yay, the plugin was loaded! | ||
18:11:55.838Z DEBUG PRobot: Loaded plugin: ./index.js | ||
Listening on https://bkeepers.localtunnel.me | ||
``` | ||
Optionally, you can also run your app through [nodemon](https://github.com/remy/nodemon#nodemon) which will listen on any files changes in your local development environment and automatically restart the server. After installing nodemon, you can run `nodemon --exec "npm start"` and from there the server will automatically restart upon file changes. | ||
## Debugging | ||
1. Always run `$ npm install` and restart the server if package.json has changed. | ||
1. Always run `$ npm install` and restart the server if `package.json` has changed. | ||
1. To turn on verbose logging, start server by running: `$ LOG_LEVEL=trace npm start` |
104
index.js
@@ -1,11 +0,12 @@ | ||
const bunyan = require('bunyan'); | ||
const bunyanFormat = require('bunyan-format'); | ||
const sentryStream = require('bunyan-sentry-stream'); | ||
const cacheManager = require('cache-manager'); | ||
const createApp = require('github-app'); | ||
const createWebhook = require('github-webhook-handler'); | ||
const Raven = require('raven'); | ||
const bunyan = require('bunyan') | ||
const bunyanFormat = require('bunyan-format') | ||
const sentryStream = require('bunyan-sentry-stream') | ||
const cacheManager = require('cache-manager') | ||
const createApp = require('github-app') | ||
const createWebhook = require('github-webhook-handler') | ||
const Raven = require('raven') | ||
const createRobot = require('./lib/robot'); | ||
const createServer = require('./lib/server'); | ||
const createRobot = require('./lib/robot') | ||
const createServer = require('./lib/server') | ||
const serializers = require('./lib/serializers') | ||
@@ -16,3 +17,3 @@ module.exports = (options = {}) => { | ||
ttl: 60 * 60 // 1 hour | ||
}); | ||
}) | ||
@@ -23,15 +24,6 @@ const logger = bunyan.createLogger({ | ||
stream: bunyanFormat({outputMode: process.env.LOG_FORMAT || 'short'}), | ||
serializers: { | ||
repository: repository => repository.full_name, | ||
event: ({id, event, payload}) => { | ||
return { | ||
id, event, | ||
action: payload.action, | ||
repository: payload.repository && payload.repository.full_name | ||
}; | ||
} | ||
} | ||
}); | ||
serializers | ||
}) | ||
const webhook = createWebhook({path: '/', secret: options.secret || 'development'}); | ||
const webhook = createWebhook({path: options.webhookPath || '/', secret: options.secret || 'development'}) | ||
const app = createApp({ | ||
@@ -41,31 +33,37 @@ id: options.id, | ||
debug: process.env.LOG_LEVEL === 'trace' | ||
}); | ||
const server = createServer(webhook); | ||
}) | ||
const server = createServer(webhook) | ||
// Log all received webhooks | ||
webhook.on('*', event => { | ||
logger.trace(event, 'webhook received'); | ||
receive(event); | ||
}); | ||
logger.trace(event, 'webhook received') | ||
receive(event) | ||
}) | ||
// Log all webhook errors | ||
webhook.on('error', logger.error.bind(logger)); | ||
webhook.on('error', logger.error.bind(logger)) | ||
// Log all unhandled rejections | ||
process.on('unhandledRejection', logger.error.bind(logger)); | ||
process.on('unhandledRejection', logger.error.bind(logger)) | ||
// Deprecate SENTRY_URL | ||
if (process.env.SENTRY_URL && !process.env.SENTRY_DSN) { | ||
process.env.SENTRY_DSN = process.env.SENTRY_URL | ||
console.warn('DEPRECATED: the `SENTRY_URL` key is now called `SENTRY_DSN`. Use of `SENTRY_URL` is deprecated and will be removed in 0.11.0') | ||
} | ||
// If sentry is configured, report all logged errors | ||
if (process.env.SENTRY_URL) { | ||
Raven.disableConsoleAlerts(); | ||
Raven.config(process.env.SENTRY_URL, { | ||
if (process.env.SENTRY_DSN) { | ||
Raven.disableConsoleAlerts() | ||
Raven.config(process.env.SENTRY_DSN, { | ||
autoBreadcrumbs: true | ||
}).install({}); | ||
}).install({}) | ||
logger.addStream(sentryStream(Raven)); | ||
logger.addStream(sentryStream(Raven)) | ||
} | ||
const robots = []; | ||
const robots = [] | ||
function receive(event) { | ||
return Promise.all(robots.map(robot => robot.receive(event))); | ||
function receive (event) { | ||
return Promise.all(robots.map(robot => robot.receive(event))) | ||
} | ||
@@ -79,30 +77,22 @@ | ||
// Return the first robot | ||
get robot() { | ||
const caller = (new Error()).stack.split('\n')[2]; | ||
console.warn('DEPRECATED: the `robot` property is deprecated and will be removed in 0.10.0'); | ||
console.warn(caller); | ||
return robots[0] || createRobot({app, cache, logger, catchErrors: true}); | ||
start () { | ||
server.listen(options.port) | ||
logger.trace('Listening on http://localhost:' + options.port) | ||
}, | ||
start() { | ||
server.listen(options.port); | ||
logger.trace('Listening on http://localhost:' + options.port); | ||
}, | ||
load (plugin) { | ||
const robot = createRobot({app, cache, logger, catchErrors: true}) | ||
load(plugin) { | ||
const robot = createRobot({app, cache, logger, catchErrors: true}); | ||
// Connect the router from the robot to the server | ||
server.use(robot.router); | ||
server.use(robot.router) | ||
// Initialize the plugin | ||
plugin(robot); | ||
robots.push(robot); | ||
plugin(robot) | ||
robots.push(robot) | ||
return robot; | ||
return robot | ||
} | ||
}; | ||
}; | ||
} | ||
} | ||
module.exports.createRobot = createRobot; | ||
module.exports.createRobot = createRobot |
@@ -1,3 +0,3 @@ | ||
const path = require('path'); | ||
const yaml = require('js-yaml'); | ||
const path = require('path') | ||
const yaml = require('js-yaml') | ||
@@ -12,5 +12,5 @@ /** | ||
class Context { | ||
constructor(event, github) { | ||
Object.assign(this, event); | ||
this.github = github; | ||
constructor (event, github) { | ||
Object.assign(this, event) | ||
this.github = github | ||
} | ||
@@ -30,4 +30,4 @@ | ||
*/ | ||
repo(object) { | ||
const repo = this.payload.repository; | ||
repo (object) { | ||
const repo = this.payload.repository | ||
@@ -37,3 +37,3 @@ return Object.assign({ | ||
repo: repo.name | ||
}, object); | ||
}, object) | ||
} | ||
@@ -53,7 +53,7 @@ | ||
*/ | ||
issue(object) { | ||
const payload = this.payload; | ||
issue (object) { | ||
const payload = this.payload | ||
return Object.assign({ | ||
number: (payload.issue || payload.pull_request || payload).number | ||
}, this.repo(), object); | ||
}, this.repo(), object) | ||
} | ||
@@ -65,11 +65,11 @@ | ||
*/ | ||
get isBot() { | ||
return this.payload.sender.type === 'Bot'; | ||
get isBot () { | ||
return this.payload.sender.type === 'Bot' | ||
} | ||
/** | ||
* Reads the plugin configuration from the given YAML file in the `.github` | ||
* Reads the app configuration from the given YAML file in the `.github` | ||
* directory of the repository. | ||
* | ||
* @example <caption>Contents of <code>.github/myplugin.yml</code>.</caption> | ||
* @example <caption>Contents of <code>.github/myapp.yml</code>.</caption> | ||
* | ||
@@ -79,6 +79,6 @@ * close: true | ||
* | ||
* @example <caption>Plugin that reads from <code>.github/myplugin.yml</code>.</caption> | ||
* @example <caption>App that reads from <code>.github/myapp.yml</code>.</caption> | ||
* | ||
* // Load config from .github/myplugin.yml in the repository | ||
* const config = await context.config('myplugin.yml'); | ||
* // Load config from .github/myapp.yml in the repository | ||
* const config = await context.config('myapp.yml'); | ||
* | ||
@@ -94,10 +94,22 @@ * if(config.close) { | ||
*/ | ||
async config(fileName, defaultConfig = {}) { | ||
const params = this.repo({path: path.join('.github', fileName)}); | ||
const res = await this.github.repos.getContent(params); | ||
const config = yaml.safeLoad(Buffer.from(res.data.content, 'base64').toString()) || {}; | ||
return Object.assign({}, defaultConfig, config); | ||
async config (fileName, defaultConfig) { | ||
const params = this.repo({path: path.join('.github', fileName)}) | ||
try { | ||
const res = await this.github.repos.getContent(params) | ||
const config = yaml.safeLoad(Buffer.from(res.data.content, 'base64').toString()) || {} | ||
return Object.assign({}, defaultConfig, config) | ||
} catch (err) { | ||
if (err.code === 404) { | ||
if (defaultConfig) { | ||
return defaultConfig | ||
} | ||
return null | ||
} else { | ||
throw err | ||
} | ||
} | ||
} | ||
} | ||
module.exports = Context; | ||
module.exports = Context |
/* eslint-disable no-await-in-loop */ | ||
// Default callback should just return the response passed to it. | ||
const defaultCallback = response => response; | ||
const defaultCallback = response => response | ||
module.exports = async function (responsePromise, callback = defaultCallback) { | ||
let collection = []; | ||
let response = await responsePromise; | ||
let collection = [] | ||
let response = await responsePromise | ||
collection = collection.concat(await callback(response)); | ||
collection = collection.concat(await callback(response)) | ||
while (this.hasNextPage(response)) { | ||
response = await this.getNextPage(response); | ||
collection = collection.concat(await callback(response)); | ||
response = await this.getNextPage(response) | ||
collection = collection.concat(await callback(response)) | ||
} | ||
return collection; | ||
}; | ||
return collection | ||
} |
@@ -1,13 +0,13 @@ | ||
module.exports = pluginLoaderFactory; | ||
module.exports = pluginLoaderFactory | ||
function pluginLoaderFactory(probot, opts = {}) { | ||
function pluginLoaderFactory (probot, opts = {}) { | ||
if (!probot) { | ||
throw new TypeError('expected probot instance'); | ||
throw new TypeError('expected probot instance') | ||
} | ||
// We could eventually support a different base dir to load plugins from. | ||
const basedir = opts.basedir || process.cwd(); | ||
const basedir = opts.basedir || process.cwd() | ||
// These are mostly to ease testing | ||
const autoloader = opts.autoloader || require('load-plugins'); | ||
const resolver = opts.resolver || require('resolve').sync; | ||
const autoloader = opts.autoloader || require('load-plugins') | ||
const resolver = opts.resolver || require('resolve').sync | ||
@@ -18,10 +18,10 @@ /** | ||
*/ | ||
function resolvePlugin(pluginName) { | ||
function resolvePlugin (pluginName) { | ||
try { | ||
return resolver(pluginName, {basedir}); | ||
return resolver(pluginName, {basedir}) | ||
} catch (err) { | ||
err.message = `Failed to resolve plugin "${pluginName}". Is it installed? | ||
err.message = `Failed to resolve "${pluginName}". Is it installed? | ||
Original error message: | ||
${err.message}`; | ||
throw err; | ||
${err.message}` | ||
throw err | ||
} | ||
@@ -35,10 +35,9 @@ } | ||
*/ | ||
function loadPlugin(pluginName, plugin) { | ||
function loadPlugin (pluginName, plugin) { | ||
try { | ||
probot.load(typeof plugin === 'string' ? require(plugin) : plugin); | ||
probot.load(typeof plugin === 'string' ? require(plugin) : plugin) | ||
} catch (err) { | ||
err.message = `Failed to load plugin "${pluginName}". This is a problem with the plugin itself; not probot. | ||
Original error message: | ||
${err.message}`; | ||
throw err; | ||
err.message = `Failed to load "${pluginName}" because of the following error: | ||
${err.message}` | ||
throw err | ||
} | ||
@@ -50,8 +49,8 @@ } | ||
*/ | ||
function autoload() { | ||
const plugins = autoloader('probot-*'); | ||
function autoload () { | ||
const plugins = autoloader('probot-*') | ||
Object.keys(plugins).forEach(pluginName => { | ||
loadPlugin(pluginName, plugins[pluginName]); | ||
probot.logger.info(`Automatically loaded plugin: ${pluginName}`); | ||
}); | ||
loadPlugin(pluginName, plugins[pluginName]) | ||
probot.logger.info(`Automatically loaded ${pluginName}`) | ||
}) | ||
} | ||
@@ -63,11 +62,11 @@ | ||
*/ | ||
function load(pluginNames = []) { | ||
function load (pluginNames = []) { | ||
pluginNames.forEach(pluginName => { | ||
const pluginPath = resolvePlugin(pluginName); | ||
loadPlugin(pluginName, pluginPath); | ||
probot.logger.debug(`Loaded plugin: ${pluginName}`); | ||
}); | ||
const pluginPath = resolvePlugin(pluginName) | ||
loadPlugin(pluginName, pluginPath) | ||
probot.logger.debug(`Loaded ${pluginName}`) | ||
}) | ||
} | ||
return {load, autoload}; | ||
return {load, autoload} | ||
} |
@@ -1,2 +0,2 @@ | ||
const fs = require('fs'); | ||
const fs = require('fs') | ||
@@ -14,16 +14,16 @@ /** | ||
*/ | ||
function findPrivateKey(filepath) { | ||
function findPrivateKey (filepath) { | ||
if (filepath) { | ||
return fs.readFileSync(filepath); | ||
return fs.readFileSync(filepath) | ||
} | ||
if (process.env.PRIVATE_KEY) { | ||
return process.env.PRIVATE_KEY.replace(/\\n/g, '\n'); | ||
return process.env.PRIVATE_KEY.replace(/\\n/g, '\n') | ||
} | ||
if (process.env.PRIVATE_KEY_PATH) { | ||
return fs.readFileSync(process.env.PRIVATE_KEY_PATH); | ||
return fs.readFileSync(process.env.PRIVATE_KEY_PATH) | ||
} | ||
const foundPath = fs.readdirSync(process.cwd()) | ||
.find(path => path.endsWith('.pem')); | ||
.find(path => path.endsWith('.pem')) | ||
if (foundPath) { | ||
return findPrivateKey(foundPath); | ||
return findPrivateKey(foundPath) | ||
} | ||
@@ -34,3 +34,3 @@ throw new Error(`Missing private key for GitHub App. Please use: | ||
* \`PRIVATE_KEY_PATH\` environment variable | ||
`); | ||
`) | ||
} | ||
@@ -40,2 +40,2 @@ | ||
findPrivateKey | ||
}; | ||
} |
120
lib/robot.js
@@ -1,9 +0,9 @@ | ||
const {EventEmitter} = require('promise-events'); | ||
const GitHubApi = require('github'); | ||
const Bottleneck = require('bottleneck'); | ||
const express = require('express'); | ||
const Context = require('./context'); | ||
const {EventEmitter} = require('promise-events') | ||
const GitHubApi = require('github') | ||
const Bottleneck = require('bottleneck') | ||
const express = require('express') | ||
const Context = require('./context') | ||
/** | ||
* The `robot` parameter available to plugins | ||
* The `robot` parameter available to apps | ||
* | ||
@@ -13,15 +13,15 @@ * @property {logger} log - A logger | ||
class Robot { | ||
constructor({app, cache, logger, router, catchErrors} = {}) { | ||
this.events = new EventEmitter(); | ||
this.app = app; | ||
this.cache = cache; | ||
this.router = router || new express.Router(); | ||
this.log = wrapLogger(logger); | ||
this.catchErrors = catchErrors; | ||
constructor ({app, cache, logger, router, catchErrors} = {}) { | ||
this.events = new EventEmitter() | ||
this.app = app | ||
this.cache = cache | ||
this.router = router || new express.Router() | ||
this.log = wrapLogger(logger) | ||
this.catchErrors = catchErrors | ||
} | ||
async receive(event) { | ||
async receive (event) { | ||
return this.events.emit('*', event).then(() => { | ||
return this.events.emit(event.event, event); | ||
}); | ||
return this.events.emit(event.event, event) | ||
}) | ||
} | ||
@@ -36,3 +36,3 @@ | ||
* // Get an express router to expose new HTTP endpoints | ||
* const app = robot.route('/my-plugin'); | ||
* const app = robot.route('/my-app'); | ||
* | ||
@@ -51,9 +51,9 @@ * // Use any middleware | ||
*/ | ||
route(path) { | ||
route (path) { | ||
if (path) { | ||
const router = new express.Router(); | ||
this.router.use(path, router); | ||
return router; | ||
const router = new express.Router() | ||
this.router.use(path, router) | ||
return router | ||
} else { | ||
return this.router; | ||
return this.router | ||
} | ||
@@ -89,10 +89,9 @@ } | ||
*/ | ||
on(event, callback) { | ||
if (callback.length === 2) { | ||
const caller = (new Error()).stack.split('\n')[2]; | ||
console.warn('DEPRECATED: Event callbacks now only take a single `context` argument.'); | ||
console.warn(caller); | ||
on (event, callback) { | ||
if (event.constructor === Array) { | ||
event.forEach(e => this.on(e, callback)) | ||
return | ||
} | ||
const [name, action] = event.split('.'); | ||
const [name, action] = event.split('.') | ||
@@ -102,13 +101,13 @@ return this.events.on(name, async event => { | ||
try { | ||
const github = await this.auth(event.payload.installation.id); | ||
const context = new Context(event, github); | ||
await callback(context, context /* DEPRECATED: for backward compat */); | ||
const github = await this.auth(event.payload.installation.id) | ||
const context = new Context(event, github) | ||
await callback(context) | ||
} catch (err) { | ||
this.log.error({err, event}); | ||
this.log.error({err, event}) | ||
if (!this.catchErrors) { | ||
throw err; | ||
throw err | ||
} | ||
} | ||
} | ||
}); | ||
}) | ||
} | ||
@@ -142,27 +141,27 @@ | ||
*/ | ||
async auth(id) { | ||
let github; | ||
async auth (id) { | ||
let github | ||
if (id) { | ||
const res = await this.cache.wrap(`app:${id}:token`, () => { | ||
this.log.trace(`creating token for installation ${id}`); | ||
return this.app.createToken(id); | ||
}, {ttl: 60 * 59}); // Cache for 1 minute less than GitHub expiry | ||
this.log.trace(`creating token for installation ${id}`) | ||
return this.app.createToken(id) | ||
}, {ttl: 60 * 59}) // Cache for 1 minute less than GitHub expiry | ||
github = new GitHubApi({debug: process.env.LOG_LEVEL === 'trace'}); | ||
github.authenticate({type: 'token', token: res.data.token}); | ||
github = new GitHubApi({debug: process.env.LOG_LEVEL === 'trace'}) | ||
github.authenticate({type: 'token', token: res.data.token}) | ||
} else { | ||
github = await this.app.asApp(); | ||
github = await this.app.asApp() | ||
} | ||
return probotEnhancedClient(github); | ||
return probotEnhancedClient(github) | ||
} | ||
} | ||
function probotEnhancedClient(github) { | ||
github = rateLimitedClient(github); | ||
function probotEnhancedClient (github) { | ||
github = rateLimitedClient(github) | ||
github.paginate = require('./paginate'); | ||
github.paginate = require('./paginate') | ||
return github; | ||
return github | ||
} | ||
@@ -172,9 +171,9 @@ | ||
// https://github.com/mikedeboer/node-github/issues/526 | ||
function rateLimitedClient(github) { | ||
const limiter = new Bottleneck(1, 1000); | ||
const oldHandler = github.handler; | ||
function rateLimitedClient (github) { | ||
const limiter = new Bottleneck(1, 1000) | ||
const oldHandler = github.handler | ||
github.handler = (msg, block, callback) => { | ||
limiter.submit(oldHandler.bind(github), msg, block, callback); | ||
}; | ||
return github; | ||
limiter.submit(oldHandler.bind(github), msg, block, callback) | ||
} | ||
return github | ||
} | ||
@@ -188,3 +187,3 @@ | ||
// | ||
function wrapLogger(logger) { | ||
function wrapLogger (logger) { | ||
const fn = logger ? logger.debug.bind(logger) : function () { }; | ||
@@ -194,9 +193,9 @@ | ||
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach(level => { | ||
fn[level] = logger ? logger[level].bind(logger) : function () { }; | ||
}); | ||
fn[level] = logger ? logger[level].bind(logger) : function () { } | ||
}) | ||
return fn; | ||
return fn | ||
} | ||
module.exports = (...args) => new Robot(...args); | ||
module.exports = (...args) => new Robot(...args) | ||
@@ -253,2 +252,7 @@ /** | ||
* robot.log.fatal("Goodbye, cruel world!"); | ||
* | ||
* Note: Probot supports execption tracking through raven, a client for | ||
* [sentry](https://github.com/getsentry/sentry). If the `SENTRY_DSN` is set | ||
* as an environment variable, all errors will be forwarded to the sentry host | ||
* specified by the environment variable. | ||
*/ |
@@ -1,10 +0,10 @@ | ||
const express = require('express'); | ||
const express = require('express') | ||
module.exports = function (webhook) { | ||
const app = express(); | ||
const app = express() | ||
app.use(webhook); | ||
app.get('/ping', (req, res) => res.end('PONG')); | ||
app.use(webhook) | ||
app.get('/ping', (req, res) => res.end('PONG')) | ||
return app; | ||
}; | ||
return app | ||
} |
@@ -1,8 +0,8 @@ | ||
const https = require('https'); | ||
const https = require('https') | ||
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved | ||
const localtunnel = require('localtunnel'); | ||
const localtunnel = require('localtunnel') | ||
module.exports = function setupTunnel(subdomain, port, retries = 0) { | ||
module.exports = function setupTunnel (subdomain, port, retries = 0) { | ||
if (typeof subdomain !== 'string') { | ||
subdomain = require('os').userInfo().username; | ||
subdomain = require('os').userInfo().username | ||
} | ||
@@ -13,16 +13,16 @@ | ||
if (err) { | ||
reject(err); | ||
reject(err) | ||
} else { | ||
testTunnel(subdomain).then(() => resolve(tunnel)).catch(() => { | ||
if (retries < 3) { | ||
console.warn(`Failed to connect to localtunnel.me. Trying again (tries: ${retries + 1})`); | ||
resolve(setupTunnel(subdomain, port, retries + 1)); | ||
console.warn(`Failed to connect to localtunnel.me. Trying again (tries: ${retries + 1})`) | ||
resolve(setupTunnel(subdomain, port, retries + 1)) | ||
} else { | ||
reject(new Error('Failed to connect to localtunnel.me. Giving up.')); | ||
reject(new Error('Failed to connect to localtunnel.me. Giving up.')) | ||
} | ||
}); | ||
}) | ||
} | ||
}); | ||
}); | ||
}; | ||
}) | ||
}) | ||
} | ||
@@ -32,3 +32,3 @@ // When a tunnel is closed and then immediately reopened (e.g. restarting the | ||
// requests through. This test that the tunnel returns 200 for /ping. | ||
function testTunnel(subdomain) { | ||
function testTunnel (subdomain) { | ||
const options = { | ||
@@ -39,9 +39,9 @@ host: `${subdomain}.localtunnel.me`, | ||
method: 'GET' | ||
}; | ||
} | ||
return new Promise((resolve, reject) => { | ||
https.request(options, res => { | ||
return res.statusCode === 200 ? resolve() : reject(); | ||
}).end(); | ||
}); | ||
return res.statusCode === 200 ? resolve() : reject(new Error('localtunnel failed to connect')) | ||
}).end() | ||
}) | ||
} |
{ | ||
"name": "probot", | ||
"version": "0.9.1", | ||
"version": "0.10.0", | ||
"description": "a trainable robot that responds to activity on GitHub", | ||
@@ -12,3 +12,3 @@ "repository": "https://github.com/probot/probot", | ||
"start": "node ./bin/probot run", | ||
"test": "mocha && xo --extend eslint-config-probot", | ||
"test": "mocha && standard", | ||
"doc": "jsdoc -c .jsdoc.json", | ||
@@ -21,39 +21,32 @@ "postpublish": "script/publish-docs" | ||
"bottleneck": "^1.16.0", | ||
"bunyan": "^1.8.5", | ||
"bunyan": "^1.8.12", | ||
"bunyan-format": "^0.2.1", | ||
"bunyan-sentry-stream": "^1.1.0", | ||
"cache-manager": "^2.4.0", | ||
"commander": "^2.10.0", | ||
"commander": "^2.11.0", | ||
"dotenv": "~4.0.0", | ||
"express": "^4.15.3", | ||
"github": "^9.2.0", | ||
"express": "^4.15.4", | ||
"github": "^9.3.1", | ||
"github-app": "^3.0.0", | ||
"github-webhook-handler": "^0.6.0", | ||
"js-yaml": "^3.8.4", | ||
"github-webhook-handler": "github:rvagg/github-webhook-handler#v0.6.1", | ||
"js-yaml": "^3.9.1", | ||
"load-plugins": "^2.1.2", | ||
"pkg-conf": "^2.0.0", | ||
"promise-events": "^0.1.3", | ||
"raven": "^2.1.0", | ||
"resolve": "^1.3.2", | ||
"semver": "^5.3.0" | ||
"raven": "^2.1.2", | ||
"resolve": "^1.4.0", | ||
"semver": "^5.4.1" | ||
}, | ||
"devDependencies": { | ||
"eslint-config-probot": "^0.1.0", | ||
"expect": "^1.20.2", | ||
"jsdoc": "^3.4.3", | ||
"jsdoc": "^3.5.4", | ||
"jsdoc-strip-async-await": "^0.1.0", | ||
"minami": "^1.1.1", | ||
"mocha": "^3.0.2", | ||
"supertest": "^3.0.0", | ||
"xo": "^0.19.0" | ||
"mocha": "^3.5.0", | ||
"standard": "^10.0.3", | ||
"supertest": "^3.0.0" | ||
}, | ||
"xo": { | ||
"overrides": [ | ||
{ | ||
"files": "test/**/*.js", | ||
"rules": { | ||
"max-nested-callbacks": "off", | ||
"prefer-arrow-callback": "off" | ||
} | ||
} | ||
"standard": { | ||
"env": [ | ||
"mocha" | ||
] | ||
@@ -60,0 +53,0 @@ }, |
@@ -9,14 +9,16 @@ # Probot | ||
## Plugins | ||
## Apps | ||
Bots are implemented as plugins, which are easy to write, deploy, and share. Here are just a few examples of things probot can do: | ||
Apps are easy to write, deploy, and share. Here are just a few examples of things that can be built with Probot: | ||
- [stale](https://github.com/probot/stale) - closes abandoned issues after a period of inactivity. | ||
- [owners](https://github.com/probot/owners) - @mentions people in Pull Requests based on contents of the OWNERS file | ||
- [configurer](https://github.com/probot/configurer) - syncs repository settings defined in `.github/config.yml` to GitHub, enabling Pull Requests for repository settings. | ||
- [settings](https://github.com/probot/settings) - syncs repository settings defined in `.github/settings.yml` to GitHub, enabling Pull Requests for repository settings. | ||
Check out [all probot plugins](https://github.com/search?q=topic%3Aprobot-plugin&type=Repositories). | ||
Check out [all Probot apps](https://github.com/search?q=topic%3Aprobot-app&type=Repositories). | ||
## Contributing | ||
Most of the interesting things are built with plugins, so consider starting by [writing a new plugin](docs/plugins.md) or improving one of the [existing ones](https://github.com/search?q=topic%3Aprobot-plugin&type=Repositories). | ||
Most of the interesting things are built with plugins, so consider starting by [writing a new app](docs/) or improving one of the [existing ones](https://github.com/search?q=topic%3Aprobot-app&type=Repositories). | ||
Want to chat with Probot users and contributors? [Join us in Slack](https://probot-slackin.herokuapp.com/)! |
@@ -1,9 +0,9 @@ | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const expect = require('expect'); | ||
const Context = require('../lib/context'); | ||
const fs = require('fs') | ||
const path = require('path') | ||
const expect = require('expect') | ||
const Context = require('../lib/context') | ||
describe('Context', function () { | ||
let event; | ||
let context; | ||
let event | ||
let context | ||
@@ -20,26 +20,26 @@ beforeEach(function () { | ||
} | ||
}; | ||
context = new Context(event); | ||
}); | ||
} | ||
context = new Context(event) | ||
}) | ||
it('inherits the payload', () => { | ||
expect(context.payload).toBe(event.payload); | ||
}); | ||
expect(context.payload).toBe(event.payload) | ||
}) | ||
describe('repo', function () { | ||
it('returns attributes from repository payload', function () { | ||
expect(context.repo()).toEqual({owner: 'bkeepers', repo:'probot'}); | ||
}); | ||
expect(context.repo()).toEqual({owner: 'bkeepers', repo: 'probot'}) | ||
}) | ||
it('merges attributes', function () { | ||
expect(context.repo({foo: 1, bar: 2})).toEqual({ | ||
owner: 'bkeepers', repo:'probot', foo: 1, bar: 2 | ||
}); | ||
}); | ||
owner: 'bkeepers', repo: 'probot', foo: 1, bar: 2 | ||
}) | ||
}) | ||
it('overrides repo attributes', function () { | ||
expect(context.repo({owner: 'muahaha'})).toEqual({ | ||
owner: 'muahaha', repo:'probot' | ||
}); | ||
}); | ||
owner: 'muahaha', repo: 'probot' | ||
}) | ||
}) | ||
@@ -49,34 +49,34 @@ // The `repository` object on the push event has a different format than the other events | ||
it('properly handles the push event', function () { | ||
event.payload = require('./fixtures/webhook/push'); | ||
event.payload = require('./fixtures/webhook/push') | ||
context = new Context(event); | ||
expect(context.repo()).toEqual({owner: 'bkeepers-inc', repo:'test'}); | ||
}); | ||
}); | ||
context = new Context(event) | ||
expect(context.repo()).toEqual({owner: 'bkeepers-inc', repo: 'test'}) | ||
}) | ||
}) | ||
describe('issue', function () { | ||
it('returns attributes from repository payload', function () { | ||
expect(context.issue()).toEqual({owner: 'bkeepers', repo:'probot', number: 4}); | ||
}); | ||
expect(context.issue()).toEqual({owner: 'bkeepers', repo: 'probot', number: 4}) | ||
}) | ||
it('merges attributes', function () { | ||
expect(context.issue({foo: 1, bar: 2})).toEqual({ | ||
owner: 'bkeepers', repo:'probot', number: 4, foo: 1, bar: 2 | ||
}); | ||
}); | ||
owner: 'bkeepers', repo: 'probot', number: 4, foo: 1, bar: 2 | ||
}) | ||
}) | ||
it('overrides repo attributes', function () { | ||
expect(context.issue({owner: 'muahaha', number: 5})).toEqual({ | ||
owner: 'muahaha', repo:'probot', number: 5 | ||
}); | ||
}); | ||
}); | ||
owner: 'muahaha', repo: 'probot', number: 5 | ||
}) | ||
}) | ||
}) | ||
describe('config', function () { | ||
let github; | ||
let github | ||
function readConfig(fileName) { | ||
const configPath = path.join(__dirname, 'fixtures', 'config', fileName); | ||
const content = fs.readFileSync(configPath, {encoding: 'utf8'}); | ||
return {data: {content: Buffer.from(content).toString('base64')}}; | ||
function readConfig (fileName) { | ||
const configPath = path.join(__dirname, 'fixtures', 'config', fileName) | ||
const content = fs.readFileSync(configPath, {encoding: 'utf8'}) | ||
return {data: {content: Buffer.from(content).toString('base64')}} | ||
} | ||
@@ -89,10 +89,10 @@ | ||
} | ||
}; | ||
} | ||
context = new Context(event, github); | ||
}); | ||
context = new Context(event, github) | ||
}) | ||
it('gets a valid configuration', async function () { | ||
github.repos.getContent.andReturn(Promise.resolve(readConfig('basic.yml'))); | ||
const config = await context.config('test-file.yml'); | ||
github.repos.getContent.andReturn(Promise.resolve(readConfig('basic.yml'))) | ||
const config = await context.config('test-file.yml') | ||
@@ -103,3 +103,3 @@ expect(github.repos.getContent).toHaveBeenCalledWith({ | ||
path: '.github/test-file.yml' | ||
}); | ||
}) | ||
expect(config).toEqual({ | ||
@@ -109,64 +109,69 @@ foo: 5, | ||
baz: 11 | ||
}); | ||
}); | ||
}) | ||
}) | ||
it('throws when the file is missing', async function () { | ||
github.repos.getContent.andReturn(Promise.reject(new Error('An error occurred'))); | ||
it('returns null when the file is missing', async function () { | ||
const error = new Error('An error occurred') | ||
error.code = 404 | ||
github.repos.getContent.andReturn(Promise.reject(error)) | ||
let e; | ||
let contents; | ||
try { | ||
contents = await context.config('test-file.yml'); | ||
} catch (err) { | ||
e = err; | ||
expect(await context.config('test-file.yml')).toBe(null) | ||
}) | ||
it('returns the default config when the file is missing and default config is passed', async function () { | ||
const error = new Error('An error occurred') | ||
error.code = 404 | ||
github.repos.getContent.andReturn(Promise.reject(error)) | ||
const defaultConfig = { | ||
foo: 5, | ||
bar: 7, | ||
baz: 11 | ||
} | ||
const contents = await context.config('test-file.yml', defaultConfig) | ||
expect(contents).toEqual(defaultConfig) | ||
}) | ||
expect(contents).toNotExist(); | ||
expect(e).toExist(); | ||
expect(e.message).toEqual('An error occurred'); | ||
}); | ||
it('throws when the configuration file is malformed', async function () { | ||
github.repos.getContent.andReturn(Promise.resolve(readConfig('malformed.yml'))); | ||
github.repos.getContent.andReturn(Promise.resolve(readConfig('malformed.yml'))) | ||
let e; | ||
let contents; | ||
let e | ||
let contents | ||
try { | ||
contents = await context.config('test-file.yml'); | ||
contents = await context.config('test-file.yml') | ||
} catch (err) { | ||
e = err; | ||
e = err | ||
} | ||
expect(contents).toNotExist(); | ||
expect(e).toExist(); | ||
expect(e.message).toMatch(/^end of the stream or a document separator/); | ||
}); | ||
expect(contents).toNotExist() | ||
expect(e).toExist() | ||
expect(e.message).toMatch(/^end of the stream or a document separator/) | ||
}) | ||
it('throws when loading unsafe yaml', async function () { | ||
github.repos.getContent.andReturn(readConfig('evil.yml')); | ||
github.repos.getContent.andReturn(readConfig('evil.yml')) | ||
let e; | ||
let config; | ||
let e | ||
let config | ||
try { | ||
config = await context.config('evil.yml'); | ||
config = await context.config('evil.yml') | ||
} catch (err) { | ||
e = err; | ||
e = err | ||
} | ||
expect(config).toNotExist(); | ||
expect(e).toExist(); | ||
expect(e.message).toMatch(/unknown tag/); | ||
}); | ||
expect(config).toNotExist() | ||
expect(e).toExist() | ||
expect(e.message).toMatch(/unknown tag/) | ||
}) | ||
it('returns an empty object when the file is empty', async function () { | ||
github.repos.getContent.andReturn(readConfig('empty.yml')); | ||
github.repos.getContent.andReturn(readConfig('empty.yml')) | ||
const contents = await context.config('test-file.yml'); | ||
const contents = await context.config('test-file.yml') | ||
expect(contents).toEqual({}); | ||
}); | ||
expect(contents).toEqual({}) | ||
}) | ||
it('overwrites default config settings', async function () { | ||
github.repos.getContent.andReturn(Promise.resolve(readConfig('basic.yml'))); | ||
const config = await context.config('test-file.yml', {foo: 10}); | ||
github.repos.getContent.andReturn(Promise.resolve(readConfig('basic.yml'))) | ||
const config = await context.config('test-file.yml', {foo: 10}) | ||
@@ -177,3 +182,3 @@ expect(github.repos.getContent).toHaveBeenCalledWith({ | ||
path: '.github/test-file.yml' | ||
}); | ||
}) | ||
expect(config).toEqual({ | ||
@@ -183,8 +188,8 @@ foo: 5, | ||
baz: 11 | ||
}); | ||
}); | ||
}) | ||
}) | ||
it('uses default settings to fill in missing options', async function () { | ||
github.repos.getContent.andReturn(Promise.resolve(readConfig('missing.yml'))); | ||
const config = await context.config('test-file.yml', {bar: 7}); | ||
github.repos.getContent.andReturn(Promise.resolve(readConfig('missing.yml'))) | ||
const config = await context.config('test-file.yml', {bar: 7}) | ||
@@ -195,3 +200,3 @@ expect(github.repos.getContent).toHaveBeenCalledWith({ | ||
path: '.github/test-file.yml' | ||
}); | ||
}) | ||
expect(config).toEqual({ | ||
@@ -201,5 +206,5 @@ foo: 5, | ||
baz: 11 | ||
}); | ||
}); | ||
}); | ||
}); | ||
}) | ||
}) | ||
}) | ||
}) |
module.exports = robot => { | ||
console.log('laoded plugin'); | ||
console.log('loaded app') | ||
} |
@@ -1,10 +0,10 @@ | ||
const expect = require('expect'); | ||
const createProbot = require('..'); | ||
const expect = require('expect') | ||
const createProbot = require('..') | ||
describe('Probot', () => { | ||
let probot; | ||
let event; | ||
let probot | ||
let event | ||
beforeEach(() => { | ||
probot = createProbot(); | ||
probot = createProbot() | ||
@@ -14,34 +14,34 @@ event = { | ||
payload: require('./fixtures/webhook/push') | ||
}; | ||
}); | ||
} | ||
}) | ||
describe('webhook delivery', () => { | ||
it('forwards webhooks to the robot', async () => { | ||
const robot = probot.load(() => {}); | ||
robot.receive = expect.createSpy(); | ||
probot.webhook.emit('*', event); | ||
expect(robot.receive).toHaveBeenCalledWith(event); | ||
}); | ||
}); | ||
const robot = probot.load(() => {}) | ||
robot.receive = expect.createSpy() | ||
probot.webhook.emit('*', event) | ||
expect(robot.receive).toHaveBeenCalledWith(event) | ||
}) | ||
}) | ||
describe('server', () => { | ||
const request = require('supertest'); | ||
const request = require('supertest') | ||
it('prefixes paths with route name', () => { | ||
probot.load(robot => { | ||
const app = robot.route('/my-plugin'); | ||
app.get('/foo', (req, res) => res.end('foo')); | ||
}); | ||
const app = robot.route('/my-plugin') | ||
app.get('/foo', (req, res) => res.end('foo')) | ||
}) | ||
return request(probot.server).get('/my-plugin/foo').expect(200, 'foo'); | ||
}); | ||
return request(probot.server).get('/my-plugin/foo').expect(200, 'foo') | ||
}) | ||
it('allows routes with no path', () => { | ||
probot.load(robot => { | ||
const app = robot.route(); | ||
app.get('/foo', (req, res) => res.end('foo')); | ||
}); | ||
const app = robot.route() | ||
app.get('/foo', (req, res) => res.end('foo')) | ||
}) | ||
return request(probot.server).get('/foo').expect(200, 'foo'); | ||
}); | ||
return request(probot.server).get('/foo').expect(200, 'foo') | ||
}) | ||
@@ -51,52 +51,94 @@ it('isolates plugins from affecting eachother', async () => { | ||
probot.load(robot => { | ||
const app = robot.route('/' + name); | ||
const app = robot.route('/' + name) | ||
app.use(function (req, res, next) { | ||
res.append('X-Test', name); | ||
next(); | ||
}); | ||
res.append('X-Test', name) | ||
next() | ||
}) | ||
app.get('/hello', (req, res) => res.end(name)); | ||
}); | ||
}); | ||
app.get('/hello', (req, res) => res.end(name)) | ||
}) | ||
}) | ||
await request(probot.server).get('/foo/hello') | ||
.expect(200, 'foo') | ||
.expect('X-Test', 'foo'); | ||
.expect('X-Test', 'foo') | ||
await request(probot.server).get('/bar/hello') | ||
.expect(200, 'bar') | ||
.expect('X-Test', 'bar'); | ||
}); | ||
}); | ||
.expect('X-Test', 'bar') | ||
}) | ||
it('allows users to configure webhook paths', async () => { | ||
probot = createProbot({webhookPath: '/webhook'}) | ||
probot.load(robot => { | ||
const app = robot.route() | ||
app.get('/webhook', (req, res) => res.end('get-webhook')) | ||
app.post('/webhook', (req, res) => res.end('post-webhook')) | ||
}) | ||
// GET requests should succeed | ||
await request(probot.server).get('/webhook') | ||
.expect(200, 'get-webhook') | ||
// POST requests should fail b/c webhook path has precedence | ||
await request(probot.server).post('/webhook') | ||
.expect(400) | ||
}) | ||
it('defaults webhook path to `/`', async () => { | ||
// GET requests to `/` should 404 at express level, not 400 at webhook level | ||
await request(probot.server).get('/') | ||
.expect(404) | ||
// POST requests to `/` should 400 b/c webhook signature will fail | ||
await request(probot.server).post('/') | ||
.expect(400) | ||
}) | ||
}) | ||
describe('receive', () => { | ||
it('forwards events to each plugin', async () => { | ||
const spy = expect.createSpy(); | ||
const robot = probot.load(robot => robot.on('push', spy)); | ||
robot.auth = expect.createSpy().andReturn(Promise.resolve({})); | ||
const spy = expect.createSpy() | ||
const robot = probot.load(robot => robot.on('push', spy)) | ||
robot.auth = expect.createSpy().andReturn(Promise.resolve({})) | ||
await probot.receive(event); | ||
await probot.receive(event) | ||
expect(spy).toHaveBeenCalled(); | ||
}); | ||
}); | ||
expect(spy).toHaveBeenCalled() | ||
}) | ||
}) | ||
describe('robot', () => { | ||
it('will be removed in 0.10', () => { | ||
// This test will fail in version 0.10 | ||
const semver = require('semver'); | ||
const pkg = require('../package'); | ||
expect(semver.satisfies(pkg.version, '<0.10')).toBe(true); | ||
}); | ||
describe('sentry', () => { | ||
afterEach(() => { | ||
// Clean up env variables | ||
delete process.env.SENTRY_URL | ||
delete process.env.SENTRY_DSN | ||
}) | ||
it('returns the first defined (for now)', () => { | ||
const robot = probot.load(() => { }); | ||
expect(probot.robot).toBe(robot); | ||
}); | ||
describe('SENTRY_URL', () => { | ||
it('will be removed in 0.11', () => { | ||
// This test will fail in version 0.11 | ||
const semver = require('semver') | ||
const pkg = require('../package') | ||
expect(semver.satisfies(pkg.version, '<0.11')).toBe(true) | ||
}) | ||
it('returns a robot if no plugins are loaded', () => { | ||
expect(probot.robot).toExist(); | ||
}); | ||
}); | ||
}); | ||
it('configures sentry via the SENTRY_URL ', () => { | ||
process.env.SENTRY_URL = '09290' | ||
expect(() => { | ||
createProbot() | ||
}).toThrow(/Invalid Sentry DSN: 09290/) | ||
}) | ||
}) | ||
describe('SENTRY_DSN', () => { | ||
it('configures sentry via the SENTRY_DSN ', () => { | ||
process.env.SENTRY_DSN = '1233' | ||
expect(() => { | ||
createProbot() | ||
}).toThrow(/Invalid Sentry DSN: 1233/) | ||
}) | ||
}) | ||
}) | ||
}) |
/* eslint prefer-arrow-callback: off */ | ||
const expect = require('expect'); | ||
const pluginLoaderFactory = require('../lib/plugin'); | ||
const expect = require('expect') | ||
const pluginLoaderFactory = require('../lib/plugin') | ||
const stubPluginPath = require.resolve('./fixtures/plugin/stub-plugin'); | ||
const basedir = process.cwd(); | ||
const stubPluginPath = require.resolve('./fixtures/plugin/stub-plugin') | ||
const basedir = process.cwd() | ||
const nullLogger = {}; | ||
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach(level => { | ||
nullLogger[level] = function () { }; | ||
}); | ||
nullLogger[level] = function () { } | ||
}) | ||
describe('plugin loader', function () { | ||
let probot; | ||
let pluginLoader; | ||
let autoloader; | ||
let autoplugins; | ||
let resolver; | ||
let probot | ||
let pluginLoader | ||
let autoloader | ||
let autoplugins | ||
let resolver | ||
@@ -24,12 +24,12 @@ beforeEach(function () { | ||
logger: nullLogger | ||
}; | ||
} | ||
autoplugins = { | ||
probotPlugin: expect.createSpy() | ||
}; | ||
} | ||
autoloader = expect.createSpy().andReturn(autoplugins); | ||
autoloader = expect.createSpy().andReturn(autoplugins) | ||
resolver = expect.createSpy().andReturn(stubPluginPath); | ||
}); | ||
resolver = expect.createSpy().andReturn(stubPluginPath) | ||
}) | ||
@@ -39,55 +39,55 @@ describe('factory', function () { | ||
it('should throw a TypeError', function () { | ||
expect(pluginLoaderFactory).toThrow(TypeError); | ||
}); | ||
}); | ||
expect(pluginLoaderFactory).toThrow(TypeError) | ||
}) | ||
}) | ||
describe('when robot provided', function () { | ||
it('should return an object', function () { | ||
expect(pluginLoaderFactory(probot)).toBeA(Object); | ||
}); | ||
}); | ||
expect(pluginLoaderFactory(probot)).toBeA(Object) | ||
}) | ||
}) | ||
describe('autoload()', function () { | ||
beforeEach(() => { | ||
pluginLoader = pluginLoaderFactory(probot, {autoloader}); | ||
}); | ||
pluginLoader = pluginLoaderFactory(probot, {autoloader}) | ||
}) | ||
it('should ask the autoloader for probot-related plugins', function () { | ||
pluginLoader.autoload(); | ||
expect(autoloader).toHaveBeenCalledWith('probot-*'); | ||
}); | ||
pluginLoader.autoload() | ||
expect(autoloader).toHaveBeenCalledWith('probot-*') | ||
}) | ||
it('should ask the robot to load the plugins', function () { | ||
pluginLoader.autoload(); | ||
expect(probot.load).toHaveBeenCalledWith(autoplugins.probotPlugin); | ||
}); | ||
}); | ||
pluginLoader.autoload() | ||
expect(probot.load).toHaveBeenCalledWith(autoplugins.probotPlugin) | ||
}) | ||
}) | ||
describe('load()', function () { | ||
beforeEach(() => { | ||
pluginLoader = pluginLoaderFactory(probot, {resolver}); | ||
}); | ||
pluginLoader = pluginLoaderFactory(probot, {resolver}) | ||
}) | ||
describe('when supplied no plugin names', function () { | ||
it('should do nothing', function () { | ||
pluginLoader.load(); | ||
expect(resolver).toNotHaveBeenCalled(); | ||
expect(probot.load).toNotHaveBeenCalled(); | ||
}); | ||
}); | ||
pluginLoader.load() | ||
expect(resolver).toNotHaveBeenCalled() | ||
expect(probot.load).toNotHaveBeenCalled() | ||
}) | ||
}) | ||
describe('when supplied plugin name(s)', function () { | ||
it('should attempt to resolve plugins by name and basedir', function () { | ||
pluginLoader.load(['foo', 'bar']); | ||
pluginLoader.load(['foo', 'bar']) | ||
expect(resolver).toHaveBeenCalledWith('foo', {basedir}) | ||
.toHaveBeenCalledWith('bar', {basedir}); | ||
}); | ||
.toHaveBeenCalledWith('bar', {basedir}) | ||
}) | ||
it('should ask the robot to load a plugin at its resolved path', function () { | ||
pluginLoader.load(['see-stub-for-resolved-path']); | ||
expect(probot.load).toHaveBeenCalledWith(require(stubPluginPath)); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
pluginLoader.load(['see-stub-for-resolved-path']) | ||
expect(probot.load).toHaveBeenCalledWith(require(stubPluginPath)) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |
@@ -1,21 +0,21 @@ | ||
const fs = require('fs'); | ||
const fs = require('fs') | ||
const expect = require('expect'); | ||
const expect = require('expect') | ||
const {findPrivateKey} = require('../lib/private-key'); | ||
const {findPrivateKey} = require('../lib/private-key') | ||
describe('private-key', function () { | ||
let privateKey; | ||
let keyfilePath; | ||
let privateKey | ||
let keyfilePath | ||
beforeEach(function () { | ||
privateKey = 'I AM PRIVET KEY!?!!~1!'; | ||
keyfilePath = '/some/path'; | ||
privateKey = 'I AM PRIVET KEY!?!!~1!' | ||
keyfilePath = '/some/path' | ||
expect.spyOn(fs, 'readFileSync') | ||
.andReturn(privateKey); | ||
}); | ||
.andReturn(privateKey) | ||
}) | ||
afterEach(function () { | ||
expect.restoreSpies(); | ||
}); | ||
expect.restoreSpies() | ||
}) | ||
@@ -25,63 +25,63 @@ describe('findPrivateKey()', function () { | ||
it('should read the file at given filepath', function () { | ||
findPrivateKey(keyfilePath); | ||
findPrivateKey(keyfilePath) | ||
expect(fs.readFileSync) | ||
.toHaveBeenCalledWith(keyfilePath); | ||
}); | ||
.toHaveBeenCalledWith(keyfilePath) | ||
}) | ||
it('should return the key', function () { | ||
expect(findPrivateKey(keyfilePath)) | ||
.toEqual(privateKey); | ||
}); | ||
}); | ||
.toEqual(privateKey) | ||
}) | ||
}) | ||
describe('when a PRIVATE_KEY env var is provided', function () { | ||
beforeEach(function () { | ||
process.env.PRIVATE_KEY = privateKey; | ||
}); | ||
process.env.PRIVATE_KEY = privateKey | ||
}) | ||
afterEach(function () { | ||
delete process.env.PRIVATE_KEY; | ||
}); | ||
delete process.env.PRIVATE_KEY | ||
}) | ||
it('should return the key', function () { | ||
expect(findPrivateKey()) | ||
.toEqual(privateKey); | ||
}); | ||
}); | ||
.toEqual(privateKey) | ||
}) | ||
}) | ||
describe('when a PRIVATE_KEY has line breaks', function () { | ||
beforeEach(function () { | ||
process.env.PRIVATE_KEY = 'line 1\\nline 2'; | ||
}); | ||
process.env.PRIVATE_KEY = 'line 1\\nline 2' | ||
}) | ||
afterEach(function () { | ||
delete process.env.PRIVATE_KEY; | ||
}); | ||
delete process.env.PRIVATE_KEY | ||
}) | ||
it('should return the key', function () { | ||
expect(findPrivateKey()) | ||
.toEqual('line 1\nline 2'); | ||
}); | ||
}); | ||
.toEqual('line 1\nline 2') | ||
}) | ||
}) | ||
describe('when a PRIVATE_KEY_PATH env var is provided', function () { | ||
beforeEach(function () { | ||
process.env.PRIVATE_KEY_PATH = keyfilePath; | ||
}); | ||
process.env.PRIVATE_KEY_PATH = keyfilePath | ||
}) | ||
afterEach(function () { | ||
delete process.env.PRIVATE_KEY_PATH; | ||
}); | ||
delete process.env.PRIVATE_KEY_PATH | ||
}) | ||
it('should read the file at given filepath', function () { | ||
findPrivateKey(); | ||
findPrivateKey() | ||
expect(fs.readFileSync) | ||
.toHaveBeenCalledWith(keyfilePath); | ||
}); | ||
.toHaveBeenCalledWith(keyfilePath) | ||
}) | ||
it('should return the key', function () { | ||
expect(findPrivateKey()) | ||
.toEqual(privateKey); | ||
}); | ||
}); | ||
.toEqual(privateKey) | ||
}) | ||
}) | ||
@@ -94,31 +94,31 @@ describe('when no private key is provided', function () { | ||
'foo.pem' | ||
]); | ||
}); | ||
]) | ||
}) | ||
it('should look for one in the current directory', function () { | ||
findPrivateKey(); | ||
findPrivateKey() | ||
expect(fs.readdirSync) | ||
.toHaveBeenCalledWith(process.cwd()); | ||
}); | ||
.toHaveBeenCalledWith(process.cwd()) | ||
}) | ||
describe('and a key file is present', function () { | ||
it('should load the key file', function () { | ||
findPrivateKey(); | ||
findPrivateKey() | ||
expect(fs.readFileSync) | ||
.toHaveBeenCalledWith('foo.pem'); | ||
}); | ||
}); | ||
.toHaveBeenCalledWith('foo.pem') | ||
}) | ||
}) | ||
describe('and a key file is not present', function () { | ||
beforeEach(function () { | ||
fs.readdirSync.restore(); | ||
}); | ||
fs.readdirSync.restore() | ||
}) | ||
it('should throw an error', function () { | ||
expect(findPrivateKey) | ||
.toThrow(Error, /missing private key for GitHub App/i); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
.toThrow(Error, /missing private key for GitHub App/i) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |
@@ -1,13 +0,13 @@ | ||
const expect = require('expect'); | ||
const Context = require('../lib/context'); | ||
const createRobot = require('../lib/robot'); | ||
const expect = require('expect') | ||
const Context = require('../lib/context') | ||
const createRobot = require('../lib/robot') | ||
describe('Robot', function () { | ||
let robot; | ||
let event; | ||
let spy; | ||
let robot | ||
let event | ||
let spy | ||
beforeEach(function () { | ||
robot = createRobot(); | ||
robot.auth = () => {}; | ||
robot = createRobot() | ||
robot.auth = () => {} | ||
@@ -20,6 +20,6 @@ event = { | ||
} | ||
}; | ||
} | ||
spy = expect.createSpy(); | ||
}); | ||
spy = expect.createSpy() | ||
}) | ||
@@ -35,55 +35,71 @@ describe('constructor', () => { | ||
fatal: expect.createSpy() | ||
}; | ||
robot = createRobot({logger}); | ||
} | ||
robot = createRobot({logger}) | ||
robot.log('hello world'); | ||
expect(logger.debug).toHaveBeenCalledWith('hello world'); | ||
}); | ||
}); | ||
robot.log('hello world') | ||
expect(logger.debug).toHaveBeenCalledWith('hello world') | ||
}) | ||
}) | ||
describe('on', function () { | ||
it('calls callback when no action is specified', async function () { | ||
robot.on('test', spy); | ||
robot.on('test', spy) | ||
expect(spy).toNotHaveBeenCalled(); | ||
await robot.receive(event); | ||
expect(spy).toHaveBeenCalled(); | ||
expect(spy.calls[0].arguments[0]).toBeA(Context); | ||
expect(spy.calls[0].arguments[0].payload).toBe(event.payload); | ||
}); | ||
expect(spy).toNotHaveBeenCalled() | ||
await robot.receive(event) | ||
expect(spy).toHaveBeenCalled() | ||
expect(spy.calls[0].arguments[0]).toBeA(Context) | ||
expect(spy.calls[0].arguments[0].payload).toBe(event.payload) | ||
}) | ||
it('calls callback with same action', async function () { | ||
robot.on('test.foo', spy); | ||
robot.on('test.foo', spy) | ||
await robot.receive(event); | ||
expect(spy).toHaveBeenCalled(); | ||
}); | ||
await robot.receive(event) | ||
expect(spy).toHaveBeenCalled() | ||
}) | ||
it('does not call callback with different action', async function () { | ||
robot.on('test.nope', spy); | ||
robot.on('test.nope', spy) | ||
await robot.receive(event); | ||
expect(spy).toNotHaveBeenCalled(); | ||
}); | ||
await robot.receive(event) | ||
expect(spy).toNotHaveBeenCalled() | ||
}) | ||
it('calls callback with *', async function () { | ||
robot.on('*', spy); | ||
robot.on('*', spy) | ||
await robot.receive(event); | ||
expect(spy).toHaveBeenCalled(); | ||
}); | ||
}); | ||
await robot.receive(event) | ||
expect(spy).toHaveBeenCalled() | ||
}) | ||
it('calls callback x amount of times when an array of x actions is passed', async function () { | ||
const event2 = { | ||
event: 'arrayTest', | ||
payload: { | ||
action: 'bar', | ||
installation: {id: 2} | ||
} | ||
} | ||
robot.on(['test.foo', 'arrayTest.bar'], spy) | ||
await robot.receive(event) | ||
await robot.receive(event2) | ||
expect(spy.calls.length).toEqual(2) | ||
}) | ||
}) | ||
describe('receive', () => { | ||
it('delivers the event', async () => { | ||
const spy = expect.createSpy(); | ||
robot.on('test', spy); | ||
const spy = expect.createSpy() | ||
robot.on('test', spy) | ||
await robot.receive(event); | ||
await robot.receive(event) | ||
expect(spy).toHaveBeenCalled(); | ||
}); | ||
expect(spy).toHaveBeenCalled() | ||
}) | ||
it('waits for async events to resolve', async () => { | ||
const spy = expect.createSpy(); | ||
const spy = expect.createSpy() | ||
@@ -93,42 +109,42 @@ robot.on('test', () => { | ||
setTimeout(() => { | ||
spy(); | ||
resolve(); | ||
}, 1); | ||
}); | ||
}); | ||
spy() | ||
resolve() | ||
}, 1) | ||
}) | ||
}) | ||
await robot.receive(event); | ||
await robot.receive(event) | ||
expect(spy).toHaveBeenCalled(); | ||
}); | ||
expect(spy).toHaveBeenCalled() | ||
}) | ||
it('returns a reject errors thrown in plugins', async () => { | ||
it('returns a reject errors thrown in apps', async () => { | ||
robot.on('test', () => { | ||
throw new Error('error from plugin'); | ||
}); | ||
throw new Error('error from app') | ||
}) | ||
try { | ||
await robot.receive(event); | ||
throw new Error('expected error to be raised from plugin'); | ||
await robot.receive(event) | ||
throw new Error('expected error to be raised from app') | ||
} catch (err) { | ||
expect(err.message).toEqual('error from plugin'); | ||
expect(err.message).toEqual('error from app') | ||
} | ||
}); | ||
}); | ||
}) | ||
}) | ||
describe('error handling', () => { | ||
let error; | ||
let error | ||
beforeEach(() => { | ||
error = new Error('testing'); | ||
robot.log.error = expect.createSpy(); | ||
}); | ||
error = new Error('testing') | ||
robot.log.error = expect.createSpy() | ||
}) | ||
it('logs errors thrown from handlers', async () => { | ||
robot.on('test', () => { | ||
throw error; | ||
}); | ||
throw error | ||
}) | ||
try { | ||
await robot.receive(event); | ||
await robot.receive(event) | ||
} catch (err) { | ||
@@ -138,12 +154,12 @@ // Expected | ||
const arg = robot.log.error.calls[0].arguments[0]; | ||
expect(arg.err).toBe(error); | ||
expect(arg.event).toBe(event); | ||
}); | ||
const arg = robot.log.error.calls[0].arguments[0] | ||
expect(arg.err).toBe(error) | ||
expect(arg.event).toBe(event) | ||
}) | ||
it('logs errors from rejected promises', async () => { | ||
robot.on('test', () => Promise.reject(error)); | ||
robot.on('test', () => Promise.reject(error)) | ||
try { | ||
await robot.receive(event); | ||
await robot.receive(event) | ||
} catch (err) { | ||
@@ -153,8 +169,8 @@ // Expected | ||
expect(robot.log.error).toHaveBeenCalled(); | ||
const arg = robot.log.error.calls[0].arguments[0]; | ||
expect(arg.err).toBe(error); | ||
expect(arg.event).toBe(event); | ||
}); | ||
}); | ||
}); | ||
expect(robot.log.error).toHaveBeenCalled() | ||
const arg = robot.log.error.calls[0].arguments[0] | ||
expect(arg.err).toBe(error) | ||
expect(arg.event).toBe(event) | ||
}) | ||
}) | ||
}) |
@@ -1,32 +0,32 @@ | ||
const expect = require('expect'); | ||
const request = require('supertest'); | ||
const createServer = require('../lib/server'); | ||
const expect = require('expect') | ||
const request = require('supertest') | ||
const createServer = require('../lib/server') | ||
describe('server', function () { | ||
let server; | ||
let webhook; | ||
let server | ||
let webhook | ||
beforeEach(() => { | ||
webhook = expect.createSpy().andCall((req, res, next) => next()); | ||
server = createServer(webhook); | ||
}); | ||
webhook = expect.createSpy().andCall((req, res, next) => next()) | ||
server = createServer(webhook) | ||
}) | ||
describe('GET /ping', () => { | ||
it('returns a 200 repsonse', () => { | ||
return request(server).get('/ping').expect(200, 'PONG'); | ||
}); | ||
}); | ||
return request(server).get('/ping').expect(200, 'PONG') | ||
}) | ||
}) | ||
describe('webhook handler', () => { | ||
it('should 500 on a webhook error', () => { | ||
webhook.andCall((req, res, callback) => callback('webhook error')); | ||
return request(server).post('/').expect(500); | ||
}); | ||
}); | ||
webhook.andCall((req, res, callback) => callback(new Error('webhook error'))) | ||
return request(server).post('/').expect(500) | ||
}) | ||
}) | ||
describe('with an unknown url', () => { | ||
it('responds with 404', () => { | ||
return request(server).get('/lolnotfound').expect(404); | ||
}); | ||
}); | ||
}); | ||
return request(server).get('/lolnotfound').expect(404) | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
GitHub dependency
Supply chain riskContains a dependency which resolves to a GitHub URL. Dependencies fetched from GitHub specifiers are not immutable can be used to inject untrusted code or reduce the likelihood of a reproducible install.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
96304
7
57
1530
24
1
38
- Removedbl@1.1.2(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removedgithub-webhook-handler@0.6.0(transitive)
- Removedprocess-nextick-args@1.0.7(transitive)
- Removedreadable-stream@2.0.6(transitive)
- Removedstring_decoder@0.10.31(transitive)
- Removedutil-deprecate@1.0.2(transitive)
Updatedbunyan@^1.8.12
Updatedcommander@^2.11.0
Updatedexpress@^4.15.4
Updatedgithub@^9.3.1
Updatedgithub-webhook-handler@github:rvagg/github-webhook-handler#v0.6.1
Updatedjs-yaml@^3.9.1
Updatedraven@^2.1.2
Updatedresolve@^1.4.0
Updatedsemver@^5.4.1