Socket
Socket
Sign inDemoInstall

probot

Package Overview
Dependencies
Maintainers
2
Versions
314
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

probot - npm Package Compare versions

Comparing version 0.9.1 to 0.10.0

CONTRIBUTING.md

1

.jsdoc.json
{
"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`

@@ -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
};
}

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc