From 6ddb997b7c06eebfeb02225718144c003802384a Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Tue, 15 Nov 2016 22:42:17 -0600 Subject: [PATCH 01/29] Experiment with examples as pure JS --- docs/examples.md | 107 ++++++++++++++++++++++++++++------------------- 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 90498f2..f72ac18 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -6,75 +6,96 @@ Here are some examples of interesting things you can do by combining these compo ### Require use of issue template - # .github/MISSING_ISSUE_TEMPLATE_AUTOREPLY.md - # - # Hey @{{ sender.login }}, thanks for opening an issue. Unfortunately, you - # are missing information from the issue template. Please open a new issue with - # all the information from the template and it will make it easier for us to - # help you. +```js +// .github/MISSING_ISSUE_TEMPLATE_AUTOREPLY.md +// +// Hey @{{ sender.login }}, thanks for opening an issue. Unfortunately, you +// are missing information from the issue template. Please open a new issue with +// all the information from the template and it will make it easier for us to +// help you. - on issues.opened - if - payload.body !matches /### Prerequisites.*### Description.*### Steps to Reproduce.*### Versions/ - or payload.body matches /- [ ]/ - then - comment(from_file(".github/MISSING_ISSUE_TEMPLATE_AUTOREPLY.md") - and label("insufficient-info") - and close; +on("issues.opened") + .filter(() => { + return !this.issue.body.match(/### Steps to Reproduce/) + || this.issue.body.includes("- [ ]") + }) + .comment.contents(".github/MISSING_ISSUE_TEMPLATE_AUTOREPLY.md") + .label("insufficient-info") + .close(); +``` ### Post welcome message for new contributors - on issues.opened or pull_request.opened - if first_time_contributor # plugins could implement conditions like this - then comment(file(".github/NEW_CONTRIBUTOR_TEMPLATE.md")); +```js +on("issues.opened", "pull_request.opened") + .filter.firstTimeContributor() // plugins could implement conditions like this + .comment.contents(".github/NEW_CONTRIBUTOR_TEMPLATE.md"); +``` ### Auto-close new pull requests - on pull_request.opened - then comment("Sorry @{{ user.login }}, pull requests are not accepted on this repository.") - and close; +```js +on("pull_request.opened") + .comment("Sorry @{{ user.login }}, pull requests are not accepted on this repository.") + .close(); +``` ### Close issues with no body - on issues.opened - if @issue.body matches /^$/ - then comment("Hey @{{ user.login }}, you didn't include a description of the problem, so we're closing this issue."); +```js +on("issues.opened") + .filter(() => this.issue.body.match(/^$/)) + .comment("Hey @{{ user.login }}, you didn't include a description of the problem, so we're closing this issue."); +``` ### @mention watchers when label added - on *.labeled then - # TODO: figure out syntax for loading watchers from file - comment("Hey {{ mentions }}, you wanted to know when the `{{ payload.label.name }}` label was added."); +```js +on("*.labeled") + // TODO: figure out syntax for loading watchers from file + .comment("Hey {{ mentions }}, you wanted to know when the `{{ payload.label.name }}` label was added."); +``` ### Assign a reviewer for new bugs - on pull_request.labeled - if labeled(bug) - then assign(random(file(OWNERS))); +```js +on("pull_request.labeled") + .filter(() => this.labeled(bug)) + .assign(random(file(OWNERS))); +``` ### Perform actions based on content of comments - on issue_comment.opened - if @issue.body matches /^@probot assign @(\w+)$/ - then assign({{ matches[0] }}) +```js +on("issue_comment.opened") + .filter(() => this.issue.body.match(/^@probot assign @(\w+)$/)) + .assign({{ matches[0] }}); - on issue_comment.opened - if @issue.body matches /^@probot label @(\w+)$/ - then label($1) +on("issue_comment.opened") + .filter(() => this.issue.body.match(/^@probot label @(\w+)$/)) + .label($1); +``` ### Close stale issues and pull requests - on *.labeled - if labeled("needs-work") and state("open") - then delay(7 days) and close +```js +every("day") + .find.issues({state: "open", label: "needs-work"}) + .filter.lastActive(7, "days") + .close(); +``` ### Tweet when a new release is created - on release.published - then tweet("Get it while it's hot! {{ repository.name }} {{ release.name }} was just released! {{ release.html_url }}") +```js +on("release.published") + .tweet("Get it while it's hot! {{ repository.name }} {{ release.name }} was just released! {{ release.html_url }}"); +``` ### Assign a reviewer issues or pull requests with a label - on issues.opened and pull_request.opened and issues.labeled and pull_request.labeled - if labeled(security) - then assign(random(members(security-first-responders)); +```js +on("issues.opened", "pull_request.opened", "issues.labeled", "pull_request.labeled") + .filter.labeled("security") + .assign(team("security-first-responders").random()); +``` From b58d51d4df5d143a94a0e5abbfa1fd035374f4ff Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Wed, 16 Nov 2016 14:36:25 -0800 Subject: [PATCH 02/29] Update docs, js arrow functions don't have `this` --- docs/examples.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index f72ac18..751188f 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -15,9 +15,9 @@ Here are some examples of interesting things you can do by combining these compo // help you. on("issues.opened") - .filter(() => { - return !this.issue.body.match(/### Steps to Reproduce/) - || this.issue.body.includes("- [ ]") + .filter((event) => { + return !event.issue.body.match(/### Steps to Reproduce/) + || event.issue.body.includes("- [ ]") }) .comment.contents(".github/MISSING_ISSUE_TEMPLATE_AUTOREPLY.md") .label("insufficient-info") @@ -44,7 +44,7 @@ on("pull_request.opened") ```js on("issues.opened") - .filter(() => this.issue.body.match(/^$/)) + .filter((event) => event.issue.body.match(/^$/)) .comment("Hey @{{ user.login }}, you didn't include a description of the problem, so we're closing this issue."); ``` @@ -60,7 +60,7 @@ on("*.labeled") ```js on("pull_request.labeled") - .filter(() => this.labeled(bug)) + .filter((event) => event.labeled(bug)) .assign(random(file(OWNERS))); ``` @@ -68,11 +68,11 @@ on("pull_request.labeled") ```js on("issue_comment.opened") - .filter(() => this.issue.body.match(/^@probot assign @(\w+)$/)) + .filter((event) => event.issue.body.match(/^@probot assign @(\w+)$/)) .assign({{ matches[0] }}); on("issue_comment.opened") - .filter(() => this.issue.body.match(/^@probot label @(\w+)$/)) + .filter((event) => event.issue.body.match(/^@probot label @(\w+)$/)) .label($1); ``` From 484bdfe19d8eea5976e9a6743955a457ab94c79b Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Wed, 16 Nov 2016 16:57:41 -0800 Subject: [PATCH 03/29] First working version of purejs --- hack.js | 22 ++++++ lib/attribute.js | 5 -- lib/binary-expression.js | 24 ------ lib/configuration.js | 65 ++++++++++------ lib/dispatcher.js | 15 +++- lib/parser.js | 21 ----- lib/parser.pegjs | 160 --------------------------------------- lib/plugins/base.js | 27 +++++++ lib/plugins/issues.js | 71 +++++++++++++++++ lib/transformer.js | 71 ----------------- lib/utils/url.js | 6 ++ lib/workflow.js | 29 +++++++ 12 files changed, 207 insertions(+), 309 deletions(-) create mode 100644 hack.js delete mode 100644 lib/attribute.js delete mode 100644 lib/binary-expression.js delete mode 100644 lib/parser.js delete mode 100644 lib/parser.pegjs create mode 100644 lib/plugins/base.js create mode 100644 lib/plugins/issues.js delete mode 100644 lib/transformer.js create mode 100644 lib/utils/url.js create mode 100644 lib/workflow.js diff --git a/hack.js b/hack.js new file mode 100644 index 0000000..53c1b80 --- /dev/null +++ b/hack.js @@ -0,0 +1,22 @@ +// THIS IS A GROSS HACK TO BYPASS THE NEED FOR A WEBHOOKS SETUP +// TO BE REMOVED + +const Configuration = require('./lib/configuration'); +const Dispatcher = require('./lib/dispatcher'); +const GitHubApi = require('github'); +const github = new GitHubApi({debug: false}); + +event = { + payload: { + repository: "", + }, + + issue : { + id: 1, + title: "Issue without steps", + body: "how do I use this thing?", + } +} +const dispatcher = new Dispatcher(github, event); +let config = Configuration.load(github, event.payload.repository) +dispatcher.call(config) diff --git a/lib/attribute.js b/lib/attribute.js deleted file mode 100644 index 5c52884..0000000 --- a/lib/attribute.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = (context, name) => { - return name.reduce((object, attr) => { - return object ? object[attr] : null; - }, context.payload); -}; diff --git a/lib/binary-expression.js b/lib/binary-expression.js deleted file mode 100644 index e880120..0000000 --- a/lib/binary-expression.js +++ /dev/null @@ -1,24 +0,0 @@ -const operators = { - 'is': (left, right) => left === right, - 'is not': (left, right) => left !== right, - 'contains': (left, right) => left.includes(right), - 'does not contain': (left, right) => !left.includes(right), - 'matches': (left, right) => left.match(right), - 'does not match': (left, right) => !left.match(right), - 'or': (left, right) => left || right, - 'and': (left, right) => left && right -}; - -function resolve(value, context) { - if (typeof value === 'function') { - return value(context); - } else { - return value; - } -} - -module.exports = (context, left, operator, right) => { - const leftValue = resolve(left, context); - const rightValue = resolve(right, context); - return operators[operator](leftValue, rightValue); -}; diff --git a/lib/configuration.js b/lib/configuration.js index cbb3f3f..070f405 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -1,39 +1,54 @@ const debug = require('debug')('PRobot'); -const Transformer = require('./transformer'); -const parser = require('./parser'); +const vm = require('vm'); + +const workflow = require('./workflow'); +const URL = require('./utils/url'); module.exports = class Configuration { // Get bot config from target repository static load(github, repository) { debug('Fetching .probot from %s', repository.full_name); - const parts = repository.full_name.split('/'); - return github.repos.getContent({ - owner: parts[0], - repo: parts[1], - path: '.probot' - }).then(data => { - const content = new Buffer(data.content, 'base64').toString(); - debug('Configuration fetched', content); - return Configuration.parse(content); - }); + // HACK: This needs to be reinstated once things are more settled + //const parts = repository.full_name.split('/'); + //return github.repos.getContent({ + // owner: parts[0], + // repo: parts[1], + // path: '.probot' + //}).then(data => { + // const content = new Buffer(data.content, 'base64').toString(); + // debug('Configuration fetched', content); + // return Configuration.parse(content); + //}); + let s = ` + workflows.push(on("issues.opened") + .filter((event) => { + return !event.issue.body.match(/### Steps to Reproduce/) + || event.issue.body.includes("- [ ]") + }) + .comment(new URL(".github/MISSING_ISSUE_TEMPLATE_AUTOREPLY.md")) + .label("insufficient-info") + .close() + ); + ` + return Configuration.parse(s) } - static parse(content) { - const transformer = new Transformer(parser.parse(content)); - return new Configuration(transformer.transform()); + static parse(content){ + const sandbox = { + on: workflow.on, + URL: URL, + workflows: [], + } + vm.createContext(sandbox); + vm.runInContext(content, sandbox); + return new Configuration(sandbox.workflows); } - constructor(behaviors) { - this.behaviors = behaviors; + constructor(workflows) { + this.workflows = workflows; } - // FIXME: this can be moved into Behavior as a condition - behaviorsFor(event) { - return this.behaviors.filter(behavior => { - return behavior.events.filter(e => { - return e.name === event.event && - (!e.action || e.action === event.payload.action); - }).length > 0; - }); + workflowsFor(event) { + return this.workflows.filter((w) => w.filterFn(event)); } }; diff --git a/lib/dispatcher.js b/lib/dispatcher.js index 63d9755..efd15d4 100644 --- a/lib/dispatcher.js +++ b/lib/dispatcher.js @@ -1,4 +1,5 @@ const Context = require('./context'); +const issues = require('./plugins/issues'); class Dispatcher { constructor(github, event) { @@ -8,12 +9,20 @@ class Dispatcher { call(config) { // Get behaviors for the event - const behaviors = config.behaviorsFor(this.event); const context = new Context(this.github, config, this.event); + const workflows = config.workflowsFor(this.event); + + // FIXME: have a better method to register evaluators + let evaluators = [ + issues.Evaluator, + ] // Handle all behaviors - return Promise.all(behaviors.map(behavior => { - return behavior.perform(context); + return Promise.all(workflows.map(w => { + evaluators.forEach((e) => { + let evaluator = new e; + return evaluator.evaluate(w, context); + }) })); } } diff --git a/lib/parser.js b/lib/parser.js deleted file mode 100644 index 295ec69..0000000 --- a/lib/parser.js +++ /dev/null @@ -1,21 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const PEG = require('pegjs'); -const PEGUtil = require('pegjs-util'); - -// FIXME: eventually this should be generated on release -const parser = PEG.generate(fs.readFileSync(path.join(__dirname, 'parser.pegjs'), 'utf8'), { - allowedStartRules: ['start', 'if'] -}); - -module.exports = { - parse: (content, options) => { - const result = PEGUtil.parse(parser, content, options); - - if (result.error) { - throw new Error('\n' + PEGUtil.errorMessage(result.error, true), null, result.error.line); - } else { - return result.ast; - } - } -}; diff --git a/lib/parser.pegjs b/lib/parser.pegjs deleted file mode 100644 index 60d8a89..0000000 --- a/lib/parser.pegjs +++ /dev/null @@ -1,160 +0,0 @@ -{ - var unroll = options.util.makeUnroll(location, options) - - // Helper Functions - - function binaryExpression(head, tail) { - return tail.reduce(function(result, element) { - return { - type: 'BinaryExpression', - operator: element[1], - left: result, - right: element[3] - }; - }, head); - } -} - -start = _* b:behavior* _* { return b; } - -behavior - = _* events:on _ conditions:if _ actions:then _* semicolon { - return { - type: 'behavior', - events: events, - conditions: conditions, - actions: actions - }; - } - / _* events:on _ actions:then _* semicolon { - return { - type: 'behavior', - events: events, - actions: actions - }; - } - -// on - -on = "on" _ head:event tail:(_ or _ event)* { return unroll(head, tail, 3); } - -event - = e:word dot a:word { return {type: 'event', name: e, action: a}; } - / e:word { return {type: 'event', name: e}; } - -// if - -if = "if" _ conditions:RelationalExpression { return conditions; } - -RelationalExpression - = head:LogicalOrExpression tail:(_ RelationalOperator _ LogicalOrExpression)* { - return binaryExpression(head, tail); - } -RelationalOperator - = "is not" - / "is" - / "does not contain" - / "contains" - / "does not match" - / "matches" - -LogicalOrExpression - = head:LogicalAndExpression tail:(_ or _ LogicalAndExpression)* { - return binaryExpression(head, tail); - } - -LogicalAndExpression - = head:UnaryExpression tail:(_ and _ UnaryExpression)* { - return binaryExpression(head, tail); - } - -UnaryExpression - = operand - / operator:UnaryOperator _ argument:UnaryExpression { - return { - type: 'UnaryExpression', - operator: operator, - argument: argument - } - } - -UnaryOperator = "not" - -operand = condition / attribute / string / boolean - -condition - = name:word "(" args:arguments ")" { - return {type: 'condition', name: name, args: args}; - } - -attribute = "@" head:word tail:(dot word)* { - return {type: 'attribute', name: unroll(head, tail, 1)}; -} - -// then - -then - = "then" _ head:action tail:(_ and _ action)* { - return unroll(head, tail, 3); - } - -action - = name:word "(" _* args:arguments _* ")" { - return {type: 'action', name: name, args: args}; - } - / name:word { return {type: 'action', name: name}; } - -arguments - = head:argument tail:(_* comma _* argument)* { return unroll(head, tail, 3); } - -argument = word / string - -boolean = true / false -true = "true" { return true; } -false = "false" { return false; } - -// Strings - -string "string" - = quotation_mark chars:char* quotation_mark { return chars.join(""); } - -char - = unescaped - / escape - sequence:( - '"' - / "\\" - / "/" - / "b" { return "\b"; } - / "f" { return "\f"; } - / "n" { return "\n"; } - / "r" { return "\r"; } - / "t" { return "\t"; } - / "u" digits:$(HEXDIG HEXDIG HEXDIG HEXDIG) { - return String.fromCharCode(parseInt(digits, 16)); - } - ) - { return sequence; } - -escape - = "\\" - -quotation_mark - = '"' - -unescaped - = [^\0-\x1F\x22\x5C] - -HEXDIG = [0-9a-f]i - -// Character Classes - -word = letters:[a-zA-Z0-9_]+ { return letters.join(''); } -dot = "." -comma = "," -semicolon = ";" -and = "and" -or = "or" -_ "whitespace" = ([ \t\n\r] / comment)+ -comment = "#" (!LineTerminator .)* -LineTerminator = [\n\r\u2028\u2029] diff --git a/lib/plugins/base.js b/lib/plugins/base.js new file mode 100644 index 0000000..280b56b --- /dev/null +++ b/lib/plugins/base.js @@ -0,0 +1,27 @@ +// This is some stuff to handle mixing in plugins into a base class +let mix = (superclass) => new MixinBuilder(superclass); + +class MixinBuilder { + constructor(superclass) { + this.superclass = superclass; + } + + with(...mixins) { + return mixins.reduce((c, mixin) => mixin(c), this.superclass); + } +} + +class EvaluatorBase { + // Public function to either pass the string through or lookup network data + resolveData(data, fn) { + // TODO make a network to fetch the data + fn(data) + return this; + } +} + + +module.exports = { + mix: mix, + Evaluator: EvaluatorBase, +} diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js new file mode 100644 index 0000000..c45cb09 --- /dev/null +++ b/lib/plugins/issues.js @@ -0,0 +1,71 @@ +const base = require('./base') + +// These methods will be exposed to the sandboxed environment +let IssuePlugin = (superclass) => class extends superclass { + comment(content) { + this._setCommentData({content: content}) + return this; + } + + label(...labels) { + this._setCommentData({labels: labels}) + return this; + } + + close(){ + this._setCommentData({close: true}) + return this; + } + + _setCommentData(obj) { + if (this.issueActions === undefined) { + this.issueActions = {} + } + for (var prop in obj) { + this.issueActions[prop] = obj[prop] + } + } +} + +// This is the function that implements all of the actions configured above. +class IssueEvaluator extends base.Evaluator { + evaluate (workflow, context) { + let event = context.event; + + // Bail if no issue related actions + if (workflow.issueActions === undefined) { + return; + } + + // Need to guard to make sure we are only processing events we know what to + // do with. It might be nice to have it throw errors in a validation step + // so that people know when that they have actions that don't match their + // events. + if (event.issue !== undefined && event.pull_request !== undefined) { + return; + } + let item = event.issue || event.pull_request; + + console.log("Issue "+item.id+":") + + // TODO: We should probably chain promises here + if (workflow.issueActions.content !== undefined) { + this.resolveData(workflow.issueActions.content, () => { + console.log("Commenting '"+workflow.issueActions.content+"'") + }) + } + + if (workflow.issueActions.labels !== undefined) { + console.log("Labeling with: "+workflow.issueActions.labels) + } + + if (workflow.issueActions.content !== undefined) { + console.log("Closing") + } + } +} + +module.exports = { + Plugin: IssuePlugin, + Evaluator: IssueEvaluator +} diff --git a/lib/transformer.js b/lib/transformer.js deleted file mode 100644 index 287e220..0000000 --- a/lib/transformer.js +++ /dev/null @@ -1,71 +0,0 @@ -const walk = require('tree-walk'); -const debug = require('debug')('PRobot'); -const Behavior = require('./behavior'); -const actions = require('./actions'); -const conditions = require('./conditions'); -const binaryExpression = require('./binary-expression'); -const attribute = require('./attribute'); - -module.exports = class Transformer { - constructor(tree) { - this.tree = tree; - } - - transform() { - return walk.reduce(this.tree, this.walk.bind(this)); - } - - walk(result, node) { - if (this[node.type]) { - return this[node.type](result, node); - } else if (node.type) { - throw new Error('Unknown node: ' + node.type); - } else { - return result || node; - } - } - - event(node) { - return node; - } - - attribute(node) { - return context => attribute(context, node.name); - } - - action(node) { - return context => { - if (!actions[node.name]) { - throw new Error('Unknown action: ' + node.name); - } - debug('action: %s', node.name, node.args); - return actions[node.name].apply(context, [context].concat(node.args)); - }; - } - - behavior(node) { - return new Behavior(node.events, node.conditions, node.actions); - } - - condition(node) { - return context => { - if (!conditions[node.name]) { - throw new Error('Unknown condition: ' + node.name); - } - debug('condition: %s', node.name, node.args); - return conditions[node.name].apply(context, [context].concat(node.args)); - }; - } - - BinaryExpression(node) { - return context => { - return binaryExpression(context, node.left, node.operator, node.right); - }; - } - - UnaryExpression(node) { - return context => { - return !node.argument(context); - }; - } -}; diff --git a/lib/utils/url.js b/lib/utils/url.js new file mode 100644 index 0000000..2e0552e --- /dev/null +++ b/lib/utils/url.js @@ -0,0 +1,6 @@ +module.exports = class URL { + constructor(url) { + this.url = url + } +} + diff --git a/lib/workflow.js b/lib/workflow.js new file mode 100644 index 0000000..646b25c --- /dev/null +++ b/lib/workflow.js @@ -0,0 +1,29 @@ +const plugin = require('./plugins/base') +const issues = require('./plugins/issues') + +class WorkflowCore { + constructor(events) { + this.events = events + } + + filter(fn) { + this.filterFn = fn; + return this; + } +} + +// FIXME: issues +plugins = [ + issues.Plugin +] + +class Workflow extends plugin.mix(WorkflowCore).with(...plugins) {} + +on = (...events) => { + return new Workflow(events); +} + +module.exports = { + Workflow: Workflow, + on: on +} From a21e03bee9d0f2602f99a5a7ff9781f801646418 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Wed, 16 Nov 2016 17:41:25 -0800 Subject: [PATCH 04/29] reimplement close action --- lib/actions.js | 1 - lib/actions/close.js | 5 ----- lib/plugins/issues.js | 29 ++++++++++++++++++++++------- 3 files changed, 22 insertions(+), 13 deletions(-) delete mode 100644 lib/actions/close.js diff --git a/lib/actions.js b/lib/actions.js index 6444c45..94a8588 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -1,6 +1,5 @@ module.exports = { assign: require('./actions/assign'), - close: require('./actions/close'), comment: require('./actions/comment'), label: require('./actions/label'), lock: require('./actions/lock'), diff --git a/lib/actions/close.js b/lib/actions/close.js deleted file mode 100644 index 7f84f65..0000000 --- a/lib/actions/close.js +++ /dev/null @@ -1,5 +0,0 @@ -const updateIssue = require('./update-issue'); - -module.exports = function (context) { - return updateIssue(context, {state: 'closed'}); -}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index c45cb09..d312bdc 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -46,22 +46,37 @@ class IssueEvaluator extends base.Evaluator { } let item = event.issue || event.pull_request; - console.log("Issue "+item.id+":") + let promises = []; - // TODO: We should probably chain promises here if (workflow.issueActions.content !== undefined) { - this.resolveData(workflow.issueActions.content, () => { - console.log("Commenting '"+workflow.issueActions.content+"'") - }) + promises.push(new Promise((resolve, reject) => { + this.resolveData(workflow.issueActions.content, () => { + console.log("Commenting '"+workflow.issueActions.content+"'") + resolve(); + }) + })); } if (workflow.issueActions.labels !== undefined) { - console.log("Labeling with: "+workflow.issueActions.labels) + promises.push(new Promise((resolve, reject) => { + console.log("Labeling with: "+workflow.issueActions.labels) + resolve() + })); } if (workflow.issueActions.content !== undefined) { - console.log("Closing") + promises.push(new Promise((resolve, reject) => { + context.github.issues.edit(context.payload.toIssue(), (err, res) => { + if (err) { + reject(); + }else{ + resolve(); + } + }); + })); } + + return promises; } } From 8f2c92aad903a4e20aef66b856ba0b0ecaa887b1 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Wed, 16 Nov 2016 17:46:15 -0800 Subject: [PATCH 05/29] reimplement labeling --- lib/actions.js | 1 - lib/actions/label.js | 5 ----- lib/plugins/issues.js | 12 +++++++++--- 3 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 lib/actions/label.js diff --git a/lib/actions.js b/lib/actions.js index 94a8588..845849a 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -1,7 +1,6 @@ module.exports = { assign: require('./actions/assign'), comment: require('./actions/comment'), - label: require('./actions/label'), lock: require('./actions/lock'), open: require('./actions/open'), unassign: require('./actions/unassign'), diff --git a/lib/actions/label.js b/lib/actions/label.js deleted file mode 100644 index 753ebdf..0000000 --- a/lib/actions/label.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function (context, ...labels) { - return context.github.issues.addLabels( - context.payload.toIssue({body: labels}) - ); -}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index d312bdc..71a879b 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -58,15 +58,21 @@ class IssueEvaluator extends base.Evaluator { } if (workflow.issueActions.labels !== undefined) { + let labels = workflow.issueActions.labels; promises.push(new Promise((resolve, reject) => { - console.log("Labeling with: "+workflow.issueActions.labels) - resolve() + context.github.issues.addLabels(context.payload.toIssue({body: labels}), (err, res) => { + if (err) { + reject(); + }else{ + resolve(); + } + }); })); } if (workflow.issueActions.content !== undefined) { promises.push(new Promise((resolve, reject) => { - context.github.issues.edit(context.payload.toIssue(), (err, res) => { + context.github.issues.edit(context.payload.toIssue({state: "closed"}), (err, res) => { if (err) { reject(); }else{ From 280cab1013fc530c78f4e4b72997824c3c51ddab Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 08:48:31 -0800 Subject: [PATCH 06/29] Reimplement comment --- lib/actions.js | 1 - lib/actions/comment.js | 7 ------- lib/plugins/issues.js | 11 ++++++++--- 3 files changed, 8 insertions(+), 11 deletions(-) delete mode 100644 lib/actions/comment.js diff --git a/lib/actions.js b/lib/actions.js index 845849a..2590d38 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -1,6 +1,5 @@ module.exports = { assign: require('./actions/assign'), - comment: require('./actions/comment'), lock: require('./actions/lock'), open: require('./actions/open'), unassign: require('./actions/unassign'), diff --git a/lib/actions/comment.js b/lib/actions/comment.js deleted file mode 100644 index 67b4bf5..0000000 --- a/lib/actions/comment.js +++ /dev/null @@ -1,7 +0,0 @@ -const handlebars = require('handlebars'); - -module.exports = function (context, template) { - return context.github.issues.createComment( - context.payload.toIssue({body: handlebars.compile(template)(context.payload)}) - ); -}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 71a879b..012c2d5 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -50,9 +50,14 @@ class IssueEvaluator extends base.Evaluator { if (workflow.issueActions.content !== undefined) { promises.push(new Promise((resolve, reject) => { - this.resolveData(workflow.issueActions.content, () => { - console.log("Commenting '"+workflow.issueActions.content+"'") - resolve(); + this.resolveData(workflow.issueActions.content, (data) => { + context.github.issues.createComment(context.payload.toIssue({body: data}, (err, res) => { + if (err) { + reject(); + }else{ + resolve(); + } + }); }) })); } From 4368632c0d4c628a2cf0c45a348f536690f629c9 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 08:52:03 -0800 Subject: [PATCH 07/29] Fix paren matching --- lib/plugins/issues.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 012c2d5..09e5526 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -57,7 +57,7 @@ class IssueEvaluator extends base.Evaluator { }else{ resolve(); } - }); + })) }) })); } From 2d567034d2c4a97ac12536341ee6dabd5643c665 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 09:37:40 -0800 Subject: [PATCH 08/29] Fix dispatcher tests --- hack.js | 22 ---- lib/behavior.js | 17 --- lib/conditions.js | 3 - lib/conditions/labeled.js | 3 - lib/configuration.js | 37 ++---- lib/plugins/issues.js | 2 +- lib/workflow.js | 15 +++ test/attribute.js | 16 --- test/conditions.js | 105 --------------- test/dispatcher.js | 17 +-- test/parser.js | 261 -------------------------------------- 11 files changed, 35 insertions(+), 463 deletions(-) delete mode 100644 hack.js delete mode 100644 lib/behavior.js delete mode 100644 lib/conditions.js delete mode 100644 lib/conditions/labeled.js delete mode 100644 test/attribute.js delete mode 100644 test/conditions.js delete mode 100644 test/parser.js diff --git a/hack.js b/hack.js deleted file mode 100644 index 53c1b80..0000000 --- a/hack.js +++ /dev/null @@ -1,22 +0,0 @@ -// THIS IS A GROSS HACK TO BYPASS THE NEED FOR A WEBHOOKS SETUP -// TO BE REMOVED - -const Configuration = require('./lib/configuration'); -const Dispatcher = require('./lib/dispatcher'); -const GitHubApi = require('github'); -const github = new GitHubApi({debug: false}); - -event = { - payload: { - repository: "", - }, - - issue : { - id: 1, - title: "Issue without steps", - body: "how do I use this thing?", - } -} -const dispatcher = new Dispatcher(github, event); -let config = Configuration.load(github, event.payload.repository) -dispatcher.call(config) diff --git a/lib/behavior.js b/lib/behavior.js deleted file mode 100644 index 84f1666..0000000 --- a/lib/behavior.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = class Behavior { - constructor(events, conditions, actions) { - this.events = events; - this.conditions = conditions; - this.actions = actions; - } - - perform(context) { - if (this.conditions && !this.conditions(context)) { - return Promise.resolve(); - } - - return Promise.all(this.actions.map(action => { - return action(context); - })); - } -}; diff --git a/lib/conditions.js b/lib/conditions.js deleted file mode 100644 index 70e5a21..0000000 --- a/lib/conditions.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - labeled: require('./conditions/labeled') -}; diff --git a/lib/conditions/labeled.js b/lib/conditions/labeled.js deleted file mode 100644 index 618b031..0000000 --- a/lib/conditions/labeled.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = (context, label) => { - return context.payload.label && context.payload.label.name === label; -}; diff --git a/lib/configuration.js b/lib/configuration.js index 070f405..0cd1783 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -8,29 +8,16 @@ module.exports = class Configuration { // Get bot config from target repository static load(github, repository) { debug('Fetching .probot from %s', repository.full_name); - // HACK: This needs to be reinstated once things are more settled - //const parts = repository.full_name.split('/'); - //return github.repos.getContent({ - // owner: parts[0], - // repo: parts[1], - // path: '.probot' - //}).then(data => { - // const content = new Buffer(data.content, 'base64').toString(); - // debug('Configuration fetched', content); - // return Configuration.parse(content); - //}); - let s = ` - workflows.push(on("issues.opened") - .filter((event) => { - return !event.issue.body.match(/### Steps to Reproduce/) - || event.issue.body.includes("- [ ]") - }) - .comment(new URL(".github/MISSING_ISSUE_TEMPLATE_AUTOREPLY.md")) - .label("insufficient-info") - .close() - ); - ` - return Configuration.parse(s) + const parts = repository.full_name.split('/'); + return github.repos.getContent({ + owner: parts[0], + repo: parts[1], + path: '.probot' + }).then(data => { + const content = new Buffer(data.content, 'base64').toString(); + debug('Configuration fetched', content); + return Configuration.parse(content); + }); } static parse(content){ @@ -49,6 +36,8 @@ module.exports = class Configuration { } workflowsFor(event) { - return this.workflows.filter((w) => w.filterFn(event)); + return this.workflows.filter((w) => { + return w.matchesEvent(event) && w.filterFn(event) + }); } }; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 09e5526..7c0d65d 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -75,7 +75,7 @@ class IssueEvaluator extends base.Evaluator { })); } - if (workflow.issueActions.content !== undefined) { + if (workflow.issueActions.close !== undefined) { promises.push(new Promise((resolve, reject) => { context.github.issues.edit(context.payload.toIssue({state: "closed"}), (err, res) => { if (err) { diff --git a/lib/workflow.js b/lib/workflow.js index 646b25c..f167e8e 100644 --- a/lib/workflow.js +++ b/lib/workflow.js @@ -4,12 +4,27 @@ const issues = require('./plugins/issues') class WorkflowCore { constructor(events) { this.events = events + this.filterFn = () => true } filter(fn) { this.filterFn = fn; return this; } + + matchesEvent(event) { + let eventWithAction = [event.event, event.payload.action]; + + return this.events.find((e) => { + let parts = e.split("."); + for (let i = 0; i < parts.length; i++) { + if (parts[i] != eventWithAction[i]) { + return false; + } + } + return true; + }); + } } // FIXME: issues diff --git a/test/attribute.js b/test/attribute.js deleted file mode 100644 index 4e741ac..0000000 --- a/test/attribute.js +++ /dev/null @@ -1,16 +0,0 @@ -const expect = require('expect'); -const attribute = require('../lib/attribute'); - -describe('attribute', () => { - it('fetches the attribute from the payload', () => { - const context = {payload: {foo: {bar: 'baz'}}}; - const value = attribute(context, ['foo', 'bar']); - expect(value).toEqual('baz'); - }); - - it('returns null for unknown attributes', () => { - const context = {payload: {}}; - const value = attribute(context, ['foo', 'unknown']); - expect(value).toEqual(null); - }); -}); diff --git a/test/conditions.js b/test/conditions.js deleted file mode 100644 index b985fd8..0000000 --- a/test/conditions.js +++ /dev/null @@ -1,105 +0,0 @@ -const expect = require('expect'); -const Transformer = require('../lib/transformer'); -const Context = require('../lib/context'); -const parser = require('../lib/parser'); -const payload = require('./fixtures/webhook/issues.labeled.json'); - -const context = new Context({}, {}, {payload}); - -function test(string) { - const ast = parser.parse(string, {startRule: 'if'}); - const condition = new Transformer(ast).transform(); - return typeof condition === 'function' ? condition(context) : condition; -} - -describe('conditions', () => { - describe('is', () => { - it('passes when operands are equal', () => { - expect(test(`if @sender.login is "bkeepers"`)).toBeTruthy(); - }); - - it('fails when operands are not equal', () => { - expect(test(`if @sender.login is "nobody"`)).toBeFalsy(); - }); - }); - - describe('is not', () => { - it('fails when operands are equal', () => { - expect(test('if @sender.login is not "bkeepers"')).toBeFalsy(); - }); - - it('passes when operands are not equal', () => { - expect(test('if @sender.login is not "nobody"')).toBeTruthy(); - }); - }); - - describe('contains', () => { - it('passes when operand contains substring', () => { - expect(test('if @issue.title contains "bug"')).toBeTruthy(); - }); - - it('fails when operand does not contain substring', () => { - expect(test('if @issue.title contains "nope"')).toBeFalsy(); - }); - }); - - describe('does not contain', () => { - it('fails when operand contains substring', () => { - expect(test('if @issue.title does not contain "bug"')).toBeFalsy(); - }); - - it('passes when operand does not contain substring', () => { - expect(test('if @issue.title does not contain "nope"')).toBeTruthy(); - }); - }); - - describe('matches', () => { - it('passes when operand matches regexp', () => { - expect(test('if @sender.login matches "ke+"')).toBeTruthy(); - }); - - it('fails when operand does not match regexp', () => { - expect(test('if @issue.title matches "nope"')).toBeFalsy(); - }); - }); - - describe('does not match', () => { - it('fails when operand matches regexp', () => { - expect(test('if @sender.login does not match "ke+"')).toBeFalsy(); - }); - - it('passes when operand does not match regexp', () => { - expect(test('if @issue.title does not match "nope"')).toBeTruthy(); - }); - }); - - describe('or', () => { - it('passes if either operand is truthy', () => { - expect(test('if labeled(bug) or labeled(feature)')).toBeTruthy(); - }); - - it('fails if neither operand is truthy', () => { - expect(test('if labeled(nope) or labeled(never)')).toBeFalsy(); - }); - }); - - describe('and', () => { - it('passes if both operands are truthy', () => { - expect(test('if labeled(bug) and @issue.title contains "bug"')).toBeTruthy(); - }); - - it('fails if one operand is not truthy', () => { - expect(test('if labeled(bug) and labeled(nope)')).toBeFalsy(); - }); - }); - - describe('not', () => { - it('passes if condition is not truthy', () => { - expect(test('if not labeled(something)')).toBeTruthy(); - }); - - it('fails if condition is truthy', () => { - expect(test('if not labeled(bug)')).toBeFalsy(); - }); - }); -}); diff --git a/test/dispatcher.js b/test/dispatcher.js index 6f12e83..cb82784 100644 --- a/test/dispatcher.js +++ b/test/dispatcher.js @@ -22,7 +22,7 @@ describe('dispatch', () => { describe('reply to new issue with a comment', () => { it('posts a coment', () => { - const config = Configuration.parse('on issues then comment("Hello World!");'); + const config = Configuration.parse('workflows.push(on("issues").comment("Hello World!"))') return dispatcher.call(config).then(() => { expect(github.issues.createComment).toHaveBeenCalled(); }); @@ -31,7 +31,7 @@ describe('dispatch', () => { describe('reply to new issue with a comment', () => { it('calls the action', () => { - const config = Configuration.parse('on issues.created then comment("Hello World!");'); + const config = Configuration.parse('workflows.push(on("issues.created").comment("Hello World!"))') return dispatcher.call(config).then(() => { expect(github.issues.createComment).toHaveBeenCalled(); @@ -41,7 +41,7 @@ describe('dispatch', () => { describe('on an event with a different action', () => { it('does not perform behavior', () => { - const config = Configuration.parse('on issues.labeled then comment("Hello World!");'); + const config = Configuration.parse('workflows.push(on("issues.labeled").comment("Hello World!"))') return dispatcher.call(config).then(() => { expect(github.issues.createComment).toNotHaveBeenCalled(); @@ -49,7 +49,7 @@ describe('dispatch', () => { }); }); - describe('conditions', () => { + describe('filter', () => { beforeEach(() => { const labeled = require('./fixtures/webhook/issues.labeled.json'); @@ -58,20 +58,15 @@ describe('dispatch', () => { dispatcher = new Dispatcher(github, event); }); - it('fails for unknown conditions', () => { - const config = Configuration.parse(`on issues.labeled if failwhale(bug) then close;`); - expect(() => dispatcher.call(config)).toThrow(/unknown condition/i); - }); - it('calls action when condition matches', () => { - const config = Configuration.parse(`on issues.labeled if labeled(bug) then close;`); + const config = Configuration.parse('workflows.push(on("issues.labeled").filter((e) => e.payload.label.name == "bug").close())') return dispatcher.call(config).then(() => { expect(github.issues.edit).toHaveBeenCalled(); }); }); it('does not call action when conditions do not match', () => { - const config = Configuration.parse(`on issues.labeled if labeled(foobar) then close;`); + const config = Configuration.parse('workflows.push(on("issues.labeled").filter((e) => e.payload.label.name == "foobar").close())') return dispatcher.call(config).then(() => { expect(github.issues.edit).toNotHaveBeenCalled(); }); diff --git a/test/parser.js b/test/parser.js deleted file mode 100644 index 14329b0..0000000 --- a/test/parser.js +++ /dev/null @@ -1,261 +0,0 @@ -const fs = require('fs'); -const expect = require('expect'); -const parser = require('../lib/parser'); - -describe('parser', () => { - it('successfully parses a bunch of examples', () => { - parser.parse(fs.readFileSync('./test/fixtures/behaviors').toString()); - }); - - it('reports syntax errors', () => { - expect(() => parser.parse('lolwut?').toThrow(/parse error/)); - }); - - it('parses multiple statements', () => { - expect(parser.parse(` - on issues.opened then close; - on pull_requests.open then assign(bkeepers); - `)).toEqual([ - { - type: 'behavior', - events: [{type: 'event', action: 'opened', name: 'issues'}], - actions: [{type: 'action', name: 'close'}] - }, - { - type: 'behavior', - events: [{type: 'event', action: 'open', name: 'pull_requests'}], - actions: [{type: 'action', name: 'assign', args: ['bkeepers']}] - } - ]); - }); - - it('parses a blank doc', () => { - expect(parser.parse('\n\n\n')).toEqual([]); - }); - - it('fails on junk', () => { - expect(() => parser.parse('onnope then nope;')).toThrow(); - expect(() => parser.parse('on nope thennope;')).toThrow(); - }); - - describe('on', () => { - it('parses an event', () => { - expect(parser.parse('on issues then close;')).toEqual([{ - type: 'behavior', - events: [{type: 'event', name: 'issues'}], - actions: [{type: 'action', name: 'close'}] - }]); - }); - - it('parses an event and action', () => { - expect(parser.parse('on issues.opened then close;')).toEqual([{ - type: 'behavior', - events: [{type: 'event', name: 'issues', action: 'opened'}], - actions: [{type: 'action', name: 'close'}] - }]); - }); - - it('parses multiple events', () => { - expect(parser.parse( - 'on issues.opened or pull_request.opened then close;' - )).toEqual([{ - type: 'behavior', - events: [ - {type: 'event', name: 'issues', action: 'opened'}, - {type: 'event', name: 'pull_request', action: 'opened'} - ], - actions: [{type: 'action', name: 'close'}] - }]); - }); - }); - - describe('if', () => { - function parse(string) { - return parser.parse(string, {startRule: 'if'}); - } - - it('parses simple conditionals', () => { - expect( - parser.parse('on issues if labeled(enhancement) then close;') - ).toEqual([{ - type: 'behavior', - events: [{type: 'event', name: 'issues'}], - conditions: {type: 'condition', name: 'labeled', args: ['enhancement']}, - actions: [{type: 'action', name: 'close'}] - }]); - }); - - it('parses logical or conditions', () => { - expect(parse('if labeled(enhancement) or labeled(design)')).toEqual({ - type: 'BinaryExpression', - operator: 'or', - left: {type: 'condition', name: 'labeled', args: ['enhancement']}, - right: {type: 'condition', name: 'labeled', args: ['design']} - }); - }); - - it('parses multiple logical conditions', () => { - expect( - parse('if labeled(enhancement) or labeled(design) or labeled(bug)') - ).toEqual({ - type: 'BinaryExpression', - operator: 'or', - left: { - type: 'BinaryExpression', - left: {type: 'condition', name: 'labeled', args: ['enhancement']}, - operator: 'or', - right: {type: 'condition', name: 'labeled', args: ['design']} - }, - right: {type: 'condition', name: 'labeled', args: ['bug']} - }); - }); - - it('parses logical and conditions', () => { - expect(parse('if labeled(enhancement) and labeled(design)')).toEqual({ - type: 'BinaryExpression', - operator: 'and', - left: {type: 'condition', name: 'labeled', args: ['enhancement']}, - right: {type: 'condition', name: 'labeled', args: ['design']} - }); - }); - - it('parses attributes, matches, and regexps', () => { - expect(parse('if @issue.user.login matches "bot$"')).toEqual({ - type: 'BinaryExpression', - operator: 'matches', - left: {type: 'attribute', name: ['issue', 'user', 'login']}, - right: 'bot$' - }); - }); - - describe('not', () => { - it('negates conditions', () => { - expect(parse('if not labeled(bug)')).toEqual({ - type: 'UnaryExpression', - operator: 'not', - argument: {type: 'condition', name: 'labeled', args: ['bug']} - }); - }); - }); - - describe('precedence', () => { - it('orders "and" over "or"', () => { - expect(parse('if true and false or true')).toEqual({ - type: 'BinaryExpression', - left: { - type: 'BinaryExpression', - left: true, - operator: 'and', - right: false - }, - operator: 'or', - right: true - }); - - expect(parse('if false or true and true')).toEqual({ - type: 'BinaryExpression', - left: false, - operator: 'or', - right: { - type: 'BinaryExpression', - left: true, - operator: 'and', - right: true - } - }); - - expect(parse('if true and true or false and false')).toEqual({ - type: 'BinaryExpression', - left: { - type: 'BinaryExpression', - left: true, - operator: 'and', - right: true - }, - operator: 'or', - right: { - type: 'BinaryExpression', - left: false, - operator: 'and', - right: false - } - }); - }); - }); - }); - - describe('then', () => { - it('parses multiple words', () => { - expect(parser.parse( - 'on issues then close and lock;' - )).toEqual([{ - type: 'behavior', - events: [{type: 'event', name: 'issues'}], - actions: [ - {type: 'action', name: 'close'}, - {type: 'action', name: 'lock'} - ] - }]); - }); - - it('parses string arguments', () => { - expect(parser.parse( - 'on issues then comment("Hello World!");' - )).toEqual([{ - type: 'behavior', - events: [{type: 'event', name: 'issues'}], - actions: [ - {type: 'action', name: 'comment', args: ['Hello World!']} - ] - }]); - }); - - it('parses word arguments', () => { - expect(parser.parse( - 'on issues then assign(bkeepers);' - )).toEqual([{ - type: 'behavior', - events: [{type: 'event', name: 'issues'}], - actions: [ - {type: 'action', name: 'assign', args: ['bkeepers']} - ] - }]); - }); - - it('parses multiple word arguments', () => { - expect(parser.parse( - 'on issues then assign(bkeepers, hubot);' - )).toEqual([{ - type: 'behavior', - events: [{type: 'event', name: 'issues'}], - actions: [ - {type: 'action', name: 'assign', args: ['bkeepers', 'hubot']} - ] - }]); - }); - }); - - describe('comments', () => { - it('ignores lines that start with comments', () => { - expect(parser.parse(` - # This could literally be anything. - on issues then close; - `)).toEqual( - [{type: 'behavior', - events: [{type: 'event', name: 'issues'}], - actions: [{type: 'action', name: 'close'}]}] - ); - }); - - it('ignores trailing comments on lines', () => { - expect(parser.parse(` - on issues # Ignore this - then close; - `)).toEqual([{ - type: 'behavior', - events: [{type: 'event', name: 'issues'}], - actions: [{type: 'action', name: 'close'}] - }]); - }); - }); -}); From 9348b522c01c882eacc2e8301f1b44fb4f42f633 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 10:18:53 -0800 Subject: [PATCH 09/29] Fix configuration tests --- lib/plugins/issues.js | 16 ++++++++++++++++ test/configuration.js | 14 +++++++------- test/fixtures/content/probot.json | 2 +- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 7c0d65d..b1a76eb 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -7,11 +7,27 @@ let IssuePlugin = (superclass) => class extends superclass { return this; } + assign(...users) { + return this; + } + + react(...users) { + return this; + } + + unassign(...users) { + return this; + } + label(...labels) { this._setCommentData({labels: labels}) return this; } + lock() { + return this; + } + close(){ this._setCommentData({close: true}) return this; diff --git a/test/configuration.js b/test/configuration.js index 27f9f09..debb26c 100644 --- a/test/configuration.js +++ b/test/configuration.js @@ -26,29 +26,29 @@ describe('Configuration', () => { path: '.probot' }); - expect(config.behaviors.length).toEqual(2); + expect(config.workflows.length).toEqual(2); done(); }); }); }); - describe('behaviorsFor', () => { + describe('workflowsFor', () => { const config = Configuration.parse(` - on issues then label(active); - on issues.created then close; - on pull_request.labeled then lock; + workflows.push(on("issues").label("active")); + workflows.push(on("issues.created").close()); + workflows.push(on("pull_request.labeled").lock()); `); it('returns behaviors for event', () => { expect( - config.behaviorsFor({event: 'issues', payload: {}}).length + config.workflowsFor({event: 'issues', payload: {}}).length ).toEqual(1); }); it('returns behaviors for event and action', () => { expect( - config.behaviorsFor({event: 'issues', payload: {action: 'created'}}).length + config.workflowsFor({event: 'issues', payload: {action: 'created'}}).length ).toEqual(2); }); }); diff --git a/test/fixtures/content/probot.json b/test/fixtures/content/probot.json index 79dcd3d..28699a7 100644 --- a/test/fixtures/content/probot.json +++ b/test/fixtures/content/probot.json @@ -8,7 +8,7 @@ "git_url": "https://api.github.com/repos/bkeepers-inc/test/git/blobs/c0736435c3f35dc5702d5024ed00d399b99623af", "download_url": "https://raw.githubusercontent.com/bkeepers-inc/test/master/.probot?token=AVvMVIL7kvcrgJYZEipG9eAgUPcGHBnxks5YBayLwA%3D%3D", "type": "file", - "content": "b24gaXNzdWVzLm9wZW5lZCB0aGVuIGNvbW1lbnQoIkhlbGxvIHdvcmxkISIp\nIGFuZCBhc3NpZ24oYmtlZXBlcnMpIGFuZCByZWFjdChoZWFydCk7Cm9uIGlz\nc3Vlcy5jbG9zZWQgdGhlbiB1bmFzc2lnbihia2VlcGVycyk7Cg==\n", + "content": "d29ya2Zsb3dzLnB1c2goCiAgb24oImlzc3Vlcy5vcGVuZWQiKQogICAgLmNv\nbW1lbnQoIkhlbGxvIFdvcmxkISIpCiAgICAuYXNzaWduKCJia2VlcGVycyIp\nCiAgICAucmVhY3QoImhlYXJ0IikKKQoKd29ya2Zsb3dzLnB1c2goCiAgb24o\nImlzc3Vlcy5jbG9zZWQiKQogICAgLnVuYXNzaWduKCJia2VlcGVycyIpCikK\n", "encoding": "base64", "_links": { "self": "https://api.github.com/repos/bkeepers-inc/test/contents/.probot?ref=master", From 09f0d7fa11ea7a955d86e6a5119446b18d094967 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 11:08:18 -0800 Subject: [PATCH 10/29] Add issues plugin test --- lib/plugins/issues.js | 24 ++++++------------------ test/fixtures/behaviors | 29 ----------------------------- test/plugins/issues.js | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 47 deletions(-) delete mode 100644 test/fixtures/behaviors create mode 100644 test/plugins/issues.js diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index b1a76eb..bfdd68b 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -80,27 +80,15 @@ class IssueEvaluator extends base.Evaluator { if (workflow.issueActions.labels !== undefined) { let labels = workflow.issueActions.labels; - promises.push(new Promise((resolve, reject) => { - context.github.issues.addLabels(context.payload.toIssue({body: labels}), (err, res) => { - if (err) { - reject(); - }else{ - resolve(); - } - }); - })); + promises.push( + context.github.issues.addLabels(context.payload.toIssue({body: labels})) + ); } if (workflow.issueActions.close !== undefined) { - promises.push(new Promise((resolve, reject) => { - context.github.issues.edit(context.payload.toIssue({state: "closed"}), (err, res) => { - if (err) { - reject(); - }else{ - resolve(); - } - }); - })); + promises.push( + context.github.issues.edit(context.payload.toIssue({state: "closed"})) + ); } return promises; diff --git a/test/fixtures/behaviors b/test/fixtures/behaviors deleted file mode 100644 index 5c4e976..0000000 --- a/test/fixtures/behaviors +++ /dev/null @@ -1,29 +0,0 @@ -# Integration test - -on issues # ignore this -then close; - -on issues then assign(hubot, probot); - -on issues.assign -if labeled(foo) and labeled(bar) or labeled(baz) -then close and comment("nope!"); - -on issues.opened -if @issue.user.login matches "bot$" -then comment("No bots allowed!") and close; - -on issues.opened if @sender.login is "bkeepers" then close; -on issues.opened if @sender.login is not "bkeepers" then close; -on issues.opened if @issue.body contains "- [ ]" then label(wip); - -on issues.opened -if not labeled(bug) -then close; - -on issues.opened -if @issue.body does not match "### Prerequisites.*### Description.*### Steps to Reproduce.*### Versions" - or @issue.body.body contains "- [ ]" -then - label("insufficient-info") - and close; diff --git a/test/plugins/issues.js b/test/plugins/issues.js new file mode 100644 index 0000000..f30b595 --- /dev/null +++ b/test/plugins/issues.js @@ -0,0 +1,32 @@ +const expect = require('expect'); +const issues = require('../../lib/plugins/issues'); +const workflow = require('../../lib/workflow'); +const dispatcher = require('../../lib/dispatcher'); +const Context = require('../../lib/context'); +const payload = require('../fixtures/webhook/comment.created.json'); + +const createSpy = expect.createSpy; + +const github = { + issues: { + edit: createSpy() + } +}; +const context = new Context(github, {}, {payload}); + +describe('issues plugin', () => { + describe('close', () => { + it('closes an issue', () => { + w = new workflow.Workflow(); + w.close() + evaluator = new issues.Evaluator; + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.edit).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + state: 'closed', + }); + }); + }) +}) From 0a060b670d9adec484bfb74841c6bc696c9a6d71 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 11:17:05 -0800 Subject: [PATCH 11/29] Move label tests into plugin --- test/actions/label.js | 35 ----------------------------------- test/plugins/issues.js | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 39 deletions(-) delete mode 100644 test/actions/label.js diff --git a/test/actions/label.js b/test/actions/label.js deleted file mode 100644 index 39bc472..0000000 --- a/test/actions/label.js +++ /dev/null @@ -1,35 +0,0 @@ -const expect = require('expect'); -const action = require('../../lib/actions/label'); -const Context = require('../../lib/context'); -const payload = require('../fixtures/webhook/comment.created.json'); - -const createSpy = expect.createSpy; - -const github = { - issues: { - addLabels: createSpy() - } -}; -const context = new Context(github, {}, {payload}); - -describe('action.label', () => { - it('adds a label', () => { - action(context, 'hello'); - expect(github.issues.addLabels).toHaveBeenCalledWith({ - owner: 'bkeepers-inc', - repo: 'test', - number: 6, - body: ['hello'] - }); - }); - - it('adds multiple labels', () => { - action(context, ['hello', 'world']); - expect(github.issues.addLabels).toHaveBeenCalledWith({ - owner: 'bkeepers-inc', - repo: 'test', - number: 6, - body: ['hello'] - }); - }); -}); diff --git a/test/plugins/issues.js b/test/plugins/issues.js index f30b595..c2bc9ae 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -9,17 +9,21 @@ const createSpy = expect.createSpy; const github = { issues: { - edit: createSpy() + edit: createSpy(), + addLabels: createSpy(), } }; const context = new Context(github, {}, {payload}); describe('issues plugin', () => { - describe('close', () => { + before( () => { + w = new workflow.Workflow(); + evaluator = new issues.Evaluator; + }) + describe('closing', () => { it('closes an issue', () => { - w = new workflow.Workflow(); w.close() - evaluator = new issues.Evaluator; + Promise.all(evaluator.evaluate(w, context)); expect(github.issues.edit).toHaveBeenCalledWith({ owner: 'bkeepers-inc', @@ -29,4 +33,30 @@ describe('issues plugin', () => { }); }); }) + + describe('labels', () => { + it('adds a label', () => { + w.label('hello'); + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.addLabels).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + body: ['hello'] + }); + }); + + it('adds multiple labels', () => { + w.label('hello', 'world'); + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.addLabels).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + body: ['hello', 'world'] + }); + }); + }); }) From 3802c1d115da7f0470d878fda4d51d57941d6047 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 11:20:40 -0800 Subject: [PATCH 12/29] Implement comment test --- test/actions/comment.js | 25 ------------------------- test/plugins/issues.js | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 25 deletions(-) delete mode 100644 test/actions/comment.js diff --git a/test/actions/comment.js b/test/actions/comment.js deleted file mode 100644 index d42d1ae..0000000 --- a/test/actions/comment.js +++ /dev/null @@ -1,25 +0,0 @@ -const expect = require('expect'); -const action = require('../../lib/actions/comment'); -const Context = require('../../lib/context'); -const payload = require('../fixtures/webhook/comment.created.json'); - -const createSpy = expect.createSpy; - -const github = { - issues: { - createComment: createSpy() - } -}; - -describe('action.comment', () => { - it('creates a comment', () => { - const context = new Context(github, {}, {payload}); - action(context, 'Hello @{{ sender.login }}!'); - expect(github.issues.createComment).toHaveBeenCalledWith({ - owner: 'bkeepers-inc', - repo: 'test', - number: 6, - body: 'Hello @bkeepers!' - }); - }); -}); diff --git a/test/plugins/issues.js b/test/plugins/issues.js index c2bc9ae..ecbed8a 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -11,6 +11,7 @@ const github = { issues: { edit: createSpy(), addLabels: createSpy(), + createComment: createSpy(), } }; const context = new Context(github, {}, {payload}); @@ -59,4 +60,18 @@ describe('issues plugin', () => { }); }); }); + + describe('comments', () => { + it('creates a comment', () => { + w.comment('Hello world!'); + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.createComment).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + body: 'Hello world!' + }); + }); + }); }) From 5b68c71d72a0c9c2dd598d5d7c1d31b97ef335b1 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 11:25:27 -0800 Subject: [PATCH 13/29] Implement issue assignment --- lib/actions.js | 10 ---------- lib/actions/assign.js | 16 ---------------- lib/plugins/issues.js | 10 +++++++++- test/actions/assign.js | 36 ------------------------------------ 4 files changed, 9 insertions(+), 63 deletions(-) delete mode 100644 lib/actions.js delete mode 100644 lib/actions/assign.js delete mode 100644 test/actions/assign.js diff --git a/lib/actions.js b/lib/actions.js deleted file mode 100644 index 2590d38..0000000 --- a/lib/actions.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - assign: require('./actions/assign'), - lock: require('./actions/lock'), - open: require('./actions/open'), - unassign: require('./actions/unassign'), - unlabel: require('./actions/unlabel'), - unlock: require('./actions/unlock'), - react: require('./actions/react'), - updateIssue: require('./actions/update-issue') -}; diff --git a/lib/actions/assign.js b/lib/actions/assign.js deleted file mode 100644 index 527a642..0000000 --- a/lib/actions/assign.js +++ /dev/null @@ -1,16 +0,0 @@ -// Assign one or more users to an issue or pull request. -// -// ``` -// # Assign a single user -// then assign(bkeepers); -// -// # Assign multiple users -// then assign(bkeepers, benbalter); -// ``` -// - -module.exports = function (context, ...logins) { - return context.github.issues.addAssigneesToIssue( - context.payload.toIssue({assignees: logins}) - ); -}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index bfdd68b..c89c610 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -7,7 +7,8 @@ let IssuePlugin = (superclass) => class extends superclass { return this; } - assign(...users) { + assign(...assignees) { + this._setCommentData({assignees: assignees}) return this; } @@ -78,6 +79,13 @@ class IssueEvaluator extends base.Evaluator { })); } + if (workflow.issueActions.assignees !== undefined) { + let assignees = workflow.issueActions.assignees; + promises.push( + context.github.issues.addAssigneesToIssue(context.payload.toIssue({assignees: assignees})) + ); + } + if (workflow.issueActions.labels !== undefined) { let labels = workflow.issueActions.labels; promises.push( diff --git a/test/actions/assign.js b/test/actions/assign.js deleted file mode 100644 index 6ce75f1..0000000 --- a/test/actions/assign.js +++ /dev/null @@ -1,36 +0,0 @@ -const expect = require('expect'); -const action = require('../../lib/actions/assign'); -const Context = require('../../lib/context'); -const payload = require('../fixtures/webhook/comment.created.json'); - -const createSpy = expect.createSpy; - -const github = { - issues: { - addAssigneesToIssue: createSpy() - } -}; - -const context = new Context(github, {}, {payload}); - -describe('action.assign', () => { - it('assigns a user', () => { - action(context, 'bkeepers'); - expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({ - owner: 'bkeepers-inc', - repo: 'test', - number: 6, - assignees: ['bkeepers'] - }); - }); - - it('assigns multiple users', () => { - action(context, 'hello', 'world'); - expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({ - owner: 'bkeepers-inc', - repo: 'test', - number: 6, - assignees: ['hello', 'world'] - }); - }); -}); From f0f44afadc944b0fce61f9aaf955c69d161538b6 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 11:38:56 -0800 Subject: [PATCH 14/29] Implement unassign --- lib/actions/unassign.js | 16 ------------- lib/plugins/issues.js | 12 ++++++++-- test/actions/unassign.js | 35 --------------------------- test/plugins/issues.js | 52 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 53 deletions(-) delete mode 100644 lib/actions/unassign.js delete mode 100644 test/actions/unassign.js diff --git a/lib/actions/unassign.js b/lib/actions/unassign.js deleted file mode 100644 index 936e02d..0000000 --- a/lib/actions/unassign.js +++ /dev/null @@ -1,16 +0,0 @@ -// Unassign one or more users from an issue or pull request. -// -// ``` -// # Unassign a single user -// then unassign(bkeepers); -// -// # Unassign multiple users -// then unassign(bkeepers, benbalter); -// ``` -// - -module.exports = function (context, ...logins) { - return context.github.issues.removeAssigneesFromIssue( - context.payload.toIssue({body: {assignees: logins}}) - ); -}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index c89c610..8dd4388 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -12,11 +12,12 @@ let IssuePlugin = (superclass) => class extends superclass { return this; } - react(...users) { + unassign(...assignees) { + this._setCommentData({unassignees: assignees}) return this; } - unassign(...users) { + react(...users) { return this; } @@ -86,6 +87,13 @@ class IssueEvaluator extends base.Evaluator { ); } + if (workflow.issueActions.unassignees !== undefined) { + let assignees = { assignees: workflow.issueActions.unassignees }; + promises.push( + context.github.issues.removeAssigneesFromIssue(context.payload.toIssue({body: assignees})) + ); + } + if (workflow.issueActions.labels !== undefined) { let labels = workflow.issueActions.labels; promises.push( diff --git a/test/actions/unassign.js b/test/actions/unassign.js deleted file mode 100644 index f4f351b..0000000 --- a/test/actions/unassign.js +++ /dev/null @@ -1,35 +0,0 @@ -const expect = require('expect'); -const action = require('../../lib/actions/unassign'); -const Context = require('../../lib/context'); -const payload = require('../fixtures/webhook/comment.created.json'); - -const createSpy = expect.createSpy; - -const github = { - issues: { - removeAssigneesFromIssue: createSpy() - } -}; -const context = new Context(github, {}, {payload}); - -describe('action.unassign', () => { - it('unassigns a user', () => { - action(context, 'bkeepers'); - expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({ - owner: 'bkeepers-inc', - repo: 'test', - number: 6, - body: {assignees: ['bkeepers']} - }); - }); - - it('unassigns multiple users', () => { - action(context, 'hello', 'world'); - expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({ - owner: 'bkeepers-inc', - repo: 'test', - number: 6, - body: {assignees: ['hello', 'world']} - }); - }); -}); diff --git a/test/plugins/issues.js b/test/plugins/issues.js index ecbed8a..d6345d8 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -12,6 +12,8 @@ const github = { edit: createSpy(), addLabels: createSpy(), createComment: createSpy(), + addAssigneesToIssue: createSpy(), + removeAssigneesFromIssue: createSpy(), } }; const context = new Context(github, {}, {payload}); @@ -74,4 +76,54 @@ describe('issues plugin', () => { }); }); }); + + describe('assignment', () => { + it('assigns a user', () => { + w.assign('bkeepers'); + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + assignees: ['bkeepers'] + }); + }); + + it('assigns multiple users', () => { + w.assign('hello', 'world'); + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + assignees: ['hello', 'world'] + }); + }); + + it('unassigns a user', () => { + w.unassign('bkeepers'); + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + body: {assignees: ['bkeepers']} + }); + }); + + it('unassigns multiple users', () => { + w.unassign('hello', 'world'); + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + body: {assignees: ['hello', 'world']} + }); + }); + }); }) From 8d965b1ce920ace60afce70992f8c0b139e05b4d Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 11:47:03 -0800 Subject: [PATCH 15/29] Implement unlabel --- lib/plugins/issues.js | 17 +++++++++++++++++ test/actions/unlabel.js | 42 ----------------------------------------- test/plugins/issues.js | 32 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 42 deletions(-) delete mode 100644 test/actions/unlabel.js diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 8dd4388..73d335b 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -26,6 +26,11 @@ let IssuePlugin = (superclass) => class extends superclass { return this; } + unlabel(...labels) { + this._setCommentData({unlabels: labels}) + return this; + } + lock() { return this; } @@ -101,6 +106,18 @@ class IssueEvaluator extends base.Evaluator { ); } + if (workflow.issueActions.unlabels !== undefined) { + let labels = workflow.issueActions.unlabels; + promises.push( + labels.map(label => { + return context.github.issues.removeLabel( + context.payload.toIssue({name: label}) + ) + }) + ); + } + + if (workflow.issueActions.close !== undefined) { promises.push( context.github.issues.edit(context.payload.toIssue({state: "closed"})) diff --git a/test/actions/unlabel.js b/test/actions/unlabel.js deleted file mode 100644 index 4d21409..0000000 --- a/test/actions/unlabel.js +++ /dev/null @@ -1,42 +0,0 @@ -const expect = require('expect'); -const action = require('../../lib/actions/unlabel'); -const Context = require('../../lib/context'); -const payload = require('../fixtures/webhook/comment.created.json'); - -const createSpy = expect.createSpy; - -const github = { - issues: { - removeLabel: createSpy() - } -}; -const context = new Context(github, {}, {payload}); - -describe('action.unlabel', () => { - it('removes a single label', () => { - action(context, 'hello'); - expect(github.issues.removeLabel).toHaveBeenCalledWith({ - owner: 'bkeepers-inc', - repo: 'test', - number: 6, - name: 'hello' - }); - }); - - it('removes a multiple labels', () => { - action(context, 'hello', 'goodbye'); - expect(github.issues.removeLabel).toHaveBeenCalledWith({ - owner: 'bkeepers-inc', - repo: 'test', - number: 6, - name: 'hello' - }); - - expect(github.issues.removeLabel).toHaveBeenCalledWith({ - owner: 'bkeepers-inc', - repo: 'test', - number: 6, - name: 'goodbye' - }); - }); -}); diff --git a/test/plugins/issues.js b/test/plugins/issues.js index d6345d8..12e6f80 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -14,6 +14,7 @@ const github = { createComment: createSpy(), addAssigneesToIssue: createSpy(), removeAssigneesFromIssue: createSpy(), + removeLabel: createSpy(), } }; const context = new Context(github, {}, {payload}); @@ -61,6 +62,37 @@ describe('issues plugin', () => { body: ['hello', 'world'] }); }); + + it('removes a single label', () => { + w.unlabel('hello'); + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.removeLabel).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + name: 'hello' + }); + }); + + it('removes a multiple labels', () => { + w.unlabel('hello', 'goodbye'); + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.removeLabel).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + name: 'hello' + }); + + expect(github.issues.removeLabel).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + name: 'goodbye' + }); + }); }); describe('comments', () => { From af85ae1beefb7e09f66ae6add32931b03bf2e2c8 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 11:52:12 -0800 Subject: [PATCH 16/29] Implement open --- lib/actions/open.js | 5 ----- lib/actions/unlabel.js | 9 --------- lib/plugins/issues.js | 10 ++++++++++ test/plugins/issues.js | 13 ++++++++++++- 4 files changed, 22 insertions(+), 15 deletions(-) delete mode 100644 lib/actions/open.js delete mode 100644 lib/actions/unlabel.js diff --git a/lib/actions/open.js b/lib/actions/open.js deleted file mode 100644 index 27662cd..0000000 --- a/lib/actions/open.js +++ /dev/null @@ -1,5 +0,0 @@ -const updateIssue = require('./update-issue'); - -module.exports = function (context) { - return updateIssue(context, {state: 'open'}); -}; diff --git a/lib/actions/unlabel.js b/lib/actions/unlabel.js deleted file mode 100644 index d4df621..0000000 --- a/lib/actions/unlabel.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = (context, ...labels) => { - // FIXME: check that it has the label first, or handle expected error: - // {"message":"Label does not exist","documentation_url":"https://developer.github.com/v3/issues/labels/#remove-a-label-from-an-issue"} - return Promise.all(labels.map(label => { - return context.github.issues.removeLabel( - context.payload.toIssue({name: label}) - ); - })); -}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 73d335b..3f2cbeb 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -35,6 +35,11 @@ let IssuePlugin = (superclass) => class extends superclass { return this; } + open(){ + this._setCommentData({open: true}) + return this; + } + close(){ this._setCommentData({close: true}) return this; @@ -117,6 +122,11 @@ class IssueEvaluator extends base.Evaluator { ); } + if (workflow.issueActions.open !== undefined) { + promises.push( + context.github.issues.edit(context.payload.toIssue({state: "open"})) + ); + } if (workflow.issueActions.close !== undefined) { promises.push( diff --git a/test/plugins/issues.js b/test/plugins/issues.js index 12e6f80..3541cde 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -24,7 +24,18 @@ describe('issues plugin', () => { w = new workflow.Workflow(); evaluator = new issues.Evaluator; }) - describe('closing', () => { + describe('state', () => { + it('opens an issue', () => { + w.open() + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.edit).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + state: 'open', + }); + }); it('closes an issue', () => { w.close() From 24d2c214209f814225c77f2bb15fc17478f98582 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 11:57:28 -0800 Subject: [PATCH 17/29] Implement locking --- lib/actions/lock.js | 3 --- lib/actions/update-issue.js | 4 ---- lib/plugins/issues.js | 7 +++++++ test/plugins/issues.js | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 7 deletions(-) delete mode 100644 lib/actions/lock.js delete mode 100644 lib/actions/update-issue.js diff --git a/lib/actions/lock.js b/lib/actions/lock.js deleted file mode 100644 index 8a5cbab..0000000 --- a/lib/actions/lock.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (context) { - return context.github.issues.lock(context.payload.toIssue()); -}; diff --git a/lib/actions/update-issue.js b/lib/actions/update-issue.js deleted file mode 100644 index be003cf..0000000 --- a/lib/actions/update-issue.js +++ /dev/null @@ -1,4 +0,0 @@ -// https://mikedeboer.github.io/node-github/#api-issues-edit -module.exports = function (context, attrs) { - return context.github.issues.edit(context.payload.toIssue(attrs)); -}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 3f2cbeb..194f3fd 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -32,6 +32,7 @@ let IssuePlugin = (superclass) => class extends superclass { } lock() { + this._setCommentData({lock: true}) return this; } @@ -122,6 +123,12 @@ class IssueEvaluator extends base.Evaluator { ); } + if (workflow.issueActions.lock !== undefined) { + promises.push( + context.github.issues.lock(context.payload.toIssue({})) + ); + } + if (workflow.issueActions.open !== undefined) { promises.push( context.github.issues.edit(context.payload.toIssue({state: "open"})) diff --git a/test/plugins/issues.js b/test/plugins/issues.js index 3541cde..34e3169 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -9,6 +9,7 @@ const createSpy = expect.createSpy; const github = { issues: { + lock: createSpy(), edit: createSpy(), addLabels: createSpy(), createComment: createSpy(), @@ -24,6 +25,20 @@ describe('issues plugin', () => { w = new workflow.Workflow(); evaluator = new issues.Evaluator; }) + + describe('locking', () => { + it('locks', () => { + w.lock() + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.lock).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + }); + }); + }); + describe('state', () => { it('opens an issue', () => { w.open() From 7976c94ae7e91224130ba3f544ed8f35b24a792d Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 12:00:08 -0800 Subject: [PATCH 18/29] Implement unlock --- lib/actions/unlock.js | 3 --- lib/plugins/issues.js | 11 +++++++++++ test/plugins/issues.js | 12 ++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) delete mode 100644 lib/actions/unlock.js diff --git a/lib/actions/unlock.js b/lib/actions/unlock.js deleted file mode 100644 index 23fade2..0000000 --- a/lib/actions/unlock.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (context) { - return context.github.issues.unlock(context.payload.toIssue()); -}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 194f3fd..675546e 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -36,6 +36,11 @@ let IssuePlugin = (superclass) => class extends superclass { return this; } + unlock() { + this._setCommentData({unlock: true}) + return this; + } + open(){ this._setCommentData({open: true}) return this; @@ -129,6 +134,12 @@ class IssueEvaluator extends base.Evaluator { ); } + if (workflow.issueActions.unlock !== undefined) { + promises.push( + context.github.issues.unlock(context.payload.toIssue({})) + ); + } + if (workflow.issueActions.open !== undefined) { promises.push( context.github.issues.edit(context.payload.toIssue({state: "open"})) diff --git a/test/plugins/issues.js b/test/plugins/issues.js index 34e3169..7f6d561 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -10,6 +10,7 @@ const createSpy = expect.createSpy; const github = { issues: { lock: createSpy(), + unlock: createSpy(), edit: createSpy(), addLabels: createSpy(), createComment: createSpy(), @@ -37,6 +38,17 @@ describe('issues plugin', () => { number: 6, }); }); + + it('unlocks', () => { + w.unlock() + + Promise.all(evaluator.evaluate(w, context)); + expect(github.issues.unlock).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + }); + }); }); describe('state', () => { From 4d068ce491836bbf345fd61686e5484c7ea3c7e7 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 12:09:26 -0800 Subject: [PATCH 19/29] Reimplement reactions --- lib/actions/react.js | 6 ------ lib/plugins/issues.js | 12 ++++++++++++ test/plugins/issues.js | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) delete mode 100644 lib/actions/react.js diff --git a/lib/actions/react.js b/lib/actions/react.js deleted file mode 100644 index 0fc27c5..0000000 --- a/lib/actions/react.js +++ /dev/null @@ -1,6 +0,0 @@ - -module.exports = function (context, react) { - return context.github.reactions.createForIssue( - context.payload.toIssue({content: react}) - ); -}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 675546e..263f663 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -51,6 +51,11 @@ let IssuePlugin = (superclass) => class extends superclass { return this; } + react(reaction){ + this._setCommentData({reaction: reaction}) + return this; + } + _setCommentData(obj) { if (this.issueActions === undefined) { this.issueActions = {} @@ -152,6 +157,13 @@ class IssueEvaluator extends base.Evaluator { ); } + if (workflow.issueActions.reaction !== undefined) { + let reaction = workflow.issueActions.reaction; + promises.push( + context.github.reactions.createForIssue(context.payload.toIssue({content: reaction})) + ); + } + return promises; } } diff --git a/test/plugins/issues.js b/test/plugins/issues.js index 7f6d561..8b96a16 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -8,6 +8,9 @@ const payload = require('../fixtures/webhook/comment.created.json'); const createSpy = expect.createSpy; const github = { + reactions: { + createForIssue: createSpy(), + }, issues: { lock: createSpy(), unlock: createSpy(), @@ -196,4 +199,18 @@ describe('issues plugin', () => { }); }); }); + + describe('reactions', () => { + it('react', () => { + w.react("heart") + + Promise.all(evaluator.evaluate(w, context)); + expect(github.reactions.createForIssue).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + content: 'heart', + }); + }); + }); }) From 11040392e2d7fa0d80b3a544b66022ace93bcd98 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 12:16:13 -0800 Subject: [PATCH 20/29] Fix style --- lib/configuration.js | 14 +++++------ lib/dispatcher.js | 10 ++++---- lib/plugins/base.js | 9 +++---- lib/plugins/issues.js | 56 +++++++++++++++++++++--------------------- lib/utils/url.js | 4 +-- lib/workflow.js | 18 +++++++------- test/dispatcher.js | 10 ++++---- test/plugins/issues.js | 32 ++++++++++++------------ 8 files changed, 76 insertions(+), 77 deletions(-) diff --git a/lib/configuration.js b/lib/configuration.js index 0cd1783..7445dd8 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -1,5 +1,5 @@ -const debug = require('debug')('PRobot'); const vm = require('vm'); +const debug = require('debug')('PRobot'); const workflow = require('./workflow'); const URL = require('./utils/url'); @@ -20,12 +20,12 @@ module.exports = class Configuration { }); } - static parse(content){ + static parse(content) { const sandbox = { on: workflow.on, - URL: URL, - workflows: [], - } + URL, + workflows: [] + }; vm.createContext(sandbox); vm.runInContext(content, sandbox); return new Configuration(sandbox.workflows); @@ -36,8 +36,8 @@ module.exports = class Configuration { } workflowsFor(event) { - return this.workflows.filter((w) => { - return w.matchesEvent(event) && w.filterFn(event) + return this.workflows.filter(w => { + return w.matchesEvent(event) && w.filterFn(event); }); } }; diff --git a/lib/dispatcher.js b/lib/dispatcher.js index efd15d4..30020ec 100644 --- a/lib/dispatcher.js +++ b/lib/dispatcher.js @@ -13,16 +13,16 @@ class Dispatcher { const workflows = config.workflowsFor(this.event); // FIXME: have a better method to register evaluators - let evaluators = [ - issues.Evaluator, - ] + const evaluators = [ + issues.Evaluator + ]; // Handle all behaviors return Promise.all(workflows.map(w => { - evaluators.forEach((e) => { + evaluators.forEach(e => { let evaluator = new e; return evaluator.evaluate(w, context); - }) + }); })); } } diff --git a/lib/plugins/base.js b/lib/plugins/base.js index 280b56b..0b068c9 100644 --- a/lib/plugins/base.js +++ b/lib/plugins/base.js @@ -1,5 +1,5 @@ // This is some stuff to handle mixing in plugins into a base class -let mix = (superclass) => new MixinBuilder(superclass); +let mix = superclass => new MixinBuilder(superclass); class MixinBuilder { constructor(superclass) { @@ -15,13 +15,12 @@ class EvaluatorBase { // Public function to either pass the string through or lookup network data resolveData(data, fn) { // TODO make a network to fetch the data - fn(data) + fn(data); return this; } } - module.exports = { mix: mix, - Evaluator: EvaluatorBase, -} + Evaluator: EvaluatorBase +}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 263f663..dd627fb 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -1,19 +1,19 @@ -const base = require('./base') +const base = require('./base'); // These methods will be exposed to the sandboxed environment -let IssuePlugin = (superclass) => class extends superclass { +let IssuePlugin = superclass => class extends superclass { comment(content) { - this._setCommentData({content: content}) + this._setCommentData({content: content}); return this; } assign(...assignees) { - this._setCommentData({assignees: assignees}) + this._setCommentData({assignees: assignees}); return this; } unassign(...assignees) { - this._setCommentData({unassignees: assignees}) + this._setCommentData({unassignees: assignees}); return this; } @@ -22,53 +22,53 @@ let IssuePlugin = (superclass) => class extends superclass { } label(...labels) { - this._setCommentData({labels: labels}) + this._setCommentData({labels: labels}); return this; } unlabel(...labels) { - this._setCommentData({unlabels: labels}) + this._setCommentData({unlabels: labels}); return this; } lock() { - this._setCommentData({lock: true}) + this._setCommentData({lock: true}); return this; } unlock() { - this._setCommentData({unlock: true}) + this._setCommentData({unlock: true}); return this; } - open(){ - this._setCommentData({open: true}) + open() { + this._setCommentData({open: true}); return this; } - close(){ - this._setCommentData({close: true}) + close() { + this._setCommentData({close: true}); return this; } - react(reaction){ - this._setCommentData({reaction: reaction}) + react(reaction) { + this._setCommentData({reaction: reaction}); return this; } _setCommentData(obj) { if (this.issueActions === undefined) { - this.issueActions = {} + this.issueActions = {}; } for (var prop in obj) { - this.issueActions[prop] = obj[prop] + this.issueActions[prop] = obj[prop]; } } -} +}; // This is the function that implements all of the actions configured above. class IssueEvaluator extends base.Evaluator { - evaluate (workflow, context) { + evaluate(workflow, context) { let event = context.event; // Bail if no issue related actions @@ -89,15 +89,15 @@ class IssueEvaluator extends base.Evaluator { if (workflow.issueActions.content !== undefined) { promises.push(new Promise((resolve, reject) => { - this.resolveData(workflow.issueActions.content, (data) => { + this.resolveData(workflow.issueActions.content, data => { context.github.issues.createComment(context.payload.toIssue({body: data}, (err, res) => { if (err) { reject(); - }else{ + } else { resolve(); } - })) - }) + })); + }); })); } @@ -109,7 +109,7 @@ class IssueEvaluator extends base.Evaluator { } if (workflow.issueActions.unassignees !== undefined) { - let assignees = { assignees: workflow.issueActions.unassignees }; + let assignees = {assignees: workflow.issueActions.unassignees}; promises.push( context.github.issues.removeAssigneesFromIssue(context.payload.toIssue({body: assignees})) ); @@ -128,7 +128,7 @@ class IssueEvaluator extends base.Evaluator { labels.map(label => { return context.github.issues.removeLabel( context.payload.toIssue({name: label}) - ) + ); }) ); } @@ -147,13 +147,13 @@ class IssueEvaluator extends base.Evaluator { if (workflow.issueActions.open !== undefined) { promises.push( - context.github.issues.edit(context.payload.toIssue({state: "open"})) + context.github.issues.edit(context.payload.toIssue({state: 'open'})) ); } if (workflow.issueActions.close !== undefined) { promises.push( - context.github.issues.edit(context.payload.toIssue({state: "closed"})) + context.github.issues.edit(context.payload.toIssue({state: 'closed'})) ); } @@ -171,4 +171,4 @@ class IssueEvaluator extends base.Evaluator { module.exports = { Plugin: IssuePlugin, Evaluator: IssueEvaluator -} +}; diff --git a/lib/utils/url.js b/lib/utils/url.js index 2e0552e..3f53170 100644 --- a/lib/utils/url.js +++ b/lib/utils/url.js @@ -1,6 +1,6 @@ module.exports = class URL { constructor(url) { - this.url = url + this.url = url; } -} +}; diff --git a/lib/workflow.js b/lib/workflow.js index f167e8e..28c5762 100644 --- a/lib/workflow.js +++ b/lib/workflow.js @@ -1,10 +1,10 @@ -const plugin = require('./plugins/base') -const issues = require('./plugins/issues') +const plugin = require('./plugins/base'); +const issues = require('./plugins/issues'); class WorkflowCore { constructor(events) { - this.events = events - this.filterFn = () => true + this.events = events; + this.filterFn = () => true; } filter(fn) { @@ -15,8 +15,8 @@ class WorkflowCore { matchesEvent(event) { let eventWithAction = [event.event, event.payload.action]; - return this.events.find((e) => { - let parts = e.split("."); + return this.events.find(e => { + let parts = e.split('.'); for (let i = 0; i < parts.length; i++) { if (parts[i] != eventWithAction[i]) { return false; @@ -30,15 +30,15 @@ class WorkflowCore { // FIXME: issues plugins = [ issues.Plugin -] +]; class Workflow extends plugin.mix(WorkflowCore).with(...plugins) {} on = (...events) => { return new Workflow(events); -} +}; module.exports = { Workflow: Workflow, on: on -} +}; diff --git a/test/dispatcher.js b/test/dispatcher.js index cb82784..2540142 100644 --- a/test/dispatcher.js +++ b/test/dispatcher.js @@ -22,7 +22,7 @@ describe('dispatch', () => { describe('reply to new issue with a comment', () => { it('posts a coment', () => { - const config = Configuration.parse('workflows.push(on("issues").comment("Hello World!"))') + const config = Configuration.parse('workflows.push(on("issues").comment("Hello World!"))'); return dispatcher.call(config).then(() => { expect(github.issues.createComment).toHaveBeenCalled(); }); @@ -31,7 +31,7 @@ describe('dispatch', () => { describe('reply to new issue with a comment', () => { it('calls the action', () => { - const config = Configuration.parse('workflows.push(on("issues.created").comment("Hello World!"))') + const config = Configuration.parse('workflows.push(on("issues.created").comment("Hello World!"))'); return dispatcher.call(config).then(() => { expect(github.issues.createComment).toHaveBeenCalled(); @@ -41,7 +41,7 @@ describe('dispatch', () => { describe('on an event with a different action', () => { it('does not perform behavior', () => { - const config = Configuration.parse('workflows.push(on("issues.labeled").comment("Hello World!"))') + const config = Configuration.parse('workflows.push(on("issues.labeled").comment("Hello World!"))'); return dispatcher.call(config).then(() => { expect(github.issues.createComment).toNotHaveBeenCalled(); @@ -59,14 +59,14 @@ describe('dispatch', () => { }); it('calls action when condition matches', () => { - const config = Configuration.parse('workflows.push(on("issues.labeled").filter((e) => e.payload.label.name == "bug").close())') + const config = Configuration.parse('workflows.push(on("issues.labeled").filter((e) => e.payload.label.name == "bug").close())'); return dispatcher.call(config).then(() => { expect(github.issues.edit).toHaveBeenCalled(); }); }); it('does not call action when conditions do not match', () => { - const config = Configuration.parse('workflows.push(on("issues.labeled").filter((e) => e.payload.label.name == "foobar").close())') + const config = Configuration.parse('workflows.push(on("issues.labeled").filter((e) => e.payload.label.name == "foobar").close())'); return dispatcher.call(config).then(() => { expect(github.issues.edit).toNotHaveBeenCalled(); }); diff --git a/test/plugins/issues.js b/test/plugins/issues.js index 8b96a16..8cbdcc5 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -9,7 +9,7 @@ const createSpy = expect.createSpy; const github = { reactions: { - createForIssue: createSpy(), + createForIssue: createSpy() }, issues: { lock: createSpy(), @@ -19,65 +19,65 @@ const github = { createComment: createSpy(), addAssigneesToIssue: createSpy(), removeAssigneesFromIssue: createSpy(), - removeLabel: createSpy(), + removeLabel: createSpy() } }; const context = new Context(github, {}, {payload}); describe('issues plugin', () => { - before( () => { + before(() => { w = new workflow.Workflow(); evaluator = new issues.Evaluator; - }) + }); describe('locking', () => { it('locks', () => { - w.lock() + w.lock(); Promise.all(evaluator.evaluate(w, context)); expect(github.issues.lock).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', - number: 6, + number: 6 }); }); it('unlocks', () => { - w.unlock() + w.unlock(); Promise.all(evaluator.evaluate(w, context)); expect(github.issues.unlock).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', - number: 6, + number: 6 }); }); }); describe('state', () => { it('opens an issue', () => { - w.open() + w.open(); Promise.all(evaluator.evaluate(w, context)); expect(github.issues.edit).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', number: 6, - state: 'open', + state: 'open' }); }); it('closes an issue', () => { - w.close() + w.close(); Promise.all(evaluator.evaluate(w, context)); expect(github.issues.edit).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', number: 6, - state: 'closed', + state: 'closed' }); }); - }) + }); describe('labels', () => { it('adds a label', () => { @@ -202,15 +202,15 @@ describe('issues plugin', () => { describe('reactions', () => { it('react', () => { - w.react("heart") + w.react('heart'); Promise.all(evaluator.evaluate(w, context)); expect(github.reactions.createForIssue).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', number: 6, - content: 'heart', + content: 'heart' }); }); }); -}) +}); From db87bcec1710d4f671013314feaebecb22f4b59a Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 17 Nov 2016 13:26:58 -0800 Subject: [PATCH 21/29] More style fixes --- lib/dispatcher.js | 8 +++--- lib/plugins/base.js | 7 +++-- lib/plugins/issues.js | 39 +++++++++++---------------- lib/workflow.js | 14 +++++----- test/plugins/issues.js | 61 +++++++++++++++++++++--------------------- 5 files changed, 60 insertions(+), 69 deletions(-) diff --git a/lib/dispatcher.js b/lib/dispatcher.js index 30020ec..dc87d64 100644 --- a/lib/dispatcher.js +++ b/lib/dispatcher.js @@ -19,11 +19,13 @@ class Dispatcher { // Handle all behaviors return Promise.all(workflows.map(w => { - evaluators.forEach(e => { - let evaluator = new e; + return evaluators.map(E => { + const evaluator = new E(); return evaluator.evaluate(w, context); }); - })); + }).reduce((a, b) => { + return a.concat(b); + }, [])); } } diff --git a/lib/plugins/base.js b/lib/plugins/base.js index 0b068c9..6c557f8 100644 --- a/lib/plugins/base.js +++ b/lib/plugins/base.js @@ -1,6 +1,3 @@ -// This is some stuff to handle mixing in plugins into a base class -let mix = superclass => new MixinBuilder(superclass); - class MixinBuilder { constructor(superclass) { this.superclass = superclass; @@ -11,6 +8,8 @@ class MixinBuilder { } } +const mix = superclass => new MixinBuilder(superclass); + class EvaluatorBase { // Public function to either pass the string through or lookup network data resolveData(data, fn) { @@ -21,6 +20,6 @@ class EvaluatorBase { } module.exports = { - mix: mix, + mix, Evaluator: EvaluatorBase }; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index dd627fb..ca72955 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -1,14 +1,13 @@ const base = require('./base'); -// These methods will be exposed to the sandboxed environment -let IssuePlugin = superclass => class extends superclass { +const IssuePlugin = superclass => class extends superclass { comment(content) { - this._setCommentData({content: content}); + this._setCommentData({content}); return this; } assign(...assignees) { - this._setCommentData({assignees: assignees}); + this._setCommentData({assignees}); return this; } @@ -17,12 +16,8 @@ let IssuePlugin = superclass => class extends superclass { return this; } - react(...users) { - return this; - } - label(...labels) { - this._setCommentData({labels: labels}); + this._setCommentData({labels}); return this; } @@ -52,7 +47,7 @@ let IssuePlugin = superclass => class extends superclass { } react(reaction) { - this._setCommentData({reaction: reaction}); + this._setCommentData({reaction}); return this; } @@ -60,16 +55,14 @@ let IssuePlugin = superclass => class extends superclass { if (this.issueActions === undefined) { this.issueActions = {}; } - for (var prop in obj) { - this.issueActions[prop] = obj[prop]; - } + Object.assign(this.issueActions, obj); } }; // This is the function that implements all of the actions configured above. class IssueEvaluator extends base.Evaluator { evaluate(workflow, context) { - let event = context.event; + const event = context.event; // Bail if no issue related actions if (workflow.issueActions === undefined) { @@ -83,14 +76,12 @@ class IssueEvaluator extends base.Evaluator { if (event.issue !== undefined && event.pull_request !== undefined) { return; } - let item = event.issue || event.pull_request; - - let promises = []; + const promises = []; if (workflow.issueActions.content !== undefined) { promises.push(new Promise((resolve, reject) => { this.resolveData(workflow.issueActions.content, data => { - context.github.issues.createComment(context.payload.toIssue({body: data}, (err, res) => { + context.github.issues.createComment(context.payload.toIssue({body: data}, err => { if (err) { reject(); } else { @@ -102,28 +93,28 @@ class IssueEvaluator extends base.Evaluator { } if (workflow.issueActions.assignees !== undefined) { - let assignees = workflow.issueActions.assignees; + const assignees = workflow.issueActions.assignees; promises.push( - context.github.issues.addAssigneesToIssue(context.payload.toIssue({assignees: assignees})) + context.github.issues.addAssigneesToIssue(context.payload.toIssue({assignees})) ); } if (workflow.issueActions.unassignees !== undefined) { - let assignees = {assignees: workflow.issueActions.unassignees}; + const assignees = {assignees: workflow.issueActions.unassignees}; promises.push( context.github.issues.removeAssigneesFromIssue(context.payload.toIssue({body: assignees})) ); } if (workflow.issueActions.labels !== undefined) { - let labels = workflow.issueActions.labels; + const labels = workflow.issueActions.labels; promises.push( context.github.issues.addLabels(context.payload.toIssue({body: labels})) ); } if (workflow.issueActions.unlabels !== undefined) { - let labels = workflow.issueActions.unlabels; + const labels = workflow.issueActions.unlabels; promises.push( labels.map(label => { return context.github.issues.removeLabel( @@ -158,7 +149,7 @@ class IssueEvaluator extends base.Evaluator { } if (workflow.issueActions.reaction !== undefined) { - let reaction = workflow.issueActions.reaction; + const reaction = workflow.issueActions.reaction; promises.push( context.github.reactions.createForIssue(context.payload.toIssue({content: reaction})) ); diff --git a/lib/workflow.js b/lib/workflow.js index 28c5762..0650c8f 100644 --- a/lib/workflow.js +++ b/lib/workflow.js @@ -13,12 +13,12 @@ class WorkflowCore { } matchesEvent(event) { - let eventWithAction = [event.event, event.payload.action]; + const eventWithAction = [event.event, event.payload.action]; return this.events.find(e => { - let parts = e.split('.'); + const parts = e.split('.'); for (let i = 0; i < parts.length; i++) { - if (parts[i] != eventWithAction[i]) { + if (parts[i] !== eventWithAction[i]) { return false; } } @@ -28,17 +28,17 @@ class WorkflowCore { } // FIXME: issues -plugins = [ +const plugins = [ issues.Plugin ]; class Workflow extends plugin.mix(WorkflowCore).with(...plugins) {} -on = (...events) => { +const on = (...events) => { return new Workflow(events); }; module.exports = { - Workflow: Workflow, - on: on + Workflow, + on }; diff --git a/test/plugins/issues.js b/test/plugins/issues.js index 8cbdcc5..acaeba2 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -1,7 +1,6 @@ const expect = require('expect'); const issues = require('../../lib/plugins/issues'); const workflow = require('../../lib/workflow'); -const dispatcher = require('../../lib/dispatcher'); const Context = require('../../lib/context'); const payload = require('../fixtures/webhook/comment.created.json'); @@ -26,15 +25,15 @@ const context = new Context(github, {}, {payload}); describe('issues plugin', () => { before(() => { - w = new workflow.Workflow(); - evaluator = new issues.Evaluator; + this.w = new workflow.Workflow(); + this.evaluator = new issues.Evaluator(); }); describe('locking', () => { it('locks', () => { - w.lock(); + this.w.lock(); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.lock).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -43,9 +42,9 @@ describe('issues plugin', () => { }); it('unlocks', () => { - w.unlock(); + this.w.unlock(); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.unlock).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -56,9 +55,9 @@ describe('issues plugin', () => { describe('state', () => { it('opens an issue', () => { - w.open(); + this.w.open(); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.edit).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -67,9 +66,9 @@ describe('issues plugin', () => { }); }); it('closes an issue', () => { - w.close(); + this.w.close(); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.edit).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -81,9 +80,9 @@ describe('issues plugin', () => { describe('labels', () => { it('adds a label', () => { - w.label('hello'); + this.w.label('hello'); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.addLabels).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -93,9 +92,9 @@ describe('issues plugin', () => { }); it('adds multiple labels', () => { - w.label('hello', 'world'); + this.w.label('hello', 'world'); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.addLabels).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -105,9 +104,9 @@ describe('issues plugin', () => { }); it('removes a single label', () => { - w.unlabel('hello'); + this.w.unlabel('hello'); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.removeLabel).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -117,9 +116,9 @@ describe('issues plugin', () => { }); it('removes a multiple labels', () => { - w.unlabel('hello', 'goodbye'); + this.w.unlabel('hello', 'goodbye'); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.removeLabel).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -138,9 +137,9 @@ describe('issues plugin', () => { describe('comments', () => { it('creates a comment', () => { - w.comment('Hello world!'); + this.w.comment('Hello world!'); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.createComment).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -152,9 +151,9 @@ describe('issues plugin', () => { describe('assignment', () => { it('assigns a user', () => { - w.assign('bkeepers'); + this.w.assign('bkeepers'); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -164,9 +163,9 @@ describe('issues plugin', () => { }); it('assigns multiple users', () => { - w.assign('hello', 'world'); + this.w.assign('hello', 'world'); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -176,9 +175,9 @@ describe('issues plugin', () => { }); it('unassigns a user', () => { - w.unassign('bkeepers'); + this.w.unassign('bkeepers'); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -188,9 +187,9 @@ describe('issues plugin', () => { }); it('unassigns multiple users', () => { - w.unassign('hello', 'world'); + this.w.unassign('hello', 'world'); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', @@ -202,9 +201,9 @@ describe('issues plugin', () => { describe('reactions', () => { it('react', () => { - w.react('heart'); + this.w.react('heart'); - Promise.all(evaluator.evaluate(w, context)); + Promise.all(this.evaluator.evaluate(this.w, context)); expect(github.reactions.createForIssue).toHaveBeenCalledWith({ owner: 'bkeepers-inc', repo: 'test', From 72f27464ceded26819fe1ca616ed5a26835f2f6a Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 17 Nov 2016 18:49:31 -0600 Subject: [PATCH 22/29] Remove unused modules --- package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/package.json b/package.json index 298a9cb..d9739d1 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,7 @@ "expect": "^1.20.2", "github": "^5.2.0", "github-webhook-handler": "^0.6.0", - "handlebars": "^4.0.5", - "jsdoc": "^3.4.2", - "pegjs": "^0.10.0", - "pegjs-util": "^1.4.0", - "tree-walk": "^0.4.0" + "handlebars": "^4.0.5" }, "devDependencies": { "mocha": "^3.0.2", From 1946b9adc20a287473010377f8045fbe9f2a07a8 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 17 Nov 2016 19:22:09 -0600 Subject: [PATCH 23/29] Move evaluator to its own file --- lib/evaluator.js | 10 ++++++++++ lib/plugins/base.js | 25 ------------------------- lib/plugins/issues.js | 4 ++-- lib/workflow.js | 8 ++++++-- 4 files changed, 18 insertions(+), 29 deletions(-) create mode 100644 lib/evaluator.js delete mode 100644 lib/plugins/base.js diff --git a/lib/evaluator.js b/lib/evaluator.js new file mode 100644 index 0000000..e96f285 --- /dev/null +++ b/lib/evaluator.js @@ -0,0 +1,10 @@ +class Evaluator { + // Public function to either pass the string through or lookup network data + resolveData(data, fn) { + // TODO make a network to fetch the data + fn(data); + return this; + } +} + +module.exports = Evaluator; diff --git a/lib/plugins/base.js b/lib/plugins/base.js deleted file mode 100644 index 6c557f8..0000000 --- a/lib/plugins/base.js +++ /dev/null @@ -1,25 +0,0 @@ -class MixinBuilder { - constructor(superclass) { - this.superclass = superclass; - } - - with(...mixins) { - return mixins.reduce((c, mixin) => mixin(c), this.superclass); - } -} - -const mix = superclass => new MixinBuilder(superclass); - -class EvaluatorBase { - // Public function to either pass the string through or lookup network data - resolveData(data, fn) { - // TODO make a network to fetch the data - fn(data); - return this; - } -} - -module.exports = { - mix, - Evaluator: EvaluatorBase -}; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index ca72955..9739789 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -1,4 +1,4 @@ -const base = require('./base'); +const Evaluator = require('../evaluator'); const IssuePlugin = superclass => class extends superclass { comment(content) { @@ -60,7 +60,7 @@ const IssuePlugin = superclass => class extends superclass { }; // This is the function that implements all of the actions configured above. -class IssueEvaluator extends base.Evaluator { +class IssueEvaluator extends Evaluator { evaluate(workflow, context) { const event = context.event; diff --git a/lib/workflow.js b/lib/workflow.js index 0650c8f..ae688e1 100644 --- a/lib/workflow.js +++ b/lib/workflow.js @@ -1,4 +1,3 @@ -const plugin = require('./plugins/base'); const issues = require('./plugins/issues'); class WorkflowCore { @@ -32,7 +31,12 @@ const plugins = [ issues.Plugin ]; -class Workflow extends plugin.mix(WorkflowCore).with(...plugins) {} +// Helper to combine an array of mixins into one class +function mix(superclass, ...mixins) { + return mixins.reduce((c, mixin) => mixin(c), superclass); +} + +class Workflow extends mix(WorkflowCore, ...plugins) {} const on = (...events) => { return new Workflow(events); From 1bae707257f7c97f1d8cd6a541a0e1dca5999416 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 17 Nov 2016 21:05:49 -0600 Subject: [PATCH 24/29] Extract Sanbox class, remove workflows.push --- lib/configuration.js | 14 ++++---------- lib/sandbox.js | 21 +++++++++++++++++++++ lib/workflow.js | 9 +-------- test/configuration.js | 16 +++++++++++++--- test/dispatcher.js | 10 +++++----- test/plugins/issues.js | 4 ++-- 6 files changed, 46 insertions(+), 28 deletions(-) create mode 100644 lib/sandbox.js diff --git a/lib/configuration.js b/lib/configuration.js index 7445dd8..1e9deb5 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -1,8 +1,6 @@ const vm = require('vm'); const debug = require('debug')('PRobot'); - -const workflow = require('./workflow'); -const URL = require('./utils/url'); +const Sandbox = require('./sandbox'); module.exports = class Configuration { // Get bot config from target repository @@ -21,13 +19,9 @@ module.exports = class Configuration { } static parse(content) { - const sandbox = { - on: workflow.on, - URL, - workflows: [] - }; - vm.createContext(sandbox); - vm.runInContext(content, sandbox); + const sandbox = new Sandbox(); + vm.createContext(sandbox.api); + vm.runInContext(content, sandbox.api); return new Configuration(sandbox.workflows); } diff --git a/lib/sandbox.js b/lib/sandbox.js new file mode 100644 index 0000000..b6d04f6 --- /dev/null +++ b/lib/sandbox.js @@ -0,0 +1,21 @@ +const URL = require('./utils/url'); +const Workflow = require('./workflow'); + +class Sandbox { + constructor() { + this.workflows = []; + + this.api = { + on: this.on.bind(this), + URL + }; + } + + on(...events) { + const workflow = new Workflow(events); + this.workflows.push(workflow); + return workflow; + } +}; + +module.exports = Sandbox; diff --git a/lib/workflow.js b/lib/workflow.js index ae688e1..70ee131 100644 --- a/lib/workflow.js +++ b/lib/workflow.js @@ -38,11 +38,4 @@ function mix(superclass, ...mixins) { class Workflow extends mix(WorkflowCore, ...plugins) {} -const on = (...events) => { - return new Workflow(events); -}; - -module.exports = { - Workflow, - on -}; +module.exports = Workflow; diff --git a/test/configuration.js b/test/configuration.js index debb26c..1287a86 100644 --- a/test/configuration.js +++ b/test/configuration.js @@ -4,6 +4,16 @@ const config = require('./fixtures/content/probot.json'); const createSpy = expect.createSpy; +config.content = new Buffer(` + on("issues.opened") + .comment("Hello World!") + .assign("bkeepers") + .react("heart"); + + on("issues.closed") + .unassign("bkeepers"); +`).toString('base64'); + describe('Configuration', () => { describe('load', () => { let github; @@ -35,9 +45,9 @@ describe('Configuration', () => { describe('workflowsFor', () => { const config = Configuration.parse(` - workflows.push(on("issues").label("active")); - workflows.push(on("issues.created").close()); - workflows.push(on("pull_request.labeled").lock()); + on("issues").label("active"); + on("issues.created").close(); + on("pull_request.labeled").lock(); `); it('returns behaviors for event', () => { diff --git a/test/dispatcher.js b/test/dispatcher.js index 2540142..64f0860 100644 --- a/test/dispatcher.js +++ b/test/dispatcher.js @@ -22,7 +22,7 @@ describe('dispatch', () => { describe('reply to new issue with a comment', () => { it('posts a coment', () => { - const config = Configuration.parse('workflows.push(on("issues").comment("Hello World!"))'); + const config = Configuration.parse('on("issues").comment("Hello World!")'); return dispatcher.call(config).then(() => { expect(github.issues.createComment).toHaveBeenCalled(); }); @@ -31,7 +31,7 @@ describe('dispatch', () => { describe('reply to new issue with a comment', () => { it('calls the action', () => { - const config = Configuration.parse('workflows.push(on("issues.created").comment("Hello World!"))'); + const config = Configuration.parse('on("issues.created").comment("Hello World!")'); return dispatcher.call(config).then(() => { expect(github.issues.createComment).toHaveBeenCalled(); @@ -41,7 +41,7 @@ describe('dispatch', () => { describe('on an event with a different action', () => { it('does not perform behavior', () => { - const config = Configuration.parse('workflows.push(on("issues.labeled").comment("Hello World!"))'); + const config = Configuration.parse('on("issues.labeled").comment("Hello World!")'); return dispatcher.call(config).then(() => { expect(github.issues.createComment).toNotHaveBeenCalled(); @@ -59,14 +59,14 @@ describe('dispatch', () => { }); it('calls action when condition matches', () => { - const config = Configuration.parse('workflows.push(on("issues.labeled").filter((e) => e.payload.label.name == "bug").close())'); + const config = Configuration.parse('on("issues.labeled").filter((e) => e.payload.label.name == "bug").close()'); return dispatcher.call(config).then(() => { expect(github.issues.edit).toHaveBeenCalled(); }); }); it('does not call action when conditions do not match', () => { - const config = Configuration.parse('workflows.push(on("issues.labeled").filter((e) => e.payload.label.name == "foobar").close())'); + const config = Configuration.parse('on("issues.labeled").filter((e) => e.payload.label.name == "foobar").close()'); return dispatcher.call(config).then(() => { expect(github.issues.edit).toNotHaveBeenCalled(); }); diff --git a/test/plugins/issues.js b/test/plugins/issues.js index acaeba2..d1cee65 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -1,6 +1,6 @@ const expect = require('expect'); const issues = require('../../lib/plugins/issues'); -const workflow = require('../../lib/workflow'); +const Workflow = require('../../lib/workflow'); const Context = require('../../lib/context'); const payload = require('../fixtures/webhook/comment.created.json'); @@ -25,7 +25,7 @@ const context = new Context(github, {}, {payload}); describe('issues plugin', () => { before(() => { - this.w = new workflow.Workflow(); + this.w = new Workflow(); this.evaluator = new issues.Evaluator(); }); From 08ac1275716bfaafce4f0e9f8f5443e9b80dc470 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 17 Nov 2016 21:13:02 -0600 Subject: [PATCH 25/29] Remove unnecessary semicolon --- lib/sandbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sandbox.js b/lib/sandbox.js index b6d04f6..d8f355c 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -16,6 +16,6 @@ class Sandbox { this.workflows.push(workflow); return workflow; } -}; +} module.exports = Sandbox; From e7a197f6a1c4fab7740f32ad1198ffb03fc254a5 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 17 Nov 2016 21:42:51 -0600 Subject: [PATCH 26/29] Use handlebars in comment templates --- lib/plugins/issues.js | 4 +++- test/plugins/issues.js | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index 9739789..f25f6a1 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -1,3 +1,4 @@ +const handlebars = require('handlebars'); const Evaluator = require('../evaluator'); const IssuePlugin = superclass => class extends superclass { @@ -81,7 +82,8 @@ class IssueEvaluator extends Evaluator { if (workflow.issueActions.content !== undefined) { promises.push(new Promise((resolve, reject) => { this.resolveData(workflow.issueActions.content, data => { - context.github.issues.createComment(context.payload.toIssue({body: data}, err => { + const template = handlebars.compile(data)(context.payload); + context.github.issues.createComment(context.payload.toIssue({body: template}, err => { if (err) { reject(); } else { diff --git a/test/plugins/issues.js b/test/plugins/issues.js index d1cee65..0637d96 100644 --- a/test/plugins/issues.js +++ b/test/plugins/issues.js @@ -147,6 +147,18 @@ describe('issues plugin', () => { body: 'Hello world!' }); }); + + it('evaluates templates with handlebars', () => { + this.w.comment('Hello @{{ sender.login }}!'); + + Promise.all(this.evaluator.evaluate(this.w, context)); + expect(github.issues.createComment).toHaveBeenCalledWith({ + owner: 'bkeepers-inc', + repo: 'test', + number: 6, + body: 'Hello @bkeepers!' + }); + }); }); describe('assignment', () => { From 48d306d49975714a5724aa13b25174d61df2cdaf Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 17 Nov 2016 21:52:11 -0600 Subject: [PATCH 27/29] Remove URL stuff for now --- lib/evaluator.js | 6 ------ lib/plugins/issues.js | 17 +++++------------ lib/sandbox.js | 4 +--- lib/utils/url.js | 6 ------ 4 files changed, 6 insertions(+), 27 deletions(-) delete mode 100644 lib/utils/url.js diff --git a/lib/evaluator.js b/lib/evaluator.js index e96f285..b3c1023 100644 --- a/lib/evaluator.js +++ b/lib/evaluator.js @@ -1,10 +1,4 @@ class Evaluator { - // Public function to either pass the string through or lookup network data - resolveData(data, fn) { - // TODO make a network to fetch the data - fn(data); - return this; - } } module.exports = Evaluator; diff --git a/lib/plugins/issues.js b/lib/plugins/issues.js index f25f6a1..8f88793 100644 --- a/lib/plugins/issues.js +++ b/lib/plugins/issues.js @@ -80,18 +80,11 @@ class IssueEvaluator extends Evaluator { const promises = []; if (workflow.issueActions.content !== undefined) { - promises.push(new Promise((resolve, reject) => { - this.resolveData(workflow.issueActions.content, data => { - const template = handlebars.compile(data)(context.payload); - context.github.issues.createComment(context.payload.toIssue({body: template}, err => { - if (err) { - reject(); - } else { - resolve(); - } - })); - }); - })); + const template = handlebars.compile(workflow.issueActions.content)(context.payload); + + promises.push( + context.github.issues.createComment(context.payload.toIssue({body: template})) + ); } if (workflow.issueActions.assignees !== undefined) { diff --git a/lib/sandbox.js b/lib/sandbox.js index d8f355c..632b282 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -1,4 +1,3 @@ -const URL = require('./utils/url'); const Workflow = require('./workflow'); class Sandbox { @@ -6,8 +5,7 @@ class Sandbox { this.workflows = []; this.api = { - on: this.on.bind(this), - URL + on: this.on.bind(this) }; } diff --git a/lib/utils/url.js b/lib/utils/url.js deleted file mode 100644 index 3f53170..0000000 --- a/lib/utils/url.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = class URL { - constructor(url) { - this.url = url; - } -}; - From 0c237b4fcec21072911ab64ecb6397cb00efe90a Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 17 Nov 2016 22:09:20 -0600 Subject: [PATCH 28/29] Look for config in .probot.js --- lib/configuration.js | 4 ++-- test/configuration.js | 2 +- test/fixtures/content/probot.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/configuration.js b/lib/configuration.js index 1e9deb5..aecd74c 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -5,12 +5,12 @@ const Sandbox = require('./sandbox'); module.exports = class Configuration { // Get bot config from target repository static load(github, repository) { - debug('Fetching .probot from %s', repository.full_name); + 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' + path: '.probot.js' }).then(data => { const content = new Buffer(data.content, 'base64').toString(); debug('Configuration fetched', content); diff --git a/test/configuration.js b/test/configuration.js index 1287a86..875cf14 100644 --- a/test/configuration.js +++ b/test/configuration.js @@ -33,7 +33,7 @@ describe('Configuration', () => { expect(github.repos.getContent).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', - path: '.probot' + path: '.probot.js' }); expect(config.workflows.length).toEqual(2); diff --git a/test/fixtures/content/probot.json b/test/fixtures/content/probot.json index 28699a7..4aca345 100644 --- a/test/fixtures/content/probot.json +++ b/test/fixtures/content/probot.json @@ -1,6 +1,6 @@ { - "name": ".probot", - "path": ".probot", + "name": ".probot.js", + "path": ".probot.js", "sha": "c0736435c3f35dc5702d5024ed00d399b99623af", "size": 127, "url": "https://api.github.com/repos/bkeepers-inc/test/contents/.probot?ref=master", From 7187c9c258576f5c6e20e151ae464637c108d386 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 17 Nov 2016 22:38:09 -0600 Subject: [PATCH 29/29] Update docs --- CONTRIBUTING.md | 8 +++-- README.md | 11 ++++-- docs/configuration.md | 84 +++++++++++++++---------------------------- docs/examples.md | 44 +++++++++++------------ 4 files changed, 64 insertions(+), 83 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb6766f..ccc06a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,9 +17,9 @@ To test with a real GitHub repository, you'll need to create a test repository a - **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` in your repo with: +0. Create a `.probot.js` in your repo with: - on issues.opened then comment("Hello World! Your bot is working!"); + 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). @@ -34,6 +34,8 @@ To test with a real GitHub repository, you'll need to create a test repository a ## Adding an action +_**TODO:** This is out of date and will be updated after plugin API is settled._ + [Actions](docs/configuration.md#then) are called when a webhook is delivered and any conditions are met. For example, this configuration in `.probot` calls two actions when a new issue is opened: ``` @@ -77,6 +79,8 @@ That's it! You now have everything you need to implement an action. If you're lo ## Adding a condition +_**TODO:** This is out of date and will be updated after plugin API is settled._ + [Conditions](docs/configuration.md#then) are called when a webhook is delivered and are used to determine if the actions should be called. For example, this configuration in `.probot` checks if an issue or pull request has a label before closing it: ``` diff --git a/README.md b/README.md index 21b56ab..fda331f 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,14 @@ _**Heads up!** The [demo integration](https://github.com/integration/probot-demo 0. Go to the **[demo integration](https://github.com/integration/probot-demo)**, click **Install**, and then select an organization. 0. Add @probot as a collaborator with write access on your repository. -0. Create a `.probot` file in your repository with the following contents. See [Configuration](docs/configuration.md) for more information on what behaviors can be built. +0. Create a `.probot.js` file in your repository with the following contents. See [Configuration](docs/configuration.md) for more information on what behaviors can be built. - on issues.opened - then comment("Hello @{{ sender.login }}. Thanks for inviting me to your project. Read more about [all the things I can help you with][config]. I can't wait to get started!\\n[config]: https://github.com/bkeepers/PRobot/blob/master/docs/configuration.md"); + on("issues.opened").comment(` + Hello @{{ sender.login }}. Thanks for inviting me to your project. + Read more about [all the things I can help you with][config]. I can't + wait to get started! + + [config]: https://github.com/bkeepers/PRobot/blob/master/docs/configuration.md + `); 0. Open a new issue. @probot should post a comment (you may need to refresh to see it). diff --git a/docs/configuration.md b/docs/configuration.md index 677b8c1..106f82c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,25 +1,25 @@ # Configuration -Behaviors are configured in a file called `.probot` in your repository. +Workflows are configured in a file called `.probot.js` in your repository. ``` -# Auto-respond to new issues and pull requests -on issues.opened or pull_request.opened -then comment("Thanks for your contribution! Expect a reply within 48 hours.") -and label(triage); +// Auto-respond to new issues and pull requests +on('issues.opened', 'pull_request.opened') + .comment('Thanks for your contribution! Expect a reply within 48 hours.') + .label('triage'); -# Auto-close new pull requests -on pull_request.opened -then comment("Sorry @{{ user.login }}, pull requests are not accepted on this repository.") -and close; +// Auto-close new pull requests +on('pull_request.opened') + .comment('Sorry @{{ user.login }}, pull requests are not accepted on this repository.') + .close(); ``` -## Behaviors +## Workflows -Behaviors are composed of: +Workflows are composed of: - [`on`](#on) - webhook events to listen to -- [`if`](#if) (optional) - conditions to determine if the actions should be performed. +- [`filter`](#filter) (optional) - conditions to determine if the actions should be performed. - [`then`](#then) - actions to take in response to the event ### `on` @@ -27,19 +27,19 @@ Behaviors are composed of: Specifies the type of GitHub [webhook event](https://developer.github.com/webhooks/#events) that this behavior applies to: ``` -on issues then… +on('issues') ``` You can also specify multiple events to trigger this behavior: ``` -on issues or pull_request then… +on('issues', 'pull_request') ``` Many events also have an `action` (e.g. `created` for the `issue` event), which can be referenced with dot notation: ``` -on issues.labeled or issues.unlabeled then… +on('issues.labeled', 'issues.unlabeled') ``` [Webhook events](https://developer.github.com/webhooks/#events) include: @@ -69,48 +69,20 @@ on issues.labeled or issues.unlabeled then… TODO: document actions -### `if` +### `filter` -Only preform the actions if theses conditions are met. Conditions can be [operations](#operations) or [functions](#functions) - -Attributes of the [webhook payload](https://developer.github.com/webhooks/#events) can be used in conditions using the `@` syntax. +Only preform the actions if the function returns `true`. The `event` is passed as an argument to the function and attributes of the [webhook payload](https://developer.github.com/webhooks/#events). ``` -if @issue.body contains "- [ ]" +.filter(event => event.payload.issue.body.includes("- [ ]")) ``` -#### Operations - -Operator | Description | Example ---------------------|-------------------------------------|----------------------------------- -`is` | equal | `@sender.login is "hubot"` -`is not` | not equal | `@sender.login is not "hubot"` -`contains` | string contains a substring | `@issue.body contains "- [ ]"` -`does not contain` | string does not contain a substring | `@issue.body does not contain "- [x]"` -`matches` | matches a regular expression | `@issue.title matches "^\[?WIP\]?"` -`does not match` | does not match a regular expression | `@issue.title does not match "v\d+\.\d+"` -`and` | logical and | `labeled(bug) and @sender.login is "hubot"` -`or` | logical or | `labeled(bug) or labeled(defect)` -`not` | negate a condition | `not labeled(bug)` - -#### Functions - -##### `labeled` - -Test if the label was added. - -``` -if labeled(bug) -``` - -### `then` - #### `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. ``` -… then comment("Hey @{{ user.login }}, thanks for the contribution!"); +.comment("Hey @{{ user.login }}, thanks for the contribution!"); ``` #### `close` @@ -118,7 +90,7 @@ Comments can be posted in response to any event performed on an Issue or Pull Re Close an issue or pull request. ``` -… then close; +.close(); ``` #### `open` @@ -126,7 +98,7 @@ Close an issue or pull request. Reopen an issue or pull request. ``` -… then open; +.open(); ``` #### `lock` @@ -134,7 +106,7 @@ Reopen an issue or pull request. Lock conversation on an issue or pull request. ``` -… then lock; +.lock(); ``` #### `unlock` @@ -142,7 +114,7 @@ Lock conversation on an issue or pull request. Unlock conversation on an issue or pull request. ``` -… then unlock; +.unlock(); ``` #### `label` @@ -150,7 +122,7 @@ Unlock conversation on an issue or pull request. Add labels ``` -… then label(bug); +.label('bug'); ``` #### `unlabel` @@ -158,25 +130,25 @@ Add labels Add labels ``` -… then unlabel("needs-work") and label("waiting-for-review"); +.unlabel('needs-work').label('waiting-for-review'); ``` #### `assign` ``` -… then assign(hubot); +.assign('hubot'); ``` #### `unassign` ``` -… then unassign(defunkt); +.unassign('defunkt'); ``` #### `react` ``` -… then react(heart); # or +1, -1, laugh, confused, heart, hooray +.react('heart'); # or +1, -1, laugh, confused, heart, hooray ``` --- diff --git a/docs/examples.md b/docs/examples.md index 751188f..9279a6f 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -14,52 +14,52 @@ Here are some examples of interesting things you can do by combining these compo // all the information from the template and it will make it easier for us to // help you. -on("issues.opened") +on('issues.opened') .filter((event) => { return !event.issue.body.match(/### Steps to Reproduce/) - || event.issue.body.includes("- [ ]") + || event.issue.body.includes('- [ ]') }) - .comment.contents(".github/MISSING_ISSUE_TEMPLATE_AUTOREPLY.md") - .label("insufficient-info") + .comment.contents('.github/MISSING_ISSUE_TEMPLATE_AUTOREPLY.md') + .label('insufficient-info') .close(); ``` ### Post welcome message for new contributors ```js -on("issues.opened", "pull_request.opened") +on('issues.opened', 'pull_request.opened') .filter.firstTimeContributor() // plugins could implement conditions like this - .comment.contents(".github/NEW_CONTRIBUTOR_TEMPLATE.md"); + .comment.contents('.github/NEW_CONTRIBUTOR_TEMPLATE.md'); ``` ### Auto-close new pull requests ```js -on("pull_request.opened") - .comment("Sorry @{{ user.login }}, pull requests are not accepted on this repository.") +on('pull_request.opened') + .comment('Sorry @{{ user.login }}, pull requests are not accepted on this repository.') .close(); ``` ### Close issues with no body ```js -on("issues.opened") +on('issues.opened') .filter((event) => event.issue.body.match(/^$/)) - .comment("Hey @{{ user.login }}, you didn't include a description of the problem, so we're closing this issue."); + .comment('Hey @{{ user.login }}, you didn't include a description of the problem, so we're closing this issue.'); ``` ### @mention watchers when label added ```js -on("*.labeled") +on('*.labeled') // TODO: figure out syntax for loading watchers from file - .comment("Hey {{ mentions }}, you wanted to know when the `{{ payload.label.name }}` label was added."); + .comment('Hey {{ mentions }}, you wanted to know when the `{{ payload.label.name }}` label was added.'); ``` ### Assign a reviewer for new bugs ```js -on("pull_request.labeled") +on('pull_request.labeled') .filter((event) => event.labeled(bug)) .assign(random(file(OWNERS))); ``` @@ -67,11 +67,11 @@ on("pull_request.labeled") ### Perform actions based on content of comments ```js -on("issue_comment.opened") +on('issue_comment.opened') .filter((event) => event.issue.body.match(/^@probot assign @(\w+)$/)) .assign({{ matches[0] }}); -on("issue_comment.opened") +on('issue_comment.opened') .filter((event) => event.issue.body.match(/^@probot label @(\w+)$/)) .label($1); ``` @@ -79,23 +79,23 @@ on("issue_comment.opened") ### Close stale issues and pull requests ```js -every("day") - .find.issues({state: "open", label: "needs-work"}) - .filter.lastActive(7, "days") +every('day') + .find.issues({state: 'open', label: 'needs-work'}) + .filter.lastActive(7, 'days') .close(); ``` ### Tweet when a new release is created ```js -on("release.published") +on('release.published') .tweet("Get it while it's hot! {{ repository.name }} {{ release.name }} was just released! {{ release.html_url }}"); ``` ### Assign a reviewer issues or pull requests with a label ```js -on("issues.opened", "pull_request.opened", "issues.labeled", "pull_request.labeled") - .filter.labeled("security") - .assign(team("security-first-responders").random()); +on('issues.opened', 'pull_request.opened', 'issues.labeled', 'pull_request.labeled') + .filter.labeled('security') + .assign(team('security-first-responders').random()); ```