Reimplement workflow with new plugin system

This commit is contained in:
Brandon Keepers 2016-11-22 22:44:24 -06:00
parent 8dd579c7f4
commit 65eaf9300b
No known key found for this signature in database
GPG Key ID: F9533396D5FACBF6
6 changed files with 99 additions and 176 deletions

View File

@ -1,5 +1,4 @@
const Context = require('./context'); const Context = require('./context');
const issues = require('./plugins/issues');
class Dispatcher { class Dispatcher {
constructor(github, event) { constructor(github, event) {
@ -11,28 +10,8 @@ class Dispatcher {
// Get behaviors for the event // Get behaviors for the event
const context = new Context(this.github, config, this.event); const context = new Context(this.github, config, this.event);
// FIXME: have a better method to register evaluators
const evaluators = [
issues.Evaluator
];
// Handle all behaviors // Handle all behaviors
return Promise.all(config.workflows.map(w => { return Promise.all(config.workflows.map(w => w.execute(context)));
if (w.matches(this.event)) {
return evaluators.map(E => {
const evaluator = new E();
if (evaluator.checkIfEventApplies(context.event)) {
return evaluator.evaluate(w, context);
} else {
return [];
}
});
} else {
return false;
}
}).reduce((a, b) => {
return a.concat(b);
}, []));
} }
} }

View File

@ -1,114 +1,48 @@
const handlebars = require('handlebars'); const handlebars = require('handlebars');
const Evaluator = require('../evaluator');
const IssuePlugin = superclass => class extends superclass { module.exports = class IssuesPlugin {
comment(comment) { // checkIfEventApplies(event) {
this._setCommentData({comment}); // return event.issue !== undefined || event.pull_request !== undefined;
return this; // }
comment(context, content) {
const template = handlebars.compile(content)(context.payload);
return context.github.issues.createComment(context.payload.toIssue({body: template}));
} }
assign(...assignees) { assign(context, ...assignees) {
this._setCommentData({assign: assignees}); return context.github.issues.addAssigneesToIssue(context.payload.toIssue({assignees}));
return this;
} }
unassign(...assignees) { unassign(context, ...assignees) {
this._setCommentData({unassign: assignees}); return context.github.issues.removeAssigneesFromIssue(context.payload.toIssue({body: {assignees}}));
return this;
} }
label(...labels) { label(context, ...labels) {
this._setCommentData({label: labels}); return context.github.issues.addLabels(context.payload.toIssue({body: labels}));
return this;
} }
unlabel(...labels) { unlabel(context, ...labels) {
this._setCommentData({unlabel: labels});
return this;
}
lock() {
this._setCommentData({lock: true});
return this;
}
unlock() {
this._setCommentData({unlock: true});
return this;
}
open() {
this._setCommentData({open: true});
return this;
}
close() {
this._setCommentData({close: true});
return this;
}
_setCommentData(obj) {
if (this.issueActions === undefined) {
this.issueActions = {};
}
Object.assign(this.issueActions, obj);
}
};
// This is the function that implements all of the actions configured above.
class IssueEvaluator extends Evaluator {
checkIfEventApplies(event) {
return event.issue !== undefined || event.pull_request !== undefined;
}
comment(content) {
const template = handlebars.compile(content)(this.payload);
return this.github.issues.createComment(this.payload.toIssue({body: template}));
}
assign(assignees) {
return this.github.issues.addAssigneesToIssue(this.payload.toIssue({assignees}));
}
unassign(assignees) {
return this.github.issues.removeAssigneesFromIssue(this.payload.toIssue({body: {assignees}}));
}
label(labels) {
return this.github.issues.addLabels(this.payload.toIssue({body: labels}));
}
unlabel(labels) {
return labels.map(label => { return labels.map(label => {
return this.github.issues.removeLabel( return context.github.issues.removeLabel(
this.payload.toIssue({name: label}) context.payload.toIssue({name: label})
); );
}); });
} }
lock() { lock(context) {
return this.github.issues.lock(this.payload.toIssue({})); return context.github.issues.lock(context.payload.toIssue({}));
} }
unlock() { unlock(context) {
return this.github.issues.unlock(this.payload.toIssue({})); return context.github.issues.unlock(context.payload.toIssue({}));
} }
open() { open(context) {
return this.github.issues.edit(this.payload.toIssue({state: 'open'})); return context.github.issues.edit(context.payload.toIssue({state: 'open'}));
} }
close() { close(context) {
return this.github.issues.edit(this.payload.toIssue({state: 'closed'})); return context.github.issues.edit(context.payload.toIssue({state: 'closed'}));
} }
pluginData(workflow) {
return workflow.issueActions;
}
}
module.exports = {
Plugin: IssuePlugin,
Evaluator: IssueEvaluator
}; };

View File

@ -14,7 +14,7 @@ class Sandbox {
on(...events) { on(...events) {
const workflow = new Workflow(events); const workflow = new Workflow(events);
this.workflows.push(workflow); this.workflows.push(workflow);
return workflow; return workflow.api;
} }
execute() { execute() {

View File

@ -1,14 +1,33 @@
const issues = require('./plugins/issues'); const Issues = require('./plugins/issues');
class WorkflowCore { const plugins = [
new Issues()
];
module.exports = class Workflow {
constructor(events) { constructor(events) {
this.stack = [];
this.events = events; this.events = events;
this.filterFn = () => true; this.filterFn = () => true;
this.api = {};
for (const plugin of plugins) {
// Get all the property names of the plugin
for (const method of Object.getOwnPropertyNames(plugin.constructor.prototype)) {
if (method !== 'constructor') {
// Define a new function in the API for this plugin method, forcing
// the binding to this to prevent any tampering.
this.api[method] = this.proxy(plugin, method).bind(this);
}
}
}
this.api.filter = this.filter.bind(this);
} }
filter(fn) { filter(fn) {
this.filterFn = fn; this.filterFn = fn;
return this; return this.api;
} }
matches(event) { matches(event) {
@ -17,18 +36,24 @@ class WorkflowCore {
return name === event.event && (!action || action === event.payload.action); return name === event.event && (!action || action === event.payload.action);
}) && this.filterFn(event); }) && this.filterFn(event);
} }
}
// FIXME: issues proxy(plugin, method) {
const plugins = [ // This function is what gets exposed to the sandbox
issues.Plugin return (...args) => {
]; // Push new function on the stack that calls the plugin method with a context.
this.stack.push(context => plugin[method](context, ...args));
// Helper to combine an array of mixins into one class // Return the API to allow methods to be chained.
function mix(superclass, ...mixins) { return this.api;
return mixins.reduce((c, mixin) => mixin(c), superclass); };
} }
class Workflow extends mix(WorkflowCore, ...plugins) {} execute(context) {
if (this.matches(context.event)) {
module.exports = Workflow; // Reduce the stack to a chain of promises, each called with the given context
this.stack.reduce((promise, func) => {
return promise.then(func.bind(func, context));
}, Promise.resolve());
}
}
};

View File

@ -40,7 +40,7 @@ class Workflow {
this.stack = []; this.stack = [];
this.api = {}; this.api = {};
for(const plugin of PLUGINS) { for (const plugin of PLUGINS) {
// Get all the property names of the plugin // Get all the property names of the plugin
for (const method of Object.getOwnPropertyNames(plugin.constructor.prototype)) { for (const method of Object.getOwnPropertyNames(plugin.constructor.prototype)) {
if (method !== 'constructor') { if (method !== 'constructor') {

View File

@ -1,36 +1,34 @@
const expect = require('expect'); const expect = require('expect');
const issues = require('../../lib/plugins/issues'); const Issues = require('../../lib/plugins/issues');
const Workflow = require('../../lib/workflow');
const Context = require('../../lib/context'); const Context = require('../../lib/context');
const payload = require('../fixtures/webhook/comment.created.json'); const payload = require('../fixtures/webhook/comment.created.json');
const createSpy = expect.createSpy; const createSpy = expect.createSpy;
const github = {
issues: {
lock: createSpy(),
unlock: createSpy(),
edit: createSpy(),
addLabels: createSpy(),
createComment: createSpy(),
addAssigneesToIssue: createSpy(),
removeAssigneesFromIssue: createSpy(),
removeLabel: createSpy()
}
};
const context = new Context(github, {}, {payload});
describe('issues plugin', () => { describe('issues plugin', () => {
const github = {
issues: {
lock: createSpy().andReturn(Promise.resolve()),
unlock: createSpy().andReturn(Promise.resolve()),
edit: createSpy().andReturn(Promise.resolve()),
addLabels: createSpy().andReturn(Promise.resolve()),
createComment: createSpy().andReturn(Promise.resolve()),
addAssigneesToIssue: createSpy().andReturn(Promise.resolve()),
removeAssigneesFromIssue: createSpy().andReturn(Promise.resolve()),
removeLabel: createSpy().andReturn(Promise.resolve())
}
};
const context = new Context(github, {}, {payload});
before(() => { before(() => {
this.w = new Workflow(); this.issues = new Issues();
this.evaluator = new issues.Evaluator();
}); });
describe('locking', () => { describe('locking', () => {
it('locks', () => { it('locks', () => {
this.w.lock(); this.issues.lock(context);
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.lock).toHaveBeenCalledWith({ expect(github.issues.lock).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -39,9 +37,8 @@ describe('issues plugin', () => {
}); });
it('unlocks', () => { it('unlocks', () => {
this.w.unlock(); this.issues.unlock(context);
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.unlock).toHaveBeenCalledWith({ expect(github.issues.unlock).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -52,9 +49,8 @@ describe('issues plugin', () => {
describe('state', () => { describe('state', () => {
it('opens an issue', () => { it('opens an issue', () => {
this.w.open(); this.issues.open(context);
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.edit).toHaveBeenCalledWith({ expect(github.issues.edit).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -63,9 +59,8 @@ describe('issues plugin', () => {
}); });
}); });
it('closes an issue', () => { it('closes an issue', () => {
this.w.close(); this.issues.close(context);
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.edit).toHaveBeenCalledWith({ expect(github.issues.edit).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -77,9 +72,8 @@ describe('issues plugin', () => {
describe('labels', () => { describe('labels', () => {
it('adds a label', () => { it('adds a label', () => {
this.w.label('hello'); this.issues.label(context, 'hello');
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.addLabels).toHaveBeenCalledWith({ expect(github.issues.addLabels).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -89,9 +83,8 @@ describe('issues plugin', () => {
}); });
it('adds multiple labels', () => { it('adds multiple labels', () => {
this.w.label('hello', 'world'); this.issues.label(context, 'hello', 'world');
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.addLabels).toHaveBeenCalledWith({ expect(github.issues.addLabels).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -101,9 +94,8 @@ describe('issues plugin', () => {
}); });
it('removes a single label', () => { it('removes a single label', () => {
this.w.unlabel('hello'); this.issues.unlabel(context, 'hello');
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.removeLabel).toHaveBeenCalledWith({ expect(github.issues.removeLabel).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -113,9 +105,8 @@ describe('issues plugin', () => {
}); });
it('removes a multiple labels', () => { it('removes a multiple labels', () => {
this.w.unlabel('hello', 'goodbye'); this.issues.unlabel(context, 'hello', 'goodbye');
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.removeLabel).toHaveBeenCalledWith({ expect(github.issues.removeLabel).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -134,9 +125,8 @@ describe('issues plugin', () => {
describe('comments', () => { describe('comments', () => {
it('creates a comment', () => { it('creates a comment', () => {
this.w.comment('Hello world!'); this.issues.comment(context, 'Hello world!');
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.createComment).toHaveBeenCalledWith({ expect(github.issues.createComment).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -146,9 +136,8 @@ describe('issues plugin', () => {
}); });
it('evaluates templates with handlebars', () => { it('evaluates templates with handlebars', () => {
this.w.comment('Hello @{{ sender.login }}!'); this.issues.comment(context, 'Hello @{{ sender.login }}!');
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.createComment).toHaveBeenCalledWith({ expect(github.issues.createComment).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -160,9 +149,8 @@ describe('issues plugin', () => {
describe('assignment', () => { describe('assignment', () => {
it('assigns a user', () => { it('assigns a user', () => {
this.w.assign('bkeepers'); this.issues.assign(context, 'bkeepers');
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({ expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -172,9 +160,8 @@ describe('issues plugin', () => {
}); });
it('assigns multiple users', () => { it('assigns multiple users', () => {
this.w.assign('hello', 'world'); this.issues.assign(context, 'hello', 'world');
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({ expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -184,9 +171,8 @@ describe('issues plugin', () => {
}); });
it('unassigns a user', () => { it('unassigns a user', () => {
this.w.unassign('bkeepers'); this.issues.unassign(context, 'bkeepers');
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({ expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',
@ -196,9 +182,8 @@ describe('issues plugin', () => {
}); });
it('unassigns multiple users', () => { it('unassigns multiple users', () => {
this.w.unassign('hello', 'world'); this.issues.unassign(context, 'hello', 'world');
Promise.all(this.evaluator.evaluate(this.w, context));
expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({ expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({
owner: 'bkeepers-inc', owner: 'bkeepers-inc',
repo: 'test', repo: 'test',