Merge remote-tracking branch 'origin/master' into doc-deploy

* origin/master: (27 commits)
  Release v0.4.1.
  Disable raven console alerts
  Relax node version requirements
  Release v0.4.0.
  Update log for local server
  Remove tunnel script
  Move subdomain config
  Update docs to use localtunnel
  Add subdomain option
  Setup tunnel using localtunnel
  Ensure node version is satisfied
  autoBreadcrumbs
  Use local logger variable
  Connect sentry to logger
  Move logger setup to index
  Fix package.json for multiple plugins
  Specific filename not needed
  Remove log
  Ignore all private keys
  Look for any pem in the cwd
  ...
This commit is contained in:
Brandon Keepers 2017-04-08 22:57:19 -05:00
commit d46c3a25fb
No known key found for this signature in database
GPG Key ID: F9533396D5FACBF6
14 changed files with 181 additions and 53 deletions

View File

@ -4,3 +4,6 @@ WEBHOOK_SECRET=development
# Uncomment this to get verbose logging
# LOG_LEVEL=trace # or `info` to show less
# Subdomain to use for localtunnel server. Defaults to your local username.
# SUBDOMAIN=

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
npm-debug.log
.DS_Store
node_modules/
private-key.pem
*.pem
.env

46
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource+probot@github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -1,5 +1,13 @@
#!/usr/bin/env 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);
}
require('commander')
.version(require('../package').version)
.usage('<command> [options]')

View File

@ -17,6 +17,7 @@ program
process.exit(1);
}
}, process.env.PRIVATE_KEY)
.option('-t, --tunnel <subdomain>', 'Expose your local bot to the internet', process.env.SUBDOMAIN || process.env.NODE_ENV != 'production')
.parse(process.argv);
if(!program.integration) {
@ -25,14 +26,45 @@ if(!program.integration) {
}
if(!program.privateKey) {
try {
program.privateKey = require('fs').readFileSync('private-key.pem');
} catch (err) {
const fs = require('fs');
const key = fs.readdirSync(process.cwd()).find(path => path.endsWith('.pem'));
if (key) {
program.privateKey = fs.readFileSync(key);
} else {
console.warn("Missing GitHub Integration private key.\nUse --private-key flag or set PRIVATE_KEY environment variable.");
process.exit(1);
}
}
if(program.tunnel) {
try {
setupTunnel();
} catch(err) {
console.warn('Run `npm install --save-dev localtunnel` to enable localtunnel.');
}
}
function setupTunnel() {
const localtunnel = require('localtunnel');
const subdomain = typeof program.tunnel == 'string' ?
program.tunnel :
require('os').userInfo().username;
const tunnel = localtunnel(program.port, {subdomain}, function (err, tunnel) {
if (err) {
console.warn('Could not open tunnel: ', err.message);
} else {
console.log('Listening on ' + tunnel.url);
tunnel.url;
}
});
tunnel.on('close', function() {
console.warn('Local tunnel closed');
});
}
const pkgConf = require('pkg-conf');
const resolve = require('resolve').sync;
const createProbot = require('../');

31
docs/best-practices.md Normal file
View File

@ -0,0 +1,31 @@
# Best practices for plugins
## Autonomy
### Never take bulk actions without explicit permission
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.
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.
### 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.
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.
## Configuration
### Require minimal configuration
Plugins _should_ provide sensible defaults for all settings.
### Provide full configuration
Plugins _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.
For example, the [owners](https://github.com/probot/owners) plugin reads from the `OWNERS` file, which is a convention that existed before the plugin was created, while the [configurer](https://github.com/probot/configurer) plugin reads from `.github/config.yml`.

View File

@ -53,13 +53,13 @@ To deploy a bot that includes multiple plugins, create a new app that has the pl
```json
{
"name": "my-probot",
"priate": true,
"private": true,
"dependencies": {
"probot-autoresponder": "~1.0",
"probot-configurer": "~1.0",
"probot-autoresponder": "probot/autoresponder",
"probot-configurer": "probot/configurer"
},
"scripts": {
"run": "probot run"
"start": "probot run"
},
"probot": {
"plugins": [

View File

@ -3,23 +3,18 @@
To run a plugin locally, you'll need to create a GitHub Integration and configure it to deliver webhooks to your local machine.
1. Make sure you have a recent version of [Node.js](https://nodejs.org/) installed
1. Install [ngrok](https://ngrok.com/download) (`$ brew cask install ngrok` on a mac), which will expose the local server to the internet so GitHub can send webhooks
1. Run `$ ngrok http 3000` to start ngrok, which should output something like `Forwarding https://4397efc6.ngrok.io -> localhost:3000`
1. [Create a new GitHub Integration](https://github.com/settings/integrations/new) with:
- **Callback URL** and **Webhook URL**: The full ngrok url above. For example: `https://4397efc6.ngrok.io/`
- **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 `private-key.pem` in the project directory
1. Download the private key and move it to the project directory
1. Edit `.env` and set `INTEGRATION_ID` to the ID of the integration you just created.
1. With `ngrok` still running, open another terminal and run `$ npm start` to start the server on http://localhost:3000
1. Run `$ npm start` to start the server, which will output `Listening on https://yourname.localtunnel.me`;
1. Update the **Webhook URL** in the [integration settings](https://github.com/settings/integrations) to use the `localtunnel.me` URL.
You'll need to create a test repository and install your Integration by clicking the "Install" button on the settings page.
Whenever you com back to work on the app after you've already had it running once, then you need to:
1. Run `$ npm start`
1. Run `$ ngrok http 3000` in another terminal window
1. `ngrok` will use a different URL every time it is restarted, so you will have to go into the [settings for your Integration](https://github.com/settings/integrations) and update all the URLs.
Whenever you com back to work on the app after you've already had it running once, you should only need to run `$ npm start`.
## Debugging

View File

@ -107,8 +107,6 @@ $ probot run -i 9999 -P private-key.pem ./autoresponder.js
Listening on http://localhost:3000
```
Once your bot is running, you'll need to use `ngrok` to receive GitHub webhooks as described in the [development](development.md) documentation.
## Publishing your bot
Plugins can be published in NPM modules, which can either be deployed as stand-alone bots, or combined with other plugins.

View File

@ -1,7 +1,11 @@
const bunyan = require('bunyan');
const bunyanFormat = require('bunyan-format');
const sentryStream = require('bunyan-sentry-stream');
const cacheManager = require('cache-manager');
const createWebhook = require('github-webhook-handler');
const createIntegration = require('github-integration');
const createWebhook = require('github-webhook-handler');
const Raven = require('raven');
const createRobot = require('./lib/robot');
const createServer = require('./lib/server');
@ -11,6 +15,12 @@ module.exports = options => {
ttl: 60 * 60 // 1 hour
});
const logger = bunyan.createLogger({
name: 'PRobot',
level: process.env.LOG_LEVEL || 'debug',
stream: bunyanFormat({outputMode: process.env.LOG_FORMAT || 'short'})
});
const webhook = createWebhook({path: '/', secret: options.secret});
const integration = createIntegration({
id: options.id,
@ -18,22 +28,21 @@ module.exports = options => {
debug: process.env.LOG_LEVEL === 'trace'
});
const server = createServer(webhook);
const robot = createRobot(integration, webhook, cache);
const robot = createRobot({integration, webhook, cache, logger});
if (process.env.SENTRY_URL) {
Raven.disableConsoleAlerts();
Raven.config(process.env.SENTRY_URL, {
captureUnhandledRejections: true
captureUnhandledRejections: true,
autoBreadcrumbs: true
}).install({});
}
// Show trace for any unhandled rejections
process.on('unhandledRejection', reason => {
robot.log.error(reason);
});
logger.addStream(sentryStream(Raven));
}
// Handle case when webhook creation fails
webhook.on('error', err => {
robot.log.error(err);
logger.error(err);
});
return {
@ -42,7 +51,7 @@ module.exports = options => {
start() {
server.listen(options.port);
console.log('Listening on http://localhost:' + options.port);
logger.trace('Listening on http://localhost:' + options.port);
},
load(plugin) {

View File

@ -1,20 +1,15 @@
const bunyan = require('bunyan');
const bunyanFormat = require('bunyan-format');
const GitHubApi = require('github');
const Bottleneck = require('bottleneck');
const Context = require('./context');
const logger = bunyan.createLogger({
name: 'PRobot',
level: process.env.LOG_LEVEL || 'debug',
stream: bunyanFormat({outputMode: process.env.LOG_FORMAT || 'short'})
});
class Robot {
constructor(integration, webhook, cache) {
constructor({integration, webhook, cache, logger}) {
this.integration = integration;
this.webhook = webhook;
this.cache = cache;
this.log = wrapLogger(logger);
this.webhook.on('*', event => this.log.trace(event, 'webhook received'));
}
on(event, callback) {
@ -44,10 +39,6 @@ class Robot {
return probotEnhancedClient(github);
}
log(...args) {
return logger.debug(...args);
}
}
function probotEnhancedClient(github) {
@ -69,9 +60,21 @@ function rateLimitedClient(github) {
return github;
}
// Add level methods on the logger
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach(level => {
Robot.prototype.log[level] = logger[level].bind(logger);
});
// Return a function that defaults to "debug" level, and has properties for
// other levels:
//
// robot.log("debug")
// robot.log.trace("verbose details");
//
function wrapLogger(logger) {
const fn = logger.debug.bind(logger);
// Add level methods on the logger
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach(level => {
fn[level] = logger[level].bind(logger);
});
return fn;
}
module.exports = (...args) => new Robot(...args);

View File

@ -1,6 +1,6 @@
{
"name": "probot",
"version": "0.3.0",
"version": "0.4.1",
"description": "a trainable robot that responds to activity on GitHub",
"repository": "https://github.com/probot/probot",
"main": "index.js",
@ -17,6 +17,7 @@
"bottleneck": "^1.15.1",
"bunyan": "^1.8.5",
"bunyan-format": "^0.2.1",
"bunyan-sentry-stream": "^1.1.0",
"cache-manager": "^2.4.0",
"commander": "^2.9.0",
"dotenv": "~4.0.0",
@ -60,6 +61,6 @@
]
},
"engines": {
"node": "~7.7.0"
"node": "^7.7"
}
}

View File

@ -1,3 +0,0 @@
#!/bin/sh
ngrok http -bind-tls=true 3000

View File

@ -3,13 +3,18 @@ const expect = require('expect');
const Context = require('../lib/context');
const createRobot = require('../lib/robot');
const nullLogger = {};
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach(level => {
nullLogger[level] = function () { };
});
describe('Robot', () => {
let webhook;
let robot;
beforeEach(() => {
webhook = new EventEmitter();
robot = createRobot({}, webhook);
robot = createRobot({webhook, logger: nullLogger});
});
describe('on', () => {