Merge pull request #67 from bkeepers/load

Load configuration from another file
This commit is contained in:
Brandon Keepers 2016-12-04 16:40:12 -06:00 committed by GitHub
commit 249809f9d2
9 changed files with 157 additions and 63 deletions

View File

@ -77,7 +77,7 @@ Only preform the actions if the function returns `true`. The `event` is passed a
.filter(event => event.payload.issue.body.includes("- [ ]"))
```
#### `comment`
### `comment`
Comments can be posted in response to any event performed on an Issue or Pull Request. Comments use [mustache](https://mustache.github.io/) for templates and can use any data from the event payload.
@ -85,7 +85,7 @@ Comments can be posted in response to any event performed on an Issue or Pull Re
.comment("Hey @{{ user.login }}, thanks for the contribution!");
```
#### `close`
### `close`
Close an issue or pull request.
@ -93,7 +93,7 @@ Close an issue or pull request.
.close();
```
#### `open`
### `open`
Reopen an issue or pull request.
@ -101,7 +101,7 @@ Reopen an issue or pull request.
.open();
```
#### `lock`
### `lock`
Lock conversation on an issue or pull request.
@ -109,7 +109,7 @@ Lock conversation on an issue or pull request.
.lock();
```
#### `unlock`
### `unlock`
Unlock conversation on an issue or pull request.
@ -117,7 +117,7 @@ Unlock conversation on an issue or pull request.
.unlock();
```
#### `label`
### `label`
Add labels
@ -125,7 +125,7 @@ Add labels
.label('bug');
```
#### `unlabel`
### `unlabel`
Add labels
@ -133,18 +133,33 @@ Add labels
.unlabel('needs-work').label('waiting-for-review');
```
#### `assign`
### `assign`
```
.assign('hubot');
```
#### `unassign`
### `unassign`
```
.unassign('defunkt');
```
## include
Loads a configuration from another file.
```js
include('.github/bot/issues.js');
include('.github/bot/releases.js');
```
You can also include configuration from another repository.
```js
include('user/repo:path.js#branch');
```
---
See [examples](examples.md) for ideas of behaviors you can implement by combining these configuration options.

View File

@ -1,32 +1,24 @@
const debug = require('debug')('PRobot');
const Sandbox = require('./sandbox');
const Workflow = require('./workflow');
const url = require('./util/github-url');
module.exports = class Configuration {
// Get bot config from target repository
static load(github, repository) {
debug('Fetching .probot.js from %s', repository.full_name);
const parts = repository.full_name.split('/');
return github.repos.getContent({
owner: parts[0],
repo: parts[1],
path: '.probot.js'
}).then(data => {
const content = new Buffer(data.content, 'base64').toString();
debug('Configuration fetched', content);
return Configuration.parse(content);
static load(context, path) {
const options = url(path);
debug('Fetching %s from %s', path, context.payload.repository.full_name);
return context.github.repos.getContent(context.toRepo(options)).then(data => {
return new Configuration(context).parse(new Buffer(data.content, 'base64').toString());
});
}
static parse(content) {
return new Configuration().parse(content);
}
constructor() {
constructor(context) {
this.context = context;
this.workflows = [];
this.api = {
on: this.on.bind(this)
on: this.on.bind(this),
include: this.include.bind(this)
};
}
@ -36,12 +28,24 @@ module.exports = class Configuration {
return workflow.api;
}
include(path) {
const load = Configuration.load(this.context, path);
this.workflows.push({
execute() {
return load.then(config => config.execute(this.context));
}
});
return undefined;
}
parse(content) {
new Sandbox(content).execute(this.api);
return this;
}
execute(context) {
return Promise.all(this.workflows.map(w => w.execute(context)));
execute() {
return Promise.all(this.workflows.map(w => w.execute(this.context)));
}
};

View File

@ -8,15 +8,15 @@ module.exports = class Context {
}
toRepo(object) {
return Object.assign({}, object, {
return Object.assign({
owner: this.payload.repository.owner.login,
repo: this.payload.repository.name
});
}, object);
}
toIssue(object) {
return Object.assign({}, object, {
return Object.assign({
number: (this.payload.issue || this.payload.pull_request || this.payload).number
}, this.toRepo());
}, this.toRepo(), object);
}
};

View File

@ -1,6 +1,6 @@
const debug = require('debug')('PRobot');
const installations = require('./installations');
const Context = require('./configuration');
const Context = require('./context');
const Configuration = require('./configuration');
class Robot {
@ -14,9 +14,8 @@ class Robot {
if (event.payload.repository) {
installations.auth(event.payload.installation.id).then(github => {
const context = new Context(github, event);
return Configuration.load(github, event.payload.repository).then(config => {
return config.execute(context);
Configuration.load(context, '.probot.js').then(config => {
return config.execute();
});
});
}

7
lib/util/github-url.js Normal file
View File

@ -0,0 +1,7 @@
const REGEX = /^(?:([\w-]+)\/([\w-]+):)?([^#]*)(?:#(.*))?$/;
// Parses paths in the form of `owner/repo:path/to/file#ref`
module.exports = function (url) {
const [, owner, repo, path, ref] = url.match(REGEX);
return Object.assign({path}, owner && {owner, repo}, ref && {ref});
};

View File

@ -1,10 +1,12 @@
const expect = require('expect');
const Configuration = require('../lib/configuration');
const config = require('./fixtures/content/probot.json');
const Context = require('../lib/context');
const content = require('./fixtures/content/probot.json');
const payload = require('./fixtures/webhook/comment.created');
const createSpy = expect.createSpy;
config.content = new Buffer(`
content.content = new Buffer(`
on("issues.opened")
.comment("Hello World!")
.assign("bkeepers");
@ -14,30 +16,41 @@ config.content = new Buffer(`
`).toString('base64');
describe('Configuration', () => {
describe('load', () => {
describe('include', () => {
let github;
const repo = JSON.parse('{"full_name": "bkeepers/test"}');
let context;
let config;
beforeEach(() => {
github = {
repos: {
getContent: createSpy().andReturn(Promise.resolve(config))
getContent: createSpy().andReturn(Promise.resolve(content))
}
};
context = new Context(github, {payload});
config = new Configuration(context);
});
it('loads from the repo', done => {
Configuration.load(github, repo).then(config => {
expect(github.repos.getContent).toHaveBeenCalledWith({
owner: 'bkeepers',
repo: 'test',
path: '.probot.js'
});
it('includes from the repo', () => {
config.include('foo.js');
expect(github.repos.getContent).toHaveBeenCalledWith({
owner: 'bkeepers-inc',
repo: 'test',
path: 'foo.js'
});
});
expect(config.workflows.length).toEqual(2);
it('returns undefined', () => {
expect(config.include('foo.js')).toBe(undefined);
});
done();
it('includes from another repository', () => {
config.include('atom/configs:foo.js#branch');
expect(github.repos.getContent).toHaveBeenCalledWith({
owner: 'atom',
repo: 'configs',
path: 'foo.js',
ref: 'branch'
});
});
});

View File

@ -25,9 +25,9 @@ describe('Context', () => {
});
});
it('does not override repo attributes', () => {
it('overrides repo attributes', () => {
expect(context.toRepo({owner: 'muahaha'})).toEqual({
owner: 'bkeepers', repo:'probot'
owner: 'muahaha', repo:'probot'
});
});
});
@ -43,9 +43,9 @@ describe('Context', () => {
});
});
it('does not override repo attributes', () => {
it('overrides repo attributes', () => {
expect(context.toIssue({owner: 'muahaha', number: 5})).toEqual({
owner: 'bkeepers', repo:'probot', number: 4
owner: 'muahaha', repo:'probot', number: 5
});
});
});

View File

@ -5,12 +5,12 @@ const payload = require('./fixtures/webhook/comment.created.json');
const createSpy = expect.createSpy;
describe('dispatch', () => {
describe('integration', () => {
const event = {event: 'issues', payload, issue: {}};
let context;
let github;
beforeEach(() => {
const event = {event: 'issues', payload, issue: {}};
github = {
issues: {
createComment: createSpy().andReturn(Promise.resolve()),
@ -21,9 +21,13 @@ describe('dispatch', () => {
context = new Context(github, event);
});
function configure(content) {
return new Configuration(context).parse(content);
}
describe('reply to new issue with a comment', () => {
it('posts a coment', () => {
const config = Configuration.parse('on("issues").comment("Hello World!")');
const config = configure('on("issues").comment("Hello World!")');
return config.execute(context).then(() => {
expect(github.issues.createComment).toHaveBeenCalled();
});
@ -32,7 +36,7 @@ describe('dispatch', () => {
describe('reply to new issue with a comment', () => {
it('calls the action', () => {
const config = Configuration.parse('on("issues.created").comment("Hello World!")');
const config = configure('on("issues.created").comment("Hello World!")');
return config.execute(context).then(() => {
expect(github.issues.createComment).toHaveBeenCalled();
@ -42,7 +46,7 @@ describe('dispatch', () => {
describe('on an event with a different action', () => {
it('does not perform behavior', () => {
const config = Configuration.parse('on("issues.labeled").comment("Hello World!")');
const config = configure('on("issues.labeled").comment("Hello World!")');
return config.execute(context).then(() => {
expect(github.issues.createComment).toNotHaveBeenCalled();
@ -59,17 +63,51 @@ describe('dispatch', () => {
});
it('calls action when condition matches', () => {
const config = Configuration.parse('on("issues.labeled").filter((e) => e.payload.label.name == "bug").close()');
const config = configure('on("issues.labeled").filter((e) => e.payload.label.name == "bug").close()');
return config.execute(context).then(() => {
expect(github.issues.edit).toHaveBeenCalled();
});
});
it('does not call action when conditions do not match', () => {
const config = Configuration.parse('on("issues.labeled").filter((e) => e.payload.label.name == "foobar").close()');
const config = configure('on("issues.labeled").filter((e) => e.payload.label.name == "foobar").close()');
return config.execute(context).then(() => {
expect(github.issues.edit).toNotHaveBeenCalled();
});
});
});
describe('include', () => {
beforeEach(() => {
const content = require('./fixtures/content/probot.json');
content.content = new Buffer('on("issues").comment("Hello!");').toString('base64');
github = {
repos: {
getContent: createSpy().andReturn(Promise.resolve(content))
},
issues: {
createComment: createSpy()
}
};
context = new Context(github, event);
});
it('includes a file in the local repository', () => {
configure('include(".github/triage.js");');
expect(github.repos.getContent).toHaveBeenCalledWith({
owner: 'bkeepers-inc',
repo: 'test',
path: '.github/triage.js'
});
});
it('executes included rules', done => {
configure('include(".github/triage.js");').execute().then(() => {
expect(github.issues.createComment).toHaveBeenCalled();
done();
});
});
});
});

18
test/util/github-url.js Normal file
View File

@ -0,0 +1,18 @@
const expect = require('expect');
const url = require('../../lib/util/github-url');
describe('github-url', () => {
const cases = {
'path.js': {path: 'path.js'},
'owner/repo:path.js': {owner: 'owner', repo: 'repo', path: 'path.js'},
'owner/repo:path/to/file.js': {owner: 'owner', repo: 'repo', path: 'path/to/file.js'},
'path.js#ref': {path: 'path.js', ref: 'ref'},
'owner/repo:path.js#ref': {owner: 'owner', repo: 'repo', path: 'path.js', ref: 'ref'}
};
Object.keys(cases).forEach(path => {
it(path, () => {
expect(url(path)).toEqual(cases[path]);
});
});
});