forked from mirrors/probot
commit
3c19bc96ec
|
@ -0,0 +1,4 @@
|
|||
# The ID of your GitHub integration
|
||||
INTEGRATION_ID=
|
||||
|
||||
WEBHOOK_SECRET=development
|
|
@ -1,3 +1,5 @@
|
|||
npm-debug.log
|
||||
.DS_Store
|
||||
node_modules/
|
||||
private-key.pem
|
||||
.env
|
||||
|
|
|
@ -4,3 +4,5 @@ node_js:
|
|||
- "6"
|
||||
notifications:
|
||||
disabled: true
|
||||
env:
|
||||
- PRIVATE_KEY=ssssshhh
|
||||
|
|
|
@ -1,29 +1,37 @@
|
|||
# Contributing
|
||||
|
||||
## Setup
|
||||
## Running Locally
|
||||
|
||||
0. Clone the repo
|
||||
0. Make sure you have the latest version of [Node.js](https://nodejs.org/) to develop locally.
|
||||
0. Clone the repository with `git clone https://github.com/bkeepers/PRobot.git`
|
||||
0. Make sure you have a recent version of [Node.js](https://nodejs.org/) installed
|
||||
0. Run `$ script/bootstrap` to install all the project dependencies
|
||||
0. Until this gets built into a proper [Integration](https://developer.github.com/early-access/integrations/), the bot will need a valid GitHub API token to be able to do anything useful. Create a new [Personal access token](https://github.com/settings/tokens/new) and select the `repo` scope.
|
||||
0. Re-start the local server with the token by running: `$ GITHUB_TOKEN=xxx script/server` to run the server on http://localhost:3000
|
||||
0. Download [ngrok](https://ngrok.com/download) (`$ brew cask install ngrok` on a mac), which will expose a local server to the internet.
|
||||
0. With the server still running, open a new terminal tab and run `ngrok http 3000`, which should output something like `Forwarding http://4397efc6.ngrok.io -> localhost:3000`.
|
||||
0. 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
|
||||
0. Run `$ ngrok http 3000`, which should output something like `Forwarding https://4397efc6.ngrok.io -> localhost:3000`
|
||||
0. [Register an integration](https://developer.github.com/early-access/integrations/creating-an-integration/) on GitHub with:
|
||||
- **Homepage URL**, **Callback URL**, and **Webhook URL**: The full ngrok url above. For example: `https://4397efc6.ngrok.io/`
|
||||
- **Secret:** `development`
|
||||
- **Permissions & events** needed will depend on how you use the bot, but for development it may be easiest to enable everything.
|
||||
0. Download the private key and move it to `private-key.pem` in the project directory
|
||||
0. Edit `.env` and fill in all the environment variables
|
||||
0. With `ngrok` still running, open another terminal and run `$ script/server` to start the server on http://localhost:3000
|
||||
|
||||
Whenever you com back to work on the app after you've already had it running once, then you need to:
|
||||
|
||||
0. Run `$ script/server`
|
||||
0. Run `$ ngrok http 3000`
|
||||
0. `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/installations) and update all the URLs.
|
||||
|
||||
## Testing
|
||||
To test with a real GitHub repository, you'll need to create a test repository and configure a new webhook:
|
||||
|
||||
0. Head over to the **Settings** page of your repository, and click on **Webhooks & services**. Then, click on **Add webhook**. Configure it with:
|
||||
- **Payload URL:** Use the full `*.ngrok.io`
|
||||
- **Secret:** `development`
|
||||
- **Which events would you like to trigger this webhook?:** Choose **Send me everything**.
|
||||
0. Create a `.probot.js` in your repo with:
|
||||
To test with a real GitHub repository, you'll need to create a test repository and install the integration you created above:
|
||||
|
||||
0. Open up the settings for your installation and click "Install"
|
||||
0. Create a `.probot.js` in your repository with:
|
||||
on("issues.opened").comment("Hello World! Your bot is working!");
|
||||
|
||||
0. Open a new issue. Your bot should post a comment (you may need to refresh to see it).
|
||||
|
||||
## Debugging
|
||||
|
||||
0. Always run `$ script/bootstrap` and restart the server if package.json has changed.
|
||||
0. To turn on verbose logging, start server by running ` $ DEBUG=Probot GITHUB_TOKEN=xxx script/server`
|
||||
0. To see what requests are going out, enable debugging mode for GitHub client in `/server.js`:
|
||||
|
|
|
@ -23,3 +23,10 @@ _**Heads up!** The [demo integration](https://github.com/integration/probot-demo
|
|||
`);
|
||||
|
||||
0. Open a new issue. @probot should post a comment (you may need to refresh to see it).
|
||||
|
||||
### Deploy your own bot to Heroku
|
||||
|
||||
0. [](https://heroku.com/deploy) - Click this button and pick an **App Name** that Heroku is happy with, like `your-name-probot`. Before you can complete this, you'll need config variables from the next step.
|
||||
0. In another tab, [create an integration](https://developer.github.com/early-access/integrations/creating-an-integration/) on GitHub, using `https://your-app-name.herokuapp.com/` as the **Homepage URL**, **Callback URL**, and **Webhook URL**. The permissions and events that your bot needs access to will depend on what you use it for.
|
||||
0. After creating your GitHub integration, go back to the Heroku tab and fill in the configuration variables with the values for the GitHub Integration
|
||||
0. Create a `.probot.yml` file in your repository. See [Configuring](#configuring).
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "PRobot",
|
||||
"description": "a trainable robot that responds to activity on GitHub",
|
||||
"keywords": ["PRobot", "node", "github"],
|
||||
"repository": "https://github.com/bkeepers/PRobot",
|
||||
"logo": "https://github.com/probot.png",
|
||||
"env": {
|
||||
"PRIVATE_KEY": {
|
||||
"description": "the private key you downloaded when creating the GitHub Integration"
|
||||
},
|
||||
"INTEGRATION_ID": {
|
||||
"description": "the ID of your GitHub Integration"
|
||||
},
|
||||
"WEBHOOK_SECRET": {
|
||||
"description": "the secret configured for your GitHub Integration"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -145,12 +145,6 @@ Add labels
|
|||
.unassign('defunkt');
|
||||
```
|
||||
|
||||
#### `react`
|
||||
|
||||
```
|
||||
.react('heart'); # or +1, -1, laugh, confused, heart, hooray
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
See [examples](examples.md) for ideas of behaviors you can implement by combining these configuration options.
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
const debug = require('debug')('PRobot');
|
||||
const GitHubApi = require('github');
|
||||
const jwt = require('./jwt');
|
||||
|
||||
module.exports = {auth};
|
||||
|
||||
const tokens = {};
|
||||
|
||||
// Authenticate as the given installation
|
||||
function auth(id) {
|
||||
const token = tokens[id];
|
||||
|
||||
if (!token || new Date(token.expires_at) < new Date()) {
|
||||
return createToken(id);
|
||||
} else {
|
||||
const github = new GitHubApi();
|
||||
github.authenticate({type: 'token', token: token.token});
|
||||
return Promise.resolve(github);
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.github.com/early-access/integrations/authentication/#as-an-installation
|
||||
function createToken(id) {
|
||||
const github = new GitHubApi();
|
||||
github.authenticate({type: 'integration', token: jwt()});
|
||||
|
||||
debug('creating token for installation', id);
|
||||
|
||||
return github.integrations.createInstallationToken({
|
||||
installation_id: id
|
||||
}).then(token => {
|
||||
// cache token
|
||||
tokens[id] = token;
|
||||
|
||||
github.authenticate({type: 'token', token: token.token});
|
||||
return github;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
const fs = require('fs');
|
||||
const process = require('process');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const cert = process.env.PRIVATE_KEY || fs.readFileSync('private-key.pem');
|
||||
|
||||
module.exports = generate;
|
||||
|
||||
function generate() {
|
||||
const payload = {
|
||||
// issued at time
|
||||
iat: Math.floor(new Date() / 1000),
|
||||
// JWT expiration time
|
||||
exp: Math.floor(new Date() / 1000) + 60,
|
||||
// Integration's GitHub identifier
|
||||
iss: process.env.INTEGRATION_ID
|
||||
};
|
||||
|
||||
// sign with RSA SHA256
|
||||
return jwt.sign(payload, cert, {algorithm: 'RS256'});
|
||||
}
|
|
@ -47,11 +47,6 @@ const IssuePlugin = superclass => class extends superclass {
|
|||
return this;
|
||||
}
|
||||
|
||||
react(reaction) {
|
||||
this._setCommentData({reaction});
|
||||
return this;
|
||||
}
|
||||
|
||||
_setCommentData(obj) {
|
||||
if (this.issueActions === undefined) {
|
||||
this.issueActions = {};
|
||||
|
@ -143,13 +138,6 @@ class IssueEvaluator extends Evaluator {
|
|||
);
|
||||
}
|
||||
|
||||
if (workflow.issueActions.reaction !== undefined) {
|
||||
const reaction = workflow.issueActions.reaction;
|
||||
promises.push(
|
||||
context.github.reactions.createForIssue(context.payload.toIssue({content: reaction}))
|
||||
);
|
||||
}
|
||||
|
||||
return promises;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
const debug = require('debug')('PRobot');
|
||||
const installations = require('./installations');
|
||||
const Configuration = require('./configuration');
|
||||
const Dispatcher = require('./dispatcher');
|
||||
|
||||
class Robot {
|
||||
listen(webhook) {
|
||||
webhook.on('*', this.receive.bind(this));
|
||||
}
|
||||
|
||||
receive(event) {
|
||||
debug('webhook', event);
|
||||
|
||||
if (event.payload.repository) {
|
||||
installations.auth(event.payload.installation.id).then(github => {
|
||||
const dispatcher = new Dispatcher(github, event);
|
||||
return Configuration.load(github, event.payload.repository).then(config => {
|
||||
dispatcher.call(config);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Robot();
|
|
@ -11,10 +11,12 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"debug": "2.2.0",
|
||||
"dotenv": "^2.0.0",
|
||||
"expect": "^1.20.2",
|
||||
"github": "^5.2.0",
|
||||
"github": "^6.0.4",
|
||||
"github-webhook-handler": "^0.6.0",
|
||||
"handlebars": "^4.0.5"
|
||||
"handlebars": "^4.0.5",
|
||||
"jsonwebtoken": "^7.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^3.0.2",
|
||||
|
@ -24,6 +26,7 @@
|
|||
"esnext": true,
|
||||
"space": true,
|
||||
"rules": {
|
||||
"camelcase": 1,
|
||||
"no-else-return": 0,
|
||||
"key-spacing": 0
|
||||
},
|
||||
|
|
|
@ -3,3 +3,6 @@
|
|||
set -e
|
||||
|
||||
npm install
|
||||
|
||||
# Copy .env template
|
||||
[[ -f .env ]] || cp .env.sample .env
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
#!/usr/bin/env node
|
||||
// Console for experimenting with GitHub API requests.
|
||||
//
|
||||
// Usage: GITHUB_TOKEN=xxx script/Console
|
||||
//
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
if (!process.env.GITHUB_TOKEN) {
|
||||
console.error('GITHUB_TOKEN environment variable must be set.');
|
||||
console.error('Create a personal access token at https://github.com/settings/tokens/new');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const repl = require('repl').start('> ');
|
||||
let GitHubApi = require('github');
|
||||
let github = new GitHubApi();
|
||||
const GitHubApi = require('github');
|
||||
const github = new GitHubApi();
|
||||
|
||||
github.authenticate({
|
||||
type: 'oauth',
|
||||
token: process.env.GITHUB_TOKEN
|
||||
});
|
||||
})
|
||||
|
||||
repl.context.github = github;
|
||||
|
|
49
server.js
49
server.js
|
@ -1,23 +1,16 @@
|
|||
require('dotenv').config({silent: true});
|
||||
|
||||
const process = require('process');
|
||||
const http = require('http');
|
||||
const createHandler = require('github-webhook-handler');
|
||||
const GitHubApi = require('github');
|
||||
const debug = require('debug')('PRobot');
|
||||
const Configuration = require('./lib/configuration');
|
||||
const Dispatcher = require('./lib/dispatcher');
|
||||
const robot = require('./lib/robot');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const webhook = createHandler({path: '/', secret: process.env.WEBHOOK_SECRET || 'development'});
|
||||
|
||||
debug('Starting');
|
||||
|
||||
const github = new GitHubApi({debug: false});
|
||||
|
||||
github.authenticate({
|
||||
type: 'token',
|
||||
token: process.env.GITHUB_TOKEN
|
||||
});
|
||||
|
||||
http.createServer((req, res) => {
|
||||
webhook(req, res, err => {
|
||||
if (err) {
|
||||
|
@ -31,39 +24,11 @@ http.createServer((req, res) => {
|
|||
});
|
||||
}).listen(PORT);
|
||||
|
||||
webhook.on('*', event => {
|
||||
debug('webhook', event);
|
||||
robot.listen(webhook);
|
||||
|
||||
if (event.payload.repository) {
|
||||
const dispatcher = new Dispatcher(github, event);
|
||||
return Configuration.load(github, event.payload.repository).then(
|
||||
config => dispatcher.call(config)
|
||||
);
|
||||
}
|
||||
// Show trace for any unhandled rejections
|
||||
process.on('unhandledRejection', reason => {
|
||||
console.error(reason);
|
||||
});
|
||||
|
||||
// Check for and accept any repository invitations
|
||||
function checkForInvites() {
|
||||
debug('Checking for repository invites');
|
||||
github.users.getRepoInvites({}).then(invites => {
|
||||
invites.forEach(invite => {
|
||||
debug('Accepting repository invite', invite.full_name);
|
||||
github.users.acceptRepoInvite(invite);
|
||||
});
|
||||
});
|
||||
|
||||
debug('Checking for organization invites');
|
||||
github.orgs.getOrganizationMemberships({state: 'pending'}).then(invites => {
|
||||
invites.forEach(invite => {
|
||||
debug('Accepting organization invite', invite.organization.login);
|
||||
github.users.editOrganizationMembership({
|
||||
org: invite.organization.login,
|
||||
state: 'active'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
checkForInvites();
|
||||
setInterval(checkForInvites, Number(process.env.INVITE_CHECK_INTERVAL || 60) * 1000);
|
||||
|
||||
console.log('Listening on http://localhost:' + PORT);
|
||||
|
|
|
@ -7,8 +7,7 @@ const createSpy = expect.createSpy;
|
|||
config.content = new Buffer(`
|
||||
on("issues.opened")
|
||||
.comment("Hello World!")
|
||||
.assign("bkeepers")
|
||||
.react("heart");
|
||||
.assign("bkeepers");
|
||||
|
||||
on("issues.closed")
|
||||
.unassign("bkeepers");
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"action": "deleted",
|
||||
"installation": {
|
||||
"id": 1729,
|
||||
"account": {
|
||||
"login": "bkeepers-inc",
|
||||
"id": 11724939,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11724939?v=3",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/bkeepers-inc",
|
||||
"html_url": "https://github.com/bkeepers-inc",
|
||||
"followers_url": "https://api.github.com/users/bkeepers-inc/followers",
|
||||
"following_url": "https://api.github.com/users/bkeepers-inc/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/bkeepers-inc/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/bkeepers-inc/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/bkeepers-inc/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/bkeepers-inc/orgs",
|
||||
"repos_url": "https://api.github.com/users/bkeepers-inc/repos",
|
||||
"events_url": "https://api.github.com/users/bkeepers-inc/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/bkeepers-inc/received_events",
|
||||
"type": "Organization",
|
||||
"site_admin": false
|
||||
},
|
||||
"access_tokens_url": "https://api.github.com/installations/1729/access_tokens",
|
||||
"repositories_url": "https://api.github.com/installation/repositories"
|
||||
},
|
||||
"sender": {
|
||||
"login": "bkeepers",
|
||||
"id": 173,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/173?v=3",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/bkeepers",
|
||||
"html_url": "https://github.com/bkeepers",
|
||||
"followers_url": "https://api.github.com/users/bkeepers/followers",
|
||||
"following_url": "https://api.github.com/users/bkeepers/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/bkeepers/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/bkeepers/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/bkeepers/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/bkeepers/orgs",
|
||||
"repos_url": "https://api.github.com/users/bkeepers/repos",
|
||||
"events_url": "https://api.github.com/users/bkeepers/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/bkeepers/received_events",
|
||||
"type": "User",
|
||||
"site_admin": true
|
||||
}
|
||||
}
|
|
@ -7,9 +7,6 @@ const payload = require('../fixtures/webhook/comment.created.json');
|
|||
const createSpy = expect.createSpy;
|
||||
|
||||
const github = {
|
||||
reactions: {
|
||||
createForIssue: createSpy()
|
||||
},
|
||||
issues: {
|
||||
lock: createSpy(),
|
||||
unlock: createSpy(),
|
||||
|
@ -210,18 +207,4 @@ describe('issues plugin', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reactions', () => {
|
||||
it('react', () => {
|
||||
this.w.react('heart');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.reactions.createForIssue).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
content: 'heart'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue