mirror of
https://github.com/zhigang1992/probot.git
synced 2026-06-16 02:44:19 +08:00
Merge pull request #58 from bkeepers/pure-js-implemented
Use JS for .probots language
This commit is contained in:
@@ -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:
|
||||
|
||||
```
|
||||
|
||||
11
README.md
11
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).
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
107
docs/examples.md
107
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((event) => {
|
||||
return !event.issue.body.match(/### Steps to Reproduce/)
|
||||
|| event.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((event) => event.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((event) => event.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((event) => event.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((event) => event.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());
|
||||
```
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
assign: require('./actions/assign'),
|
||||
close: require('./actions/close'),
|
||||
comment: require('./actions/comment'),
|
||||
label: require('./actions/label'),
|
||||
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')
|
||||
};
|
||||
@@ -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})
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
const updateIssue = require('./update-issue');
|
||||
|
||||
module.exports = function (context) {
|
||||
return updateIssue(context, {state: 'closed'});
|
||||
};
|
||||
@@ -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)})
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = function (context, ...labels) {
|
||||
return context.github.issues.addLabels(
|
||||
context.payload.toIssue({body: labels})
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function (context) {
|
||||
return context.github.issues.lock(context.payload.toIssue());
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
const updateIssue = require('./update-issue');
|
||||
|
||||
module.exports = function (context) {
|
||||
return updateIssue(context, {state: 'open'});
|
||||
};
|
||||
6
lib/actions/react.js
vendored
6
lib/actions/react.js
vendored
@@ -1,6 +0,0 @@
|
||||
|
||||
module.exports = function (context, react) {
|
||||
return context.github.reactions.createForIssue(
|
||||
context.payload.toIssue({content: react})
|
||||
);
|
||||
};
|
||||
@@ -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}})
|
||||
);
|
||||
};
|
||||
@@ -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})
|
||||
);
|
||||
}));
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function (context) {
|
||||
return context.github.issues.unlock(context.payload.toIssue());
|
||||
};
|
||||
@@ -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));
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = (context, name) => {
|
||||
return name.reduce((object, attr) => {
|
||||
return object ? object[attr] : null;
|
||||
}, context.payload);
|
||||
};
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
labeled: require('./conditions/labeled')
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = (context, label) => {
|
||||
return context.payload.label && context.payload.label.name === label;
|
||||
};
|
||||
@@ -1,16 +1,16 @@
|
||||
const vm = require('vm');
|
||||
const debug = require('debug')('PRobot');
|
||||
const Transformer = require('./transformer');
|
||||
const parser = require('./parser');
|
||||
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);
|
||||
@@ -19,21 +19,19 @@ module.exports = class Configuration {
|
||||
}
|
||||
|
||||
static parse(content) {
|
||||
const transformer = new Transformer(parser.parse(content));
|
||||
return new Configuration(transformer.transform());
|
||||
const sandbox = new Sandbox();
|
||||
vm.createContext(sandbox.api);
|
||||
vm.runInContext(content, sandbox.api);
|
||||
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 => {
|
||||
return w.matchesEvent(event) && w.filterFn(event);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const Context = require('./context');
|
||||
const issues = require('./plugins/issues');
|
||||
|
||||
class Dispatcher {
|
||||
constructor(github, event) {
|
||||
@@ -8,13 +9,23 @@ 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
|
||||
const evaluators = [
|
||||
issues.Evaluator
|
||||
];
|
||||
|
||||
// Handle all behaviors
|
||||
return Promise.all(behaviors.map(behavior => {
|
||||
return behavior.perform(context);
|
||||
}));
|
||||
return Promise.all(workflows.map(w => {
|
||||
return evaluators.map(E => {
|
||||
const evaluator = new E();
|
||||
return evaluator.evaluate(w, context);
|
||||
});
|
||||
}).reduce((a, b) => {
|
||||
return a.concat(b);
|
||||
}, []));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
lib/evaluator.js
Normal file
4
lib/evaluator.js
Normal file
@@ -0,0 +1,4 @@
|
||||
class Evaluator {
|
||||
}
|
||||
|
||||
module.exports = Evaluator;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
160
lib/parser.pegjs
160
lib/parser.pegjs
@@ -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]
|
||||
160
lib/plugins/issues.js
Normal file
160
lib/plugins/issues.js
Normal file
@@ -0,0 +1,160 @@
|
||||
const handlebars = require('handlebars');
|
||||
const Evaluator = require('../evaluator');
|
||||
|
||||
const IssuePlugin = superclass => class extends superclass {
|
||||
comment(content) {
|
||||
this._setCommentData({content});
|
||||
return this;
|
||||
}
|
||||
|
||||
assign(...assignees) {
|
||||
this._setCommentData({assignees});
|
||||
return this;
|
||||
}
|
||||
|
||||
unassign(...assignees) {
|
||||
this._setCommentData({unassignees: assignees});
|
||||
return this;
|
||||
}
|
||||
|
||||
label(...labels) {
|
||||
this._setCommentData({labels});
|
||||
return this;
|
||||
}
|
||||
|
||||
unlabel(...labels) {
|
||||
this._setCommentData({unlabels: labels});
|
||||
return this;
|
||||
}
|
||||
|
||||
lock() {
|
||||
this._setCommentData({lock: true});
|
||||
return this;
|
||||
}
|
||||
|
||||
unlock() {
|
||||
this._setCommentData({unlock: true});
|
||||
return this;
|
||||
}
|
||||
|
||||
open() {
|
||||
this._setCommentData({open: true});
|
||||
return this;
|
||||
}
|
||||
|
||||
close() {
|
||||
this._setCommentData({close: true});
|
||||
return this;
|
||||
}
|
||||
|
||||
react(reaction) {
|
||||
this._setCommentData({reaction});
|
||||
return this;
|
||||
}
|
||||
|
||||
_setCommentData(obj) {
|
||||
if (this.issueActions === undefined) {
|
||||
this.issueActions = {};
|
||||
}
|
||||
Object.assign(this.issueActions, obj);
|
||||
}
|
||||
};
|
||||
|
||||
// This is the function that implements all of the actions configured above.
|
||||
class IssueEvaluator extends Evaluator {
|
||||
evaluate(workflow, context) {
|
||||
const 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;
|
||||
}
|
||||
const promises = [];
|
||||
|
||||
if (workflow.issueActions.content !== undefined) {
|
||||
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) {
|
||||
const assignees = workflow.issueActions.assignees;
|
||||
promises.push(
|
||||
context.github.issues.addAssigneesToIssue(context.payload.toIssue({assignees}))
|
||||
);
|
||||
}
|
||||
|
||||
if (workflow.issueActions.unassignees !== undefined) {
|
||||
const assignees = {assignees: workflow.issueActions.unassignees};
|
||||
promises.push(
|
||||
context.github.issues.removeAssigneesFromIssue(context.payload.toIssue({body: assignees}))
|
||||
);
|
||||
}
|
||||
|
||||
if (workflow.issueActions.labels !== undefined) {
|
||||
const labels = workflow.issueActions.labels;
|
||||
promises.push(
|
||||
context.github.issues.addLabels(context.payload.toIssue({body: labels}))
|
||||
);
|
||||
}
|
||||
|
||||
if (workflow.issueActions.unlabels !== undefined) {
|
||||
const labels = workflow.issueActions.unlabels;
|
||||
promises.push(
|
||||
labels.map(label => {
|
||||
return context.github.issues.removeLabel(
|
||||
context.payload.toIssue({name: label})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (workflow.issueActions.lock !== undefined) {
|
||||
promises.push(
|
||||
context.github.issues.lock(context.payload.toIssue({}))
|
||||
);
|
||||
}
|
||||
|
||||
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'}))
|
||||
);
|
||||
}
|
||||
|
||||
if (workflow.issueActions.close !== undefined) {
|
||||
promises.push(
|
||||
context.github.issues.edit(context.payload.toIssue({state: 'closed'}))
|
||||
);
|
||||
}
|
||||
|
||||
if (workflow.issueActions.reaction !== undefined) {
|
||||
const reaction = workflow.issueActions.reaction;
|
||||
promises.push(
|
||||
context.github.reactions.createForIssue(context.payload.toIssue({content: reaction}))
|
||||
);
|
||||
}
|
||||
|
||||
return promises;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Plugin: IssuePlugin,
|
||||
Evaluator: IssueEvaluator
|
||||
};
|
||||
19
lib/sandbox.js
Normal file
19
lib/sandbox.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const Workflow = require('./workflow');
|
||||
|
||||
class Sandbox {
|
||||
constructor() {
|
||||
this.workflows = [];
|
||||
|
||||
this.api = {
|
||||
on: this.on.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
on(...events) {
|
||||
const workflow = new Workflow(events);
|
||||
this.workflows.push(workflow);
|
||||
return workflow;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Sandbox;
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
};
|
||||
41
lib/workflow.js
Normal file
41
lib/workflow.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const issues = require('./plugins/issues');
|
||||
|
||||
class WorkflowCore {
|
||||
constructor(events) {
|
||||
this.events = events;
|
||||
this.filterFn = () => true;
|
||||
}
|
||||
|
||||
filter(fn) {
|
||||
this.filterFn = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
matchesEvent(event) {
|
||||
const eventWithAction = [event.event, event.payload.action];
|
||||
|
||||
return this.events.find(e => {
|
||||
const parts = e.split('.');
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (parts[i] !== eventWithAction[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: issues
|
||||
const plugins = [
|
||||
issues.Plugin
|
||||
];
|
||||
|
||||
// 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) {}
|
||||
|
||||
module.exports = Workflow;
|
||||
@@ -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",
|
||||
|
||||
@@ -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']
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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!'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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']
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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']}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -23,32 +33,32 @@ describe('Configuration', () => {
|
||||
expect(github.repos.getContent).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers',
|
||||
repo: 'test',
|
||||
path: '.probot'
|
||||
path: '.probot.js'
|
||||
});
|
||||
|
||||
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;
|
||||
on("issues").label("active");
|
||||
on("issues.created").close();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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('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('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('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('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('on("issues.labeled").filter((e) => e.payload.label.name == "foobar").close()');
|
||||
return dispatcher.call(config).then(() => {
|
||||
expect(github.issues.edit).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
29
test/fixtures/behaviors
vendored
29
test/fixtures/behaviors
vendored
@@ -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;
|
||||
6
test/fixtures/content/probot.json
vendored
6
test/fixtures/content/probot.json
vendored
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
261
test/parser.js
261
test/parser.js
@@ -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'}]
|
||||
}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
227
test/plugins/issues.js
Normal file
227
test/plugins/issues.js
Normal file
@@ -0,0 +1,227 @@
|
||||
const expect = require('expect');
|
||||
const issues = require('../../lib/plugins/issues');
|
||||
const Workflow = require('../../lib/workflow');
|
||||
const Context = require('../../lib/context');
|
||||
const payload = require('../fixtures/webhook/comment.created.json');
|
||||
|
||||
const createSpy = expect.createSpy;
|
||||
|
||||
const github = {
|
||||
reactions: {
|
||||
createForIssue: createSpy()
|
||||
},
|
||||
issues: {
|
||||
lock: createSpy(),
|
||||
unlock: createSpy(),
|
||||
edit: createSpy(),
|
||||
addLabels: createSpy(),
|
||||
createComment: createSpy(),
|
||||
addAssigneesToIssue: createSpy(),
|
||||
removeAssigneesFromIssue: createSpy(),
|
||||
removeLabel: createSpy()
|
||||
}
|
||||
};
|
||||
const context = new Context(github, {}, {payload});
|
||||
|
||||
describe('issues plugin', () => {
|
||||
before(() => {
|
||||
this.w = new Workflow();
|
||||
this.evaluator = new issues.Evaluator();
|
||||
});
|
||||
|
||||
describe('locking', () => {
|
||||
it('locks', () => {
|
||||
this.w.lock();
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.lock).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6
|
||||
});
|
||||
});
|
||||
|
||||
it('unlocks', () => {
|
||||
this.w.unlock();
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.unlock).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('state', () => {
|
||||
it('opens an issue', () => {
|
||||
this.w.open();
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.edit).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
state: 'open'
|
||||
});
|
||||
});
|
||||
it('closes an issue', () => {
|
||||
this.w.close();
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.edit).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
state: 'closed'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('labels', () => {
|
||||
it('adds a label', () => {
|
||||
this.w.label('hello');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.addLabels).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
body: ['hello']
|
||||
});
|
||||
});
|
||||
|
||||
it('adds multiple labels', () => {
|
||||
this.w.label('hello', 'world');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.addLabels).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
body: ['hello', 'world']
|
||||
});
|
||||
});
|
||||
|
||||
it('removes a single label', () => {
|
||||
this.w.unlabel('hello');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.removeLabel).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
name: 'hello'
|
||||
});
|
||||
});
|
||||
|
||||
it('removes a multiple labels', () => {
|
||||
this.w.unlabel('hello', 'goodbye');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.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', () => {
|
||||
it('creates a comment', () => {
|
||||
this.w.comment('Hello world!');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.createComment).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
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', () => {
|
||||
it('assigns a user', () => {
|
||||
this.w.assign('bkeepers');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
assignees: ['bkeepers']
|
||||
});
|
||||
});
|
||||
|
||||
it('assigns multiple users', () => {
|
||||
this.w.assign('hello', 'world');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.addAssigneesToIssue).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
assignees: ['hello', 'world']
|
||||
});
|
||||
});
|
||||
|
||||
it('unassigns a user', () => {
|
||||
this.w.unassign('bkeepers');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
body: {assignees: ['bkeepers']}
|
||||
});
|
||||
});
|
||||
|
||||
it('unassigns multiple users', () => {
|
||||
this.w.unassign('hello', 'world');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.issues.removeAssigneesFromIssue).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
body: {assignees: ['hello', 'world']}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reactions', () => {
|
||||
it('react', () => {
|
||||
this.w.react('heart');
|
||||
|
||||
Promise.all(this.evaluator.evaluate(this.w, context));
|
||||
expect(github.reactions.createForIssue).toHaveBeenCalledWith({
|
||||
owner: 'bkeepers-inc',
|
||||
repo: 'test',
|
||||
number: 6,
|
||||
content: 'heart'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user