Repository: open-bot/open-bot
Branch: master
Commit: 73c146a30ed2
Files: 72
Total size: 101.2 KB
Directory structure:
gitextract_29rk1j_5/
├── .gitignore
├── README.md
├── doc/
│ └── rules.md
├── handlers.js
├── lib/
│ ├── open-bot/
│ │ ├── Config.js
│ │ ├── actions/
│ │ │ ├── close.js
│ │ │ ├── comment.js
│ │ │ ├── index.js
│ │ │ ├── label.js
│ │ │ ├── merge.js
│ │ │ ├── new_issue.js
│ │ │ ├── reopen.js
│ │ │ ├── schedule.js
│ │ │ ├── set.js
│ │ │ └── status.js
│ │ ├── api-utils.js
│ │ ├── filters/
│ │ │ ├── IssuePullRequestBaseFilter.js
│ │ │ ├── TimeFilter.js
│ │ │ ├── age.js
│ │ │ ├── all.js
│ │ │ ├── any.js
│ │ │ ├── check.js
│ │ │ ├── comment.js
│ │ │ ├── commit.js
│ │ │ ├── ensure.js
│ │ │ ├── fetch.js
│ │ │ ├── in_order.js
│ │ │ ├── index.js
│ │ │ ├── issue.js
│ │ │ ├── label.js
│ │ │ ├── last_action_age.js
│ │ │ ├── match.js
│ │ │ ├── not.js
│ │ │ ├── number_of_comments.js
│ │ │ ├── open.js
│ │ │ ├── permission.js
│ │ │ ├── pull_request.js
│ │ │ ├── review.js
│ │ │ ├── status.js
│ │ │ ├── string_cleanup.js
│ │ │ ├── threshold.js
│ │ │ └── travis_job.js
│ │ ├── github.js
│ │ ├── helpers/
│ │ │ ├── findCount.js
│ │ │ ├── findLast.js
│ │ │ ├── findLastIndex.js
│ │ │ ├── getDate.js
│ │ │ ├── interpolate.js
│ │ │ ├── matchRange.js
│ │ │ ├── once.js
│ │ │ ├── parseDate.js
│ │ │ └── seq.js
│ │ ├── open-bot.js
│ │ ├── package.json
│ │ └── travis.js
│ ├── open-bot-cli/
│ │ ├── open-bot.js
│ │ └── package.json
│ ├── open-bot-handle-github-event/
│ │ ├── handler.js
│ │ └── package.json
│ ├── open-bot-process-scheduled-tasks/
│ │ ├── handler.js
│ │ └── package.json
│ ├── open-bot-process-tasks/
│ │ ├── handler.js
│ │ └── package.json
│ ├── open-bot-scheduler/
│ │ ├── index.js
│ │ └── package.json
│ └── package.json
├── open-bot.js
├── open-bot.yaml
├── package.json
├── scripts/
│ ├── for-each-package.js
│ └── sync-dependencies.js
└── serverless.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules
config.json
.serverless
.cache
================================================
FILE: README.md
================================================
# open-bot
An unopinionated bot driven by a configuration file in the repository.
* Operates on issue/PRs
* Triggered by webhook event or manual batch processing (CLI)
* Rules specify behavior
* Runs on AWS with serverless
## Configuration
* Add the bot's webhook to the webhooks in the repo/org configuration (content type json, events: `Issue comment`, `Issues`, `Pull request`, `Pull request review`, `Pull request review comment`, `Status`)
* Create a `open-bot.yaml` file in the repository root.
* Add the bot name to the yaml file (`bot` property)
* Add rules to the `rules` property (array) see [Rules](doc/rules.md)
## How does it work?
It checks all filters to get the lastest date where the filter matches. If not all filters match it stops here.
For each action it checks if and when the action was already applied. If yes it compares action date if lastest filter date and breaks if action was applied after filter match. Elsewise it runs the action.
This means: It makes sure that actions runs once **after** filters match. If filters match again, actions run again.
## Deploy your own bot
* Clone the repo
* Run `yarn`
* Run `node open-bot configurate` and answer questions
* Run `yarn run deploy-prod` to deploy the bot (takes a while)
* Note you need to have AWS CLI tools installed and configured. See http://docs.aws.amazon.com/cli/latest/userguide/installing.html
* Run `node open-bot` to gain access to the other tools, i. e. batch processing
## Contributing
### Add new `filters` or `actions`
* Add a file in the folder
* Edit the index.js file
* Test it locally with `node open-bot process some/repo#123 --settings open-bot.yaml --simulate`
* Send a PR
## License
Free for open-source projects.
Get in contact with me if you want to use it for private repos.
================================================
FILE: doc/rules.md
================================================
# Rules
A rule is a pair of `filters` and `actions`.
Example (complete configuration file):
``` yaml
bot: my-bot
rules:
- filters:
commit: true
actions:
comment: Thanks for the commit.
```
This rule adds a comment to a pull request when anyone adds a commit to it.
## Filters
There are many filters available. If you miss one, send a PR.
### commit
``` yaml
commit: true
```
``` yaml
commit: ab?c # commit message
```
``` yaml
commit:
matching: ab?c # commit message
author: ab?c # author github login
committer: ab?c # committer github login
```
Matches a commit in a PR.
### comment
``` yaml
comment: true
```
``` yaml
comment: ab?c # comment body
```
``` yaml
comment:
matching: ab?c # comment body
author: ab?c # comment author
```
Matches a comment in a issue/PR.
Makes the comment info accessible via data.
If `matching` is a regular expression, it also makes the match accessible via data with `_match` prefix.
### label
``` yaml
label: some-label # label name
```
``` yaml
label:
- some-label # label name
- some-other-label # label name
```
``` yaml
label:
labels:
- some-label # label name
- some-other-label # label name
labelRegExp: ab?c # label name regexp
```
Matches a label in a issue/PR.
### issue
``` yaml
issue: true
```
``` yaml
issue: ab?c # issue body
```
``` yaml
issue:
matching: ab?c # issue body
title: ab?c # issue title
author: ab?c # issue author
locked: true # issue is locked or not
```
Matches an issue.
Note: Doesn't match pull request.
### pull_request
``` yaml
pull_request: true
```
``` yaml
pull_request: ab?c # pull request body
```
``` yaml
pull_request:
matching: ab?c # pull request body
title: ab?c # pull request title
author: ab?c # pull request author
locked: true # pull request is locked or not
merged: true # pull request is merged or not
mergeable: true # pull request is mergeable or not
merged_by: ab?c # login of person who merged the PR
head_ref: ab?c # branchname of PR source
base_ref: ab?c # branchname of PR target
mergeable_state: ab?c # state of the PR
# one of: clean, dirty, blocked, stable, unstable, unknown
```
Matches a pull request.
Note: Doesn't match issue.
### open
``` yaml
open: true # issue/PR is open
```
``` yaml
open: false # issue/PR is not open
```
Matches if the state of the issue is open/closed.
### number_of_comments
``` yaml
number_of_comments: 3 # >= 3 comments in issue/PR
```
``` yaml
number_of_comments: # 3 - 6 comments in issue/PR
minimum: 3
maximum: 6
```
Matches the number of comments in a issue/PR.
### review
``` yaml
review: true # there is a review in the PR
```
``` yaml
review: ab?c # there is a review with this body in the PR
```
``` yaml
review:
matching: ab?c # review body
state: ab?c # review state (APPROVED, CHANGES_REQUESTED, COMMENT)
author: ab?c # author of review
upToDate: true # review is on latest commit
```
Matches a pull request review.
### status
``` yaml
status: ab?c # PR status message
```
``` yaml
status:
matching: ab?c # PR status message
state: ab?c # status state (success, failure, error, pending)
context: ab?c # status context
```
### check
``` yaml
check: ab?c # PR check message
```
``` yaml
check:
matching: ab?c # PR check message
conclusion: ab?c # check conclusion (success, failure)
name: ab?c # check context
```
Matches pull request check.
### permission
``` yaml
permission: true # issue creator has write or admin permission
```
``` yaml
permission: ab?c # issue creator permission level (admin, write, read, none)
```
``` yaml
permission:
user: "{{comment.actor.login}}" # user to review permission for
matching: ab?c # permission level (admin, write, read, none)
```
Matches if the permission level of the user matches the regular expression.
### age
``` yaml
age: 4w # age of issue/PR (creation > 4 weeks)
```
``` yaml
age:
minimum: 6h
maximum: 4d # 6 hours - 4 days old
minimumDate: 2016-01-01
maximumDate: 2016-12-31 # created in 2016
```
Matches if the issue/PR age is in the range.
``` yaml
age:
value: "{{comment.created_at}}"
minimum: 6h
maximum: 4d # 6 hours - 4 days old
minimumDate: 2016-01-01
maximumDate: 2016-12-31 # created in 2016
```
Matches if the provided date value is in the range.
### last_action_age
``` yaml
last_action_age: 4w # last user action in issue/PR
```
``` yaml
last_action_age:
minimum: 10s
maximum: 10m # 10 seconds - 10 minutes
minimumDate: 2016-01-01
maximumDate: 2016-12-31
includeBotActions: true
```
Matches if the last action age is in the range.
`includeBotActions` defaults to false, which excludes actions from the bot itself (only user actions).
### ensure
``` yaml
ensure:
value: "{{comment.actor.login}}" # a value (see interpolation)
matching: ab?c # matches this regexp
notMatching: ab?c # doesn't match this regexp
equals: abc # is equal to (interpolation possible)
notEquals: abc # is not equal to (interpolation possible)
range: "< 10, 15 - 20" # is in this range (see range)
```
Matches if the condition is true.
### any
``` yaml
any:
comment: true
age: 4w
```
Matches if any of the children match.
Here: Has a comment or is older than 4 weeks.
### all
``` yaml
all:
comment: true
age: 4w
```
Matches if all of the children match.
Here: Has a comment and is older than 4 weeks.
### not
``` yaml
not:
comment: true
age: 4w
```
Matches if children (all) don't match.
Here: Has no comment and is younger than 4 weeks.
### threshold
``` yaml
threshold:
minimum: 2
maximum: 3
filters:
comment_1: hello
comment_2: hi
comment_3: bye
comment_4: bb
```
Matches if the number of matched children is between minimum and maximum.
Here: Has at least 2 of the comments "hello", "hi", "bye", "bb" but not all of them.
### in_order
``` yaml
in_order:
commit: true
review: true
```
Matches if the order of the matched children is correct.
Note: It doesn't look for any order pair. It matches the children on it's own and compares times after that.
Here: The latest review is after the latest commit.
## Actions
### close
``` yaml
close: true
```
Closes the issue/PR.
### reopen
``` yaml
reopen: true
```
Reopen the issue/PR.
### label
``` yaml
label: some-label # adds a label
```
``` yaml
label:
- some-label
- some-other-label
```
``` yaml
label:
add:
- some-label
- some-other-label
remove:
- any-label
- any-other-label
```
Adds and/or removes labels from issue/PR.
### comment
``` yaml
comment: Hello @{{issue.user.login}}!
```
``` yaml
comment:
message: Hello @{{issue.user.login}}!
identifier: comment-label
invasive: true
edit: true
```
Adds a comment.
If `identifier` is provided it removes an old comment with the same identifier.
If `invasive` is set it resent the comment even if the body is equal.
`invasive` defaults to `false` if `identifier` is set and to `true` otherwise.
If `edit` is `true` it will edit the old comment instead will the new message.
If `message` is empty or undefined it will not add a new comment. It may remove an old comment if `identifer` is set.
### status
``` yaml
status:
description: "{{comment.body}}" # message
target_url: "{{comment.html_url}}" # link
context: status-label # the context of the PR status
state: pending # one of success, failure, error, pending
```
Reports a pull request status.
### merge
``` yaml
merge:
commit_title: "Merge pull request #{{pull_request.number}}"
commit_message: "{{pull_request.title}}"
merge_method: merge # one of merge, squash, rebase
```
Merges a PR. Like pressing the merge button.
### set
``` yaml
set:
id: SOME_VARIABLE_NAME
value: "{{comment.body}}"
```
Sets a variable name to some value. This variable is also available in rules followed by this rule. Best read this variable with the `ensure` filter.
### schedule
``` yaml
schedule: 2d # 2 days
```
Schedule another rules check in the specified timespan. The lowest schedule wins and overwrite any longer schedule.
## Range
Some expressions accept ranges. Example:
``` text
< 10 > 5, 15-20
```
Means: (smaller than 10 and bigger than 5) or between 15 and 20 (inclusive)
``` js
// in javascript
(x < 10 && x > 5) || (x >= 5 && x <= 20)
```
## Interpolation
Handlebars.js is used to interpolate values.
### Context
`owner`: repo owner
`repo`: repo name
`item`: full issue name `open-bot/open-bot#1`
`botUsername`: the username of the bot
`data`: data provided by filters
`issue`: github api issue data
others: keys provided by the filters
Example:
``` yaml
- filters:
comment_1: Hello
comment_2: World
actions:
comment: |-
Hello World provided by @{{comment_1.actor.login}} and @{{comment_2.actor.login}}.
```
Note: every filter/action key can be prefixed with `_(number)` to make them unique.
Note: you can add a `id` property to assign the value to some other name.
Note: See github api documentation for full object details.
### Helpers
`{{quote comment.body}}`: Wraps value in `>` to make it a markdown quote.
`{{stringify comment}}`: JSON.stringify the value.
## Examples
``` yaml
- filters: # Look for a comment
comment: Hello
actions: # Post a new comment
comment: Hello @{{comment.actor.login}}.
```
Respond to any comment containing "Hello" with a comment "Hello @user.".
``` yaml
- filters:
open: true
status: # Look for the latest travis results
context: "continuous-integration/travis-ci/pr"
ensure: # Check travis state
value: "{{status.state}}"
equals: "success"
actions:
label: # relabel
add: "ci-ok"
remove: "ci-not-ok"
comment: # post comment
identifier: "ci-result"
edit: true
message: |-
Success! :smile:
- filters:
open: true
status:
context: "continuous-integration/travis-ci/pr"
ensure:
value: "{{status.state}}"
equals: "failure"
actions:
label:
add: "ci-not-ok"
remove: "ci-ok"
comment:
identifier: "ci-result"
edit: true
message: |-
Failed. @{{issue.user.login}} Check [CI results]({{status.target_url}})!
```
Label pull request with `ci-ok` or `ci-not-ok` depending on the CI result. Also comment on the pull request to trigger the user.
``` yaml
- filters:
open: true
threshold:
maximum: 1
filters:
issue_1: Type
issue_2: Expected
issue_3: Current
actions:
label: missing-information
close: true
comment: Closed because information is missing. Edit the issue!
- filters:
open: false
label: missing-information
threshold:
minimum: 2
filters:
issue_1: Type
issue_2: Expected
issue_3: Current
actions:
label:
remove: missing-information
reopen: true
comment: Thanks!
```
Require at least two of `Type` `Expected` and `Current` in the issue. Elsewise close the issue and comment. Reopens when information is added later.
================================================
FILE: handlers.js
================================================
const handlers = [
"process-tasks",
"handle-github-event",
"process-scheduled-tasks"
];
handlers.forEach(name => {
const camelCasedName = name.replace(/-(.)/g, match => match[1].toUpperCase());
Object.defineProperty(exports, camelCasedName, {
get: () => require(`./lib/open-bot-${name}/handler.js`)
});
});
================================================
FILE: lib/open-bot/Config.js
================================================
const actionsList = require("./actions");
const filtersList = require("./filters");
const seq = require("./helpers/seq");
class Rule {
constructor(idx, filters, actions) {
this.idx = idx;
this.filters = filters;
this.actions = actions;
}
static formatResult(result) {
return `${result} = ${result < 0 ? "-" : "+"}${result ? new Date(Math.abs(result)).toLocaleString() : 0}`
}
run(context, issue) {
return seq(this.filters, (filter, idx) => {
const result = filter.findLast(context, issue);
return Promise.resolve(result).then(result => {
context.reporter({
item: context.item,
debug: true,
action: `filter ${this.idx}.${idx}`,
message: Rule.formatResult(result)
});
return result;
});
}, result => result < 0)
.then(filterResults => {
const isMatched = filterResults.every(r => r >= 0);
if(isMatched) {
const max = filterResults.reduce((max, i) => Math.max(max, i), -1);
context.reporter({
item: context.item,
debug: true,
action: `run actions ${this.idx}`,
message: Rule.formatResult(max)
});
return this.runActions(context, issue, max);
}
return filterResults;
});
}
runActions(context, issue, lastFilterImpuls) {
return seq(this.actions, (action, idx) => {
return Promise.resolve(action.findLast(context, issue))
.then(pos => {
context.reporter({
item: context.item,
debug: true,
action: `action ${this.idx}.${idx}`,
message: Rule.formatResult(pos)
});
if(pos < lastFilterImpuls) {
return Promise.resolve(action.run(context, issue))
.then(() => "activated")
.catch(err => {
context.reporter({
item: context.item,
error: action.constructor.name + " failed: " + err
});
return "errored";
});
}
return "already done";
});
});
}
}
class Config {
constructor(configJson) {
this.rules = configJson.rules.map((rule, idx) => Config.parseRule(rule, idx));
this.bot = configJson.bot;
}
static parseRule(ruleJson, idx) {
const filters = this.parseItems(ruleJson.filters, filtersList);
const actions = this.parseItems(ruleJson.actions, actionsList);
return new Rule(idx, filters, actions);
}
static parseItems(data, types) {
if(!data) return [];
return Array.isArray(data) ?
data.map(item => this.parseItem(item, null, types)) :
Object.keys(data).map(key => this.parseItem(data[key], key.replace(/_\d+$/, ""), types, key))
}
static parseItem(data, type, types, key) {
const typeName = type || data.type;
const Type = types[typeName];
if(!Type) throw new Error(`'${typeName} is not a valid type`);
return new Type(data, key);
}
run(context, issue) {
return seq(this.rules, rule => rule.run(context, issue));
}
}
module.exports = Config;
================================================
FILE: lib/open-bot/actions/close.js
================================================
const findLast = require("../helpers/findLast");
class CloseAction {
constructor(options) { }
findLast({ botUsername }, { state, timeline }) {
if(state === "closed")
return Infinity;
return timeline.then(timeline => findLast(timeline, event => {
return event.event === "closed" &&
event.actor &&
event.actor.login === botUsername;
}));
}
run({ owner, repo, item, github, reporter }, { number }) {
reporter({ item, action: "close" });
return github.editIssue(owner, repo, number, { state: "closed" });
}
}
module.exports = CloseAction;
================================================
FILE: lib/open-bot/actions/comment.js
================================================
const findLast = require("../helpers/findLast");
const findLastIndex = require("../helpers/findLastIndex");
const interpolate = require("../helpers/interpolate");
class CommentAction {
constructor(options) {
this.identifier = undefined;
this.edit = false;
this.message = undefined;
this.invasive = undefined;
if(typeof options === "string") {
this.message = options;
} else {
if(typeof options.message === "string")
this.message = options.message;
if(typeof options.identifier === "string")
this.identifier = options.identifier;
if(typeof options.edit === "boolean")
this.edit = options.edit;
if(typeof options.invasive === "boolean")
this.invasive = options.invasive;
}
}
findLast(context, issue) {
const { botUsername } = context;
const { timeline } = issue;
const msg = interpolate(this.message, { context, issue, identifier: this.identifier });
const invasive = typeof this.invasive === "boolean" ? this.invasive : !this.identifier;
return timeline.then(timeline => {
const last = timeline[timeline.length - 1];
if(last &&
last.event === "commented" &&
last.actor.login === botUsername &&
last.body === msg) {
return Infinity;
}
const result = findLast(timeline, event => {
return event.event === "commented" &&
event.actor.login === botUsername &&
event.body === msg;
});
return !invasive && result > 0 ? Infinity : result;
});
}
run(context, issue) {
const { owner, repo, item, github, reporter, botUsername } = context;
const { number, comments } = issue;
const msg = this.message && interpolate(this.message, { context, issue, identifier: this.identifier });
const addComment = () => {
if(msg) {
reporter({ item, action: "add comment", message: msg });
return github.createComment(owner, repo, number, msg);
}
}
if(this.identifier) {
return comments.then(comments => {
const idx = findLastIndex(comments, ({ user: { login } = {}, body }) =>
login === botUsername && body.indexOf(`<!-- identifier: ${this.identifier} -->`) === 0
);
if(idx < 0) {
return addComment();
} else if(!msg) {
reporter({ item, action: "remove comment" });
return github.deleteComment(owner, repo, comments[idx].id);
} else if(this.edit) {
reporter({ item, action: "edit comment", message: msg });
return github.editComment(owner, repo, comments[idx].id, msg);
} else {
reporter({ item, action: "readd comment", message: msg });
return github.deleteComment(owner, repo, comments[idx].id)
.then(() => github.createComment(owner, repo, number, msg));
}
});
} else {
return addComment();
}
}
}
module.exports = CommentAction;
================================================
FILE: lib/open-bot/actions/index.js
================================================
exports = module.exports = Object.create(null);
exports.close = require("./close");
exports.reopen = require("./reopen");
exports.label = require("./label");
exports.comment = require("./comment");
exports.status = require("./status");
exports.merge = require("./merge");
exports.new_issue = require("./new_issue");
exports.set = require("./set");
exports.schedule = require("./schedule");
================================================
FILE: lib/open-bot/actions/label.js
================================================
const findLast = require("../helpers/findLast");
class LabelAction {
constructor(options) {
this.add = undefined;
this.remove = undefined;
if(typeof options === "string") {
this.add = [options];
} else if(Array.isArray(options)) {
this.add = options;
} else {
if(typeof options.add === "string" || Array.isArray(options.add))
this.add = [].concat(options.add);
if(typeof options.remove === "string" || Array.isArray(options.remove))
this.remove = [].concat(options.remove);
}
}
findLast({ botUsername }, { timeline, labels }) {
const labelNames = labels.map(label => label.name);
if((!this.add || this.add.every(l => labelNames.includes(l))) &&
(!this.remove || this.remove.every(l => !labelNames.includes(l))))
return Infinity;
return timeline.then(timeline => findLast(timeline, event => {
if(this.add &&
event.event === "labeled" &&
event.actor.login === botUsername &&
this.add.includes(event.label.name))
return true;
if(this.remove &&
event.event === "unlabeled" &&
event.actor.login === botUsername &&
this.remove.includes(event.label.name))
return true;
return false;
}));
}
run({ owner, repo, item, github, reporter }, { number, labels }) {
const labelNames = labels.map(label => label.name);
const add = this.add && this.add.filter(l => !labelNames.includes(l));
const remove = this.remove && this.remove.filter(l => labelNames.includes(l));
reporter({ item, action: [
add && add.length && "add labels " + add.join(" "),
remove && remove.length && "remove labels " + remove.join(" "),
].filter(Boolean).join(", ")})
return Promise.all([
add && github.addLabels(owner, repo, number, add),
remove && github.removeLabels(owner, repo, number, remove)
].filter(Boolean));
}
}
module.exports = LabelAction;
================================================
FILE: lib/open-bot/actions/merge.js
================================================
const interpolate = require("../helpers/interpolate");
class MergeAction {
constructor(options) {
this.commit_title = undefined;
this.commit_message = undefined;
this.merge_method = undefined;
if(typeof options.commit_title === "string")
this.commit_title = options.commit_title;
if(typeof options.commit_message === "string")
this.commit_message = options.commit_message;
if(typeof options.merge_method === "string")
this.merge_method = options.merge_method;
}
findLast({ botUsername }, { pull_request_info }) {
return pull_request_info.then(({ merged_by, merged_at, mergeable, merged }) => {
if(
merged_by &&
merged_by.login === botUsername &&
merged_at
) {
return new Date(merged_at).getTime();
}
if(mergeable !== true || merged) return Infinity;
return false;
})
}
run(context, issue) {
const { owner, repo, item, github, reporter } = context;
const { number, pull_request_info } = issue;
return pull_request_info.then(({ title, head: { sha, label }, number }) => {
const commit_title = this.commit_title && interpolate(this.commit_title, { context, issue }) || `Merge pull request #${number} from ${label.replace(":", "/")}`;
const commit_message = this.commit_message && interpolate(this.commit_message, { context, issue }) || title;
const merge_method = this.merge_method && interpolate(this.merge_method, { context, issue }) || "merge";
reporter({ item, action: `merge ${merge_method} ${commit_title} ${commit_message}` });
return github.merge(owner, repo, number, commit_title, commit_message, sha, merge_method);
});
}
}
module.exports = MergeAction;
================================================
FILE: lib/open-bot/actions/new_issue.js
================================================
const interpolate = require("../helpers/interpolate");
class NewIssueAction {
constructor(options) {
this.target = options.target;
this.title = options.title || "{{{issue.title}}}";
this.body = options.body || "";
if(typeof this.target !== "string" || !this.target)
throw new Error("new_issue: target missing");
}
findLast() {
return -1;
}
run(context, issue) {
const { reporter, item, github } = context;
const target = interpolate(this.target, { context, issue });
const title = interpolate(this.title, { context, issue });
const body = interpolate(this.body, { context, issue });
const targetSplit = target.split("/");
if(targetSplit.length !== 2)
throw new Error("")
reporter({ item, action: "new issue", message: `${target}: ${title}\n${body}` });
const [owner, repo] = targetSplit;
return github.createIssue(owner, repo, title, body);
}
}
module.exports = NewIssueAction;
================================================
FILE: lib/open-bot/actions/reopen.js
================================================
const findLast = require("../helpers/findLast");
class ReopenAction {
constructor(options) { }
findLast({ botUsername }, { state, timeline }) {
if(state === "open")
return Infinity;
return timeline.then(timeline => findLast(timeline, event => {
return event.event === "reopened" &&
event.actor.login === botUsername;
}));
}
run({ owner, repo, item, github, reporter }, { number }) {
reporter({ item, action: "reopen" });
return github.editIssue(owner, repo, number, { state: "open" });
}
}
module.exports = ReopenAction;
================================================
FILE: lib/open-bot/actions/schedule.js
================================================
const schedule = require("../../open-bot-scheduler").schedule;
const Duration = require("duration-js");
const interpolate = require("../helpers/interpolate");
class ScheduleAction {
constructor(options) {
if(typeof options === "object")
this.in = options.in;
else
this.in = options;
}
findLast() {
return -1;
}
run(context, issue) {
const { owner, repo, item, simulate, reporter } = context;
const { number } = issue;
const inDuration = new Duration(interpolate(this.in, { context, issue })).valueOf();
reporter({ item, action: "schedule", message: `in ${Math.round(inDuration/1000)} seconds` });
if(simulate) return Promise.resolve();
return schedule(owner, repo, number, inDuration);
}
}
module.exports = ScheduleAction;
================================================
FILE: lib/open-bot/actions/set.js
================================================
const interpolate = require("../helpers/interpolate");
class SetAction {
constructor(options) {
this.id = options.id;
this.value = options.value;
}
findLast() {
return -1;
}
run(context, issue) {
const { reporter, data, item } = context;
const value = interpolate(this.value, { context, issue });
reporter({ item, action: "set", message: `${this.id} = ${value}` });
data[this.id] = value;
return Promise.resolve();
}
}
module.exports = SetAction;
================================================
FILE: lib/open-bot/actions/status.js
================================================
const interpolate = require("../helpers/interpolate");
const findLast = require("../helpers/findLast");
class StatusAction {
constructor(options, key) {
this.description = options.description;
this.state = options.state || "success";
this.target_url = options.target_url;
this.context = options.context;
}
findLast(context, issue) {
const { botUsername } = context;
const { pull_request_statuses } = issue;
const myContext = this.context || botUsername;
const myDescription = interpolate(this.description, { context, issue });
const myTargetUrl = this.target_url && interpolate(this.target_url, { context, issue });
return pull_request_statuses.then(pull_request_statuses => {
return findLast(pull_request_statuses, ({ state, context, target_url, description }) => {
return state === this.state &&
context === myContext &&
(!myTargetUrl || target_url === myTargetUrl) &&
description === myDescription;
});
});
}
run(context, issue) {
const { owner, repo, item, github, reporter, botUsername } = context;
const { pull_request_info } = issue;
const description = interpolate(this.description, { context, issue });
const target_url = this.target_url && interpolate(this.target_url, { context, issue });
reporter({ item, action: "report pull request status", message: description });
return pull_request_info.then(pull_request_info => {
return github.createStatus(owner, repo, pull_request_info.head.sha, this.context || botUsername, this.state, { description, target_url });
});
}
}
module.exports = StatusAction;
================================================
FILE: lib/open-bot/api-utils.js
================================================
const fs = require("fs");
const path = require("path");
const mkdirp = require("mkdirp");
const crypto = require("crypto");
const Queue = require("promise-queue");
const clone = require("clone");
const USER_AGENT = "github.com/open-bot/open-bot";
class ApiUtils {
constructor({ cache }) {
this.cache = cache;
this.queue = new Queue(30);
this.cacheDirectoryExists = Object.create(null);
}
prepareCache(name, cacheDirectory) {
if(!this.cacheDirectoryExists[name]) {
return this.cacheDirectoryExists[name] = new Promise((resolve, reject) => {
fs.exists(cacheDirectory, exist => {
if(exist) return resolve();
mkdirp(cacheDirectory, err => {
if(err) return reject(err);
resolve();
});
});
});
} else {
return this.cacheDirectoryExists[name];
}
}
fetch(name, api, data) {
const cacheDirectory = path.resolve(this.cache, name);
return this.prepareCache(name, cacheDirectory)
.then(() => this.queue.add(() => this._fetchAll(this.cached(api, cacheDirectory), data)));
}
fetchAll(name, api, data) {
const cacheDirectory = path.resolve(this.cache, name);
return this.prepareCache(name, cacheDirectory)
.then(() => this.queue.add(() => this._fetchAll(this.cached(api, cacheDirectory), data)));
}
cached(api, cacheDirectory) {
return (data) => {
const key = JSON.stringify(data);
const digest = crypto.createHash("md5").update(key).digest("hex");
// we assume there is no hash conflict ;)
const cacheFile = path.join(cacheDirectory, digest + ".json");
const requestWithCachedData = (etag, cacheData) => {
data.headers = {
"user-agent": USER_AGENT,
"if-none-match": etag
};
api(data).then(res => {
if(res.status === "304 Not Modified" || res.status === 304) {
return cacheData;
}
return new Promise(resolve => fs.writeFile(cacheFile, JSON.stringify({
etag: res.headers.etag,
data: res.data,
headers: res.headers
}), "utf-8", err => {
return resolve(res);
}));
}, err => {
if(err.code === 304) {
return cacheData;
}
throw err;
});
};
const requestWithoutCachedData = () => {
data.headers = {
"user-agent": USER_AGENT
};
return api(data).then(res => new Promise(resolve => {
fs.writeFile(cacheFile, JSON.stringify({
etag: res.headers.etag,
data: res.data,
headers: res.headers
}), "utf-8", err => {
return resolve(res);
});
}));
};
return new Promise((resolve, reject) => {
fs.exists(cacheFile, exist => {
if(exist) {
fs.readFile(cacheFile, "utf-8", (err, content) => {
if(!err && content) {
try {
const data = JSON.parse(content);
return resolve(requestWithCachedData(etag, data));
} catch(e) {
err = e;
}
}
return resolve(requestWithoutCachedData());
});
} else resolve(requestWithoutCachedData());
});
});
}
}
_fetchAll(api, data) {
data = clone(data);
const usePages = typeof data.per_page !== "number";
if(usePages) {
data.per_page = 100;
data.page = 1;
}
let results = [];
const onResult = (result) => {
if(usePages && Array.isArray(result.data) && result.headers && result.headers.link) {
const links = this._parseLinks(result.headers.link);
result.data.forEach(res => results.push(res));
if(links.next) {
data.page++;
return api(clone(data)).then(onResult);
} else {
return results;
}
} else {
return result.data;
}
};
return api(clone(data)).then(onResult);
}
_fetch(api, data) {
return new Promise((resolve, reject) => {
api(data, (err, result) => {
if(err) return reject(new Error(err));
resolve(result.data);
});
});
}
_parseLinks(link) {
var re = /<([^>]+)>;\s+rel="([a-z]+)"/g;
var links = {};
var match;
while(match = re.exec(link)) {
links[match[2]] = match[1];
}
return links;
}
}
module.exports = ApiUtils;
================================================
FILE: lib/open-bot/filters/IssuePullRequestBaseFilter.js
================================================
const parseDate = require("../helpers/parseDate");
class IssuePullRequestBaseFilter {
constructor(options) {
this.matching = undefined;
this.title = undefined;
this.author = undefined;
this.locked = undefined;
if(typeof options === "string") {
this.matching = new RegExp(options, "i");
} else if(options && typeof options === "object") {
if(typeof options.matching === "string")
this.matching = new RegExp(options.matching, "i");
if(typeof options.title === "string")
this.title = new RegExp(options.title, "i");
if(typeof options.author === "string")
this.author = new RegExp(options.author, "i");
if(typeof options.locked === "boolean")
this.locked = options.locked;
}
}
findLast({ botUsername }, { created_at, body, title, locked, user: { login } }) {
const cd = parseDate(created_at);
if(this.matching) {
if(!this.matching.test(body)) return -cd;
}
if(this.author) {
if(!this.author.test(login)) return -cd;
} else {
if(login === botUsername) return -cd;
}
if(this.title) {
if(!this.title.test(title)) return -cd;
}
if(typeof this.locked === "boolean") {
if(this.locked !== locked) return -cd;
}
return cd;
}
}
module.exports = IssuePullRequestBaseFilter;
================================================
FILE: lib/open-bot/filters/TimeFilter.js
================================================
const Duration = require("duration-js");
class TimeFilter {
constructor(options) {
this.minimum = undefined;
this.maximum = undefined;
this.minimumDate = undefined;
this.maximumDate = undefined;
if(typeof options === "string") {
this.minimum = new Duration(options).valueOf();
} else {
if(typeof options.minimum === "string") {
this.minimum = new Duration(options.minimum).valueOf();
} else if(typeof options.minimum === "number") {
this.minimum = options.minimum;
}
if(typeof options.maximum === "string") {
this.maximum = new Duration(options.maximum).valueOf();
} else if(typeof options.maximum === "number") {
this.maximum = options.maximum;
}
if(options.minimumDate) {
this.minimumDate = new Date(options.minimumDate).getTime();
}
if(options.maximumDate) {
this.maximumDate = new Date(options.maximumDate).getTime();
}
}
}
getDate(context, issue) {
throw new Error("Not implemented");
}
findLast(context, issue) {
return Promise.resolve(this.getDate(context, issue)).then(dateValue => {
if(!dateValue)
return -1;
const date = new Date(dateValue).getTime();
const age = Date.now() - date;
if(this.minimum && age < this.minimum)
return -1;
if(this.maximum && age > this.maximum)
return -1;
if(this.minimumDate && date < this.minimumDate)
return -1;
if(this.maximumDate && date > this.maximumDate)
return -1;
return date;
});
}
}
module.exports = TimeFilter;
================================================
FILE: lib/open-bot/filters/age.js
================================================
const TimeFilter = require("./TimeFilter");
const interpolate = require("../helpers/interpolate");
class AgeFilter extends TimeFilter {
constructor(options) {
super(options);
this.value = "{{issue.created_at}}";
if(options && typeof options === "object") {
if(typeof options.value === "string")
this.value = options.value;
}
}
getDate(context, issue) {
return interpolate(this.value, { context, issue });
}
}
module.exports = AgeFilter;
================================================
FILE: lib/open-bot/filters/all.js
================================================
const seq = require("../helpers/seq");
class AllFilter {
constructor(options) {
const filtersList = require("../filters");
const Config = require("../Config");
this.filters = Config.parseItems(options, filtersList);
}
findLast(context, issue) {
return seq(this.filters, filter => filter.findLast(context, issue))
.then(matchedItems => matchedItems.filter(i => i < 0).length ?
matchedItems.reduce((a, b) => Math.min(a, b), -1) :
matchedItems.reduce((a, b) => Math.max(a, b), 0)
);
}
}
module.exports = AllFilter;
================================================
FILE: lib/open-bot/filters/any.js
================================================
const seq = require("../helpers/seq");
class AnyFilter {
constructor(options) {
const filtersList = require("../filters");
const Config = require("../Config");
this.filters = Config.parseItems(options, filtersList);
}
findLast(context, issue) {
return seq(this.filters, filter => filter.findLast(context, issue))
.then(matchedItems => matchedItems.filter(i => i >= 0).length ?
matchedItems.filter(i => i >= 0).reduce((a, b) => Math.min(a, b), Infinity) :
matchedItems.reduce((a, b) => Math.min(a, b), -1)
);
}
}
module.exports = AnyFilter;
================================================
FILE: lib/open-bot/filters/check.js
================================================
const findLast = require("../helpers/findLast");
class CheckFilter {
constructor(options, key) {
this.id = options.id || key;
this.matching = undefined;
this.conclusion = undefined;
this.name = undefined;
if(typeof options === "string") {
this.matching = new RegExp(options, "i");
} else if(typeof options === "object") {
if(typeof options.matching === "string")
this.matching = new RegExp(options.matching, "i");
if(typeof options.conclusion === "string")
this.conclusion = new RegExp(options.conclusion, "i");
if(typeof options.name === "string")
this.name = new RegExp(options.name, "i");
}
}
findLast({ botUsername, data }, { pull_request_checks }) {
return pull_request_checks.then(pull_request_checks => {
return findLast(pull_request_checks, (check) => {
const { conclusion, name, output: { title } = {}, creator: { login } = {} } = check;
if(login === botUsername) return false;
if(this.matching) {
if(!this.matching.test(title)) return false;
}
if(this.conclusion) {
if(!this.conclusion.test(conclusion)) return false;
}
if(this.name) {
if(!this.name.test(name)) return false;
}
if(this.id) {
data[this.id] = check;
}
return true;
});
});
}
}
module.exports = CheckFilter;
================================================
FILE: lib/open-bot/filters/comment.js
================================================
const findLast = require("../helpers/findLast");
class CommentFilter {
constructor(options, key) {
this.id = options.id || key;
this.matching = undefined;
this.author = undefined;
switch(typeof options) {
case "string":
this.matching = new RegExp(options, "i");
break;
case "object":
if(options) {
if(typeof options.matching === "string")
this.matching = new RegExp(options.matching, "i");
if(typeof options.author === "string")
this.author = new RegExp(options.author, "i");
}
break;
}
}
findLast({ data, botUsername }, { timeline }) {
return timeline.then(timeline => findLast(timeline, event => {
const { event: eventType, actor: { login } = {}, body } = event;
if(eventType !== "commented") return false;
if(this.matching) {
if(!this.matching.test(body)) return false;
}
if(this.author) {
if(!login || !this.author.test(login)) return false;
} else {
if(login === botUsername) return false;
}
if(this.id) {
data[this.id] = event;
if(this.matching)
data[`${this.id}_match`] = this.matching.exec(body);
}
return true;
}));
}
}
module.exports = CommentFilter;
================================================
FILE: lib/open-bot/filters/commit.js
================================================
const findLast = require("../helpers/findLast");
class CommitFilter {
constructor(options, key) {
this.id = options.id || key;
this.matching = undefined;
this.author = undefined;
this.committer = undefined;
switch(typeof options) {
case "string":
this.matching = new RegExp(options, "i");
break;
case "object":
if(options) {
if(typeof options.matching === "string")
this.matching = new RegExp(options.matching, "i");
if(typeof options.author === "string")
this.author = new RegExp(options.author, "i");
if(typeof options.committer === "string")
this.committer = new RegExp(options.committer, "i");
}
break;
}
}
findLast({ data, botUsername }, { pull_request_commits }) {
return pull_request_commits.then(commits => findLast(commits, event => {
const { committer: comitterObject, author: authorObject, commit: { message } = {} } = event;
const { login: committer } = comitterObject || {};
const { login: author } = authorObject || {};
if(this.matching) {
if(!this.matching.test(message)) return false;
}
if(this.author) {
if(!author || !this.author.test(author)) return false;
}
if(this.committer) {
if(!committer || !this.committer.test(committer)) return false;
} else {
if(committer === botUsername) return false;
}
if(this.id)
data[this.id] = event;
return true;
}));
}
}
module.exports = CommitFilter;
================================================
FILE: lib/open-bot/filters/ensure.js
================================================
const interpolate = require("../helpers/interpolate");
const findLast = require("../helpers/findLast");
const matchRange = require("../helpers/matchRange");
class EnsureFilter {
constructor(options) {
this.value = options.value;
this.matching = undefined;
this.notMatching = undefined;
this.equals = undefined;
this.notEquals = undefined;
this.range = undefined;
if(typeof options.matching === "string")
this.matching = new RegExp(options.matching, "i");
if(typeof options.notMatching === "string")
this.notMatching = new RegExp(options.notMatching, "i");
if(typeof options.equals !== "undefined")
this.equals = options.equals;
if(typeof options.notEquals !== "undefined")
this.notEquals = options.notEquals;
if(typeof options.range !== "undefined")
this.range = options.range;
}
findLast(context, issue) {
const value = interpolate(this.value, { context, issue });
if(this.matching) {
if(!this.matching.test(value)) return -1;
}
if(this.notMatching) {
if(this.notMatching.test(value)) return -1;
}
if(typeof this.equals !== "undefined") {
const equ = typeof this.equals === "string" ? interpolate(this.equals, { context, issue }) : this.equals;
if(equ !== value) return -1;
}
if(typeof this.notEquals !== "undefined") {
const neq = typeof this.notEquals === "string" ? interpolate(this.notEquals, { context, issue }) : this.notEquals;
if(neq === value) return -1;
}
if(typeof this.range !== "undefined") {
if(!matchRange(this.range, +value)) return -1;
}
return 1;
}
}
module.exports = EnsureFilter;
================================================
FILE: lib/open-bot/filters/fetch.js
================================================
const findLast = require("../helpers/findLast");
const matchRange = require("../helpers/matchRange");
class FetchFilter {
constructor(options, key) {
this.id = options.id || key;
if(typeof options === "string")
this.value = options;
else
this.value = options.value;
}
findLast({ data }) {
const items = this.value.split(".");
let current = data;
for(let item of items) {
if(typeof current !== "object" || !current) {
return -1;
}
current = current[item];
}
return Promise.resolve(current).then(result => {
if(this.id) {
data[this.id] = result;
}
return 1;
});
}
}
module.exports = FetchFilter;
================================================
FILE: lib/open-bot/filters/in_order.js
================================================
const seq = require("../helpers/seq");
class InOrderFilter {
constructor(options) {
const filtersList = require("../filters");
const Config = require("../Config");
this.filters = Config.parseItems(options, filtersList);
}
findLast(context, issue) {
return seq(this.filters, filter => filter.findLast(context, issue))
.then(matchedItems => {
if(matchedItems.some(item => item < 0)) {
return matchedItems.filter(item => item < 0)[0];
}
for(let i = 1; i < matchedItems.length; i++) {
if(matchedItems[i - 1] > matchedItems[i])
return -matchedItems[i - 1];
}
return matchedItems[matchedItems.length - 1];
});
}
}
module.exports = InOrderFilter;
================================================
FILE: lib/open-bot/filters/index.js
================================================
exports = module.exports = Object.create(null);
exports.issue = require("./issue");
exports.pull_request = require("./pull_request");
exports.comment = require("./comment");
exports.number_of_comments = require("./number_of_comments");
exports.threshold = require("./threshold");
exports.label = require("./label");
exports.open = require("./open");
exports.age = require("./age");
exports.last_action_age = require("./last_action_age");
exports.not = require("./not");
exports.any = require("./any");
exports.all = require("./all");
exports.in_order = require("./in_order");
exports.commit = require("./commit");
exports.review = require("./review");
exports.status = require("./status");
exports.check = require("./check");
exports.ensure = require("./ensure");
exports.permission = require("./permission");
exports.travis_job = require("./travis_job");
exports.fetch = require("./fetch");
exports.match = require("./match");
exports.string_cleanup = require("./string_cleanup");
================================================
FILE: lib/open-bot/filters/issue.js
================================================
const parseDate = require("../helpers/parseDate");
const IssuePullRequestBaseFilter = require("./IssuePullRequestBaseFilter");
class IssueFilter extends IssuePullRequestBaseFilter {
constructor(options) {
super(options);
}
findLast(context, issue) {
const { pull_request, created_at } = issue;
const cd = parseDate(created_at);
if(pull_request) return -cd;
return super.findLast(context, issue);
}
}
module.exports = IssueFilter;
================================================
FILE: lib/open-bot/filters/label.js
================================================
const findLast = require("../helpers/findLast");
const parseDate = require("../helpers/parseDate");
class LabelFilter {
constructor(options, key) {
this.id = options.id || key;
this.labels = undefined;
this.labelRegExp = undefined;
if(typeof options === "string") {
this.labels = [options];
} else if(Array.isArray(options)) {
this.labels = options;
}
if(typeof options.labelRegExp === "string")
this.labelRegExp = new RegExp(options.labelRegExp, "i");
if((!this.labels || this.labels.length === 0) && !this.labelRegExp)
throw new Error("No labels specified in 'label' and no 'labelRegExp' specified");
}
findLast({ data, botUsername }, { created_at, labels, timeline }) {
const labelNames = labels.map(label => label.name);
let hasLabel = true;
if(this.labels && !this.labels.every(label => labelNames.includes(label)))
hasLabel = false;
if(this.labelRegExp && !labelNames.some(label => this.labelRegExp.test(label)))
hasLabel = false;
return timeline.then(timeline => {
let info = findLast(timeline, event => {
const { event: eventType, label: { name } = {}, actor: { login } = {} } = event;
if(eventType !== (hasLabel ? "labeled" : "unlabeled")) return false;
if(login === botUsername) return false;
if(this.labels && this.labels.includes(name)) return true;
if(this.labelRegExp && this.labelRegExp.test(name)) return true;
if(this.id) {
data[this.id] = event;
}
});
info = info < 0 ? parseDate(created_at) : info;
return hasLabel ? info : -info;
});
}
}
module.exports = LabelFilter;
================================================
FILE: lib/open-bot/filters/last_action_age.js
================================================
const findLast = require("../helpers/findLast");
const TimeFilter = require("./TimeFilter");
const USER_ACTIONS = [
"assigned",
"closed",
"commented",
"committed",
"cross-referenced",
"head_ref_restored",
"labeled",
"milestoned",
"renamed",
"reopened",
"review_requested",
"reviewed",
"line-commented"
];
class LastActionAgeFilter extends TimeFilter {
constructor(options) {
super(options);
if(typeof options === "object") {
this.includeBotActions = !!options.includeBotActions;
}
}
getDate({ botUsername }, { created_at, timeline }) {
return timeline.then(timeline => {
const maxCommentDate = findLast(timeline, event =>
USER_ACTIONS.includes(event.event) &&
(!event.actor || this.includeBotActions || event.actor.login !== botUsername)
);
const issueDate = new Date(created_at).getTime();
return Math.max(issueDate, maxCommentDate);
});
}
}
module.exports = LastActionAgeFilter;
================================================
FILE: lib/open-bot/filters/match.js
================================================
const interpolate = require("../helpers/interpolate");
class MatchFilter {
constructor(options, key) {
this.id = options.id || key;
this.value = options.value;
this.matching = new RegExp(options.matching, "i");
}
findLast(context, issue) {
const value = interpolate(this.value, { context, issue });
const match = this.matching.exec(value);
if(!match) return -1;
if(this.id) {
context.data[this.id] = match;
}
return 1;
}
}
module.exports = MatchFilter;
================================================
FILE: lib/open-bot/filters/not.js
================================================
const seq = require("../helpers/seq");
class NotFilter {
constructor(options) {
const filtersList = require("../filters");
const Config = require("../Config");
this.filters = Config.parseItems(options, filtersList);
}
findLast(context, issue) {
return seq(this.filters, filter => filter.findLast(context, issue))
.then(matchedItems => matchedItems.filter(i => i < 0).length ?
-matchedItems.reduce((a, b) => Math.min(a, b), -1) :
-matchedItems.reduce((a, b) => Math.max(a, b), 0)
);
}
}
module.exports = NotFilter;
================================================
FILE: lib/open-bot/filters/number_of_comments.js
================================================
const findCount = require("../helpers/findCount");
const parseDate = require("../helpers/parseDate");
class NumberOfCommentsFilter {
constructor(options) {
this.minimum = undefined;
this.maximum = undefined;
if(typeof options === "number") {
this.minimum = options;
} else {
if(typeof options.minimum === "number")
this.minimum = options.minimum;
if(typeof options.maximum === "number")
this.maximum = options.maximum;
}
}
findLast({ }, { timeline, created_at }) {
return timeline.then(timeline => findCount(timeline, this.minimum, this.maximum, parseDate(created_at), ({ event }) => {
return event === "commented";
}));
}
}
module.exports = NumberOfCommentsFilter;
================================================
FILE: lib/open-bot/filters/open.js
================================================
const findLast = require("../helpers/findLast");
const parseDate = require("../helpers/parseDate");
class OpenFilter {
constructor(options) {
this.state = options ? "open" : "closed";
}
findLast({ botUsername }, { created_at, timeline, state }) {
const isTrue = this.state === state;
const lookFor = state === "open" ? "reopened" : "closed";
return timeline.then(timeline => {
let lastChange = findLast(timeline, ({ event, actor: { login } = {} }) => {
return event === lookFor &&
login !== botUsername;
})
if(lastChange < 0) lastChange = parseDate(created_at);
return isTrue ? lastChange : -lastChange;
});
}
}
module.exports = OpenFilter;
================================================
FILE: lib/open-bot/filters/permission.js
================================================
const interpolate = require("../helpers/interpolate");
class PermissionFilter {
constructor(options, key) {
this.id = options.id || key;
this.user = "{{issue.user.login}}";
this.matching = /^(write|admin)$/;
if(typeof options === "string") {
this.matching = new RegExp(options, "i");
} else if(options && typeof options === "object") {
if(typeof options.user === "string")
this.user = options.user;
if(typeof options.matching === "string")
this.matching = new RegExp(options.matching, "i");
}
}
findLast(context, issue) {
const user = interpolate(this.user, { context, issue });
const { owner, repo, github } = context;
return github.getPermissionForRepo(owner, repo, user).then(({ permission }) => {
return this.matching.test(permission) ? 1 : -1;
});
}
}
module.exports = PermissionFilter;
================================================
FILE: lib/open-bot/filters/pull_request.js
================================================
const parseDate = require("../helpers/parseDate");
const IssuePullRequestBaseFilter = require("./IssuePullRequestBaseFilter");
const matchRange = require("../helpers/matchRange");
class PullRequestFilter extends IssuePullRequestBaseFilter {
constructor(options, key) {
super(options);
this.id = options.id || key;
this.head_ref = undefined;
this.base_ref = undefined;
this.merged = undefined;
this.mergeable = undefined;
this.mergeable_state = undefined;
this.merged_by = undefined;
this.additions = undefined;
this.deletions = undefined;
this.commits = undefined;
this.changed_files = undefined;
if(options && typeof options === "object") {
if(typeof options.head_ref === "string")
this.head_ref = new RegExp(options.head_ref, "i");
if(typeof options.base_ref === "string")
this.base_ref = new RegExp(options.base_ref, "i");
if(typeof options.mergeable_state === "string")
this.mergeable_state = new RegExp(options.mergeable_state, "i");
if(typeof options.merged_by === "string")
this.merged_by = new RegExp(options.merged_by, "i");
if(typeof options.mergeable === "boolean")
this.mergeable = options.mergeable;
if(typeof options.merged === "boolean")
this.merged = options.merged;
if(options.additions)
this.additions = options.additions;
if(options.deletions)
this.deletions = options.deletions;
if(options.commits)
this.commits = options.commits;
if(options.changed_files)
this.changed_files = options.changed_files;
}
}
findLast(context, issue) {
const { pull_request, created_at, pull_request_info } = issue;
const cd = parseDate(created_at);
if(!pull_request) return -cd;
return pull_request_info.then(pull_request_info => {
const {
head: { ref: head_ref } = {},
base: { ref: base_ref } = {},
merged,
mergeable,
mergeable_state,
merged_by,
additions,
deletions,
commits,
changed_files,
} = pull_request_info;
if(typeof this.merged === "boolean") {
if(merged !== this.merged) return -cd;
}
if(typeof this.mergeable === "boolean") {
if(mergeable !== this.mergeable) return -cd;
}
if(this.head_ref) {
if(!this.head_ref.test(head_ref)) return -cd;
}
if(this.base_ref) {
if(!this.base_ref.test(base_ref)) return -cd;
}
if(this.merged_by) {
if(!merged_by || typeof merged_by !== "object") return -cd;
if(!this.merged_by.test(merged_by.login)) return -cd;
}
if(this.mergeable_state) {
if(!this.mergeable_state.test(mergeable_state)) return -cd;
}
if(this.additions) {
if(!matchRange(this.additions, additions)) return -cd;
}
if(this.deletions) {
if(!matchRange(this.deletions, deletions)) return -cd;
}
if(this.commits) {
if(!matchRange(this.commits, commits)) return -cd;
}
if(this.changed_files) {
if(!matchRange(this.changed_files, changed_files)) return -cd;
}
context.data[this.id] = pull_request_info;
return super.findLast(context, issue);
});
}
}
module.exports = PullRequestFilter;
================================================
FILE: lib/open-bot/filters/review.js
================================================
const findLast = require("../helpers/findLast");
class ReviewFilter {
constructor(options, key) {
this.id = options.id || key;
this.matching = undefined;
this.state = undefined;
this.author = undefined;
this.upToDate = undefined;
switch(typeof options) {
case "string":
this.matching = new RegExp(options, "i");
break;
case "object":
if(options) {
if(typeof options.matching === "string")
this.matching = new RegExp(options.matching, "i");
if(typeof options.state === "string")
this.state = new RegExp(options.state, "i");
if(typeof options.author === "string")
this.author = new RegExp(options.author, "i");
if(typeof options.upToDate === "boolean")
this.upToDate = options.upToDate;
}
break;
}
}
findLast({ data, botUsername }, { pull_request_info, pull_request_reviews }) {
return pull_request_info.then(pull_request_info =>
pull_request_reviews.then(timeline => findLast(timeline, event => {
const { commit_id, user: { login } = {}, body, state } = event;
if(typeof this.upToDate === "boolean") {
const isUpToDate = pull_request_info.head.sha === commit_id;
if(this.upToDate !== isUpToDate) return false;
}
if(this.state) {
if(!this.state.test(state)) return false;
}
if(this.matching) {
if(!this.matching.test(body)) return false;
}
if(this.author) {
if(!login || !this.author.test(login)) return false;
} else {
if(login === botUsername) return false;
}
if(this.id)
data[this.id] = event;
return true;
})));
}
}
module.exports = ReviewFilter;
================================================
FILE: lib/open-bot/filters/status.js
================================================
const findLast = require("../helpers/findLast");
class StatusFilter {
constructor(options, key) {
this.id = options.id || key;
this.matching = undefined;
this.state = undefined;
this.context = undefined;
if(typeof options === "string") {
this.matching = new RegExp(options, "i");
} else if(typeof options === "object") {
if(typeof options.matching === "string")
this.matching = new RegExp(options.matching, "i");
if(typeof options.state === "string")
this.state = new RegExp(options.state, "i");
if(typeof options.context === "string")
this.context = new RegExp(options.context, "i");
}
}
findLast({ botUsername, data }, { pull_request_statuses }) {
return pull_request_statuses.then(pull_request_statuses => {
return findLast(pull_request_statuses, (status) => {
const { state, context, description, creator: { login } = {} } = status;
if(login === botUsername) return false;
if(this.matching) {
if(!this.matching.test(description)) return false;
}
if(this.state) {
if(!this.state.test(state)) return false;
}
if(this.context) {
if(!this.context.test(context)) return false;
}
if(this.id) {
data[this.id] = status;
}
return true;
});
});
}
}
module.exports = StatusFilter;
================================================
FILE: lib/open-bot/filters/string_cleanup.js
================================================
const interpolate = require("../helpers/interpolate");
class StringCleanupFilter {
constructor(options, key) {
this.id = options.id || key;
this.value = options.value;
if(Array.isArray(options.remove))
this.remove = options.remove.map(item => new RegExp(item, "gi"));
else
this.remove = [new RegExp(options.remove, "gi")];
}
findLast(context, issue) {
let value = interpolate(this.value, { context, issue });
this.remove.forEach(item => {
value = value.replace(item, "");
});
if(this.id) {
context.data[this.id] = value;
}
return 1;
}
}
module.exports = StringCleanupFilter;
================================================
FILE: lib/open-bot/filters/threshold.js
================================================
const seq = require("../helpers/seq");
const parseDate = require("../helpers/parseDate");
const findCount = require("../helpers/findCount");
class ThresholdFilter {
constructor(options) {
const filtersList = require("../filters");
const Config = require("../Config");
this.filters = Config.parseItems(options.filters, filtersList);
this.minimum = typeof options.minimum === "number" && options.minimum;
this.maximum = typeof options.maximum === "number" && options.maximum;
if(typeof this.minimum !== "number" && typeof this.maximum !== "number")
throw new Error("Must specify minimum and/or maximum of threshold");
}
findLast(context, issue) {
return seq(this.filters, filter => filter.findLast(context, issue))
.then(matchedItems => matchedItems.sort((a, b) => Math.abs(a) - Math.abs(b)))
.then(matchedItems => findCount(matchedItems, this.minimum, this.maximum, parseDate(issue.created_at), time => time >= 0));
}
}
module.exports = ThresholdFilter;
================================================
FILE: lib/open-bot/filters/travis_job.js
================================================
const findLast = require("../helpers/findLast");
const matchRange = require("../helpers/matchRange");
class TravisJobFilter {
constructor(options, key) {
this.id = options.id || key;
this.config = undefined;
this.state = undefined;
this.allow_failure = undefined;
if(options.config && typeof options.config === "object") {
this.config = Object.keys(options.config).reduce((obj, key) => {
obj[key] = new RegExp(options.config[key], "i");
return obj;
}, {});
}
if(typeof options.state === "string")
this.state = new RegExp(options.state, "i");
if(typeof options.allow_failure === "boolean")
this.allow_failure = options.allow_failure;
}
findLast({ data, botUsername }, { travis_jobs }) {
return travis_jobs.then(travis_jobs => findLast(travis_jobs, travis_job => {
const { config, state, allow_failure } = travis_job;
if(typeof this.allow_failure === "boolean") {
if(this.allow_failure !== allow_failure) return false;
}
if(this.state) {
if(!this.state.test(state)) return false;
}
if(this.config) {
const keys = Object.keys(this.config);
for(let key of keys) {
if(!this.config[key].test(config[key])) return false;
}
}
if(this.id)
data[this.id] = travis_job;
return true;
}));
}
}
module.exports = TravisJobFilter;
================================================
FILE: lib/open-bot/github.js
================================================
const { Octokit } = require("@octokit/rest");
const clone = require("clone");
const seq = require("./helpers/seq");
const ApiUtils = require("./api-utils");
const USER_AGENT = "github.com/open-bot/open-bot";
class Github {
constructor({ token, cache, simulate }) {
this.simulate = !!simulate;
this.api = new Octokit({
auth: token,
version: "3.0.0",
userAgent: USER_AGENT,
previews: ["mockingbird-preview", "squirrel-girl-preview"],
includePreview: true
});
this.apiUtils = new ApiUtils({ cache });
}
getReposOfUser(username) {
return this._fetch("repos.getForUser", this.api.repos.listForUser, {
username
});
}
getReposOfOrg(org) {
return this._fetch("repos.getForOrg", this.api.repos.listForOrg, {
org
});
}
getRepo(owner, repo) {
return this._fetch("repos.get", this.api.repos.get, {
owner,
repo
});
}
getIssuesForRepo(owner, repo, { state = "all", since, labels } = {}) {
return this._fetch("issues.getForRepo", this.api.issues.listForRepo, this.filterUndefined({
owner,
repo,
state,
since,
labels,
sort: "updated"
}));
}
getPullRequestForCommit(owner, repo, sha) {
return this._fetch("pullRequests.getAll", this.api.pulls.list, {
per_page: 10,
owner,
repo,
state: "open"
}).then(pullRequests => pullRequests.filter(pr => pr.head && pr.head.sha === sha)[0]);
}
getIssue(owner, repo, issue_number) {
return this._fetch("issues.get", this.api.issues.get, {
owner,
repo,
issue_number
});
}
getEventsForIssue(owner, repo, issue_number) {
return this._fetch("issues.getEventsTimeline", this.api.issues.listEventsForTimeline, {
owner,
repo,
issue_number
});
}
getCommentsForIssue(owner, repo, issue_number) {
return this._fetch("issues.getComments", this.api.issues.listComments, {
owner,
repo,
issue_number
});
}
getPermissionForRepo(owner, repo, username) {
return this._fetch("repos.reviewUserPermissionLevel", this.api.repos.getCollaboratorPermissionLevel, {
owner,
repo,
username
});
}
getPullRequest(owner, repo, pull_number) {
return this._fetch("pullRequests.get", this.api.pulls.get, {
owner,
repo,
pull_number
});
}
getPullRequest(owner, repo, pull_number) {
return this._fetch("pullRequests.get", this.api.pulls.get, {
owner,
repo,
pull_number
});
}
getCommitsForPullRequest(owner, repo, pull_number) {
return this._fetch("pullRequests.getCommits", this.api.pulls.listCommits, {
owner,
repo,
pull_number
});
}
getFilesForPullRequest(owner, repo, pull_number) {
return this._fetch("pullRequests.getFiles", this.api.pulls.listFiles, {
owner,
repo,
pull_number
});
}
getReviewsForPullRequest(owner, repo, pull_number) {
return this._fetch("pullRequests.getReviews", this.api.pulls.listReviews, {
owner,
repo,
pull_number
});
}
getReviewRequestsForPullRequest(owner, repo, pull_number) {
return this._fetch("pulls.listRequestedReviewers", this.api.pulls.listRequestedReviewers, {
owner,
repo,
pull_number
});
}
getStatuses(owner, repo, ref) {
return this._fetch("repos.listCommitStatusesForRef", this.api.repos.listCommitStatusesForRef, {
owner,
repo,
ref
});
}
getChecks(owner, repo, ref) {
return this._fetch("repos.checks.listForRef", this.api.checks.listForRef, {
owner,
repo,
ref
});
}
getBlob(owner, repo, pathInRepo) {
return this._fetch("repos.getContent", this.api.repos.getContent, {
owner,
repo,
path: pathInRepo
});
}
editIssue(owner, repo, issue_number, update) {
update = clone(update);
update.owner = owner;
update.repo = repo;
update.issue_number = issue_number;
return this._action(this.api.issues.update, update);
}
addLabels(owner, repo, issue_number, labels) {
return this._action(this.api.issues.addLabels, {
owner,
repo,
issue_number,
labels
});
}
removeLabels(owner, repo, issue_number, labels) {
return seq(labels, label => this._action(this.api.issues.removeLabel, {
owner,
repo,
issue_number,
name: label
}));
}
createIssue(owner, repo, title, body) {
return this._action(this.api.issues.create, {
owner,
repo,
title,
body
});
}
createComment(owner, repo, issue_number, body) {
return this._action(this.api.issues.createComment, {
owner,
repo,
issue_number,
body
});
}
editComment(owner, repo, comment_id, body) {
return this._action(this.api.issues.updateComment, {
owner,
repo,
comment_id,
body
});
}
deleteComment(owner, repo, comment_id) {
return this._action(this.api.issues.deleteComment, {
owner,
repo,
comment_id
});
}
createStatus(owner, repo, sha, context, state, { target_url, description } = {}) {
return this._action(this.api.repos.createCommitStatus, {
owner,
repo,
sha,
state,
target_url,
description,
context
});
}
merge(owner, repo, pull_number, commit_title, commit_message, sha, merge_method) {
return this._action(this.api.pulls.merge, {
owner,
repo,
pull_number,
commit_title,
commit_message,
sha,
merge_method
});
}
filterUndefined(obj) {
return Object.keys(obj).reduce((o, k) => {
if(typeof obj[k] !== "undefined")
o[k] = obj[k];
return o;
}, {})
}
_action(api, data) {
if(this.simulate) return Promise.resolve();
return api(data);
}
_fetch(name, api, data) {
return this.apiUtils.fetchAll(name, api, data);
}
}
module.exports = Github;
================================================
FILE: lib/open-bot/helpers/findCount.js
================================================
const getDate = require("./getDate");
module.exports = function findCount(array, min, max, startDate, fn) {
if(typeof min !== "number") min = 0;
if(typeof max !== "number") max = Infinity;
let result = array.filter((item, i) => !fn(item, i, array)).map(getDate).reduce((a, b) => Math.min(a, b), -startDate);
if(min <= 0) {
result = -result;
}
let count = 0;
for(let i = 0; i < array.length; i++) {
if(fn(array[i], i, array)) {
count++;
if(count === min) {
result = Math.abs(getDate(array[i]));
}
if(count === max + 1) {
result = -Math.abs(getDate(array[i]));
}
}
}
return result;
}
================================================
FILE: lib/open-bot/helpers/findLast.js
================================================
const findLastIndex = require("./findLastIndex");
const getDate = require("./getDate");
module.exports = function findLast(list, fn) {
const idx = findLastIndex(list, fn);
if(idx < 0) return -1;
const item = list[idx];
return getDate(item) + (idx / list.length);
}
================================================
FILE: lib/open-bot/helpers/findLastIndex.js
================================================
module.exports = function findLastIndex(array, fn) {
for(let i = array.length - 1; i >= 0; i--) {
if(fn(array[i], i, array))
return i;
}
return -1;
}
================================================
FILE: lib/open-bot/helpers/getDate.js
================================================
const parseDate = require("./parseDate");
module.exports = item => {
if(typeof item === "number") return item;
return parseDate(item.submitted_at || item.created_at || item.finished_at || item.started_at || (item.commit && item.commit.committer && item.commit.committer.date)) || 1;
}
================================================
FILE: lib/open-bot/helpers/interpolate.js
================================================
const handlebars = require("handlebars");
handlebars.registerHelper("quote", function(value) {
return new handlebars.SafeString(value.split("\n").map(line => `> ${line}`).join("\n"));
});
handlebars.registerHelper("stringify", function(value) {
return JSON.stringify(value, 0, 2);
});
module.exports = function interpolate(message, { context, issue, identifier }) {
const data = Object.assign({
owner: context.owner,
repo: context.repo,
item: context.item,
botUsername: context.botUsername,
data: context.data,
issue: issue
}, context.data);
const template = handlebars.compile(message);
let result = template(data);
if(identifier) {
result = `<!-- identifier: ${identifier} -->\n\n` + result;
}
return result;
};
================================================
FILE: lib/open-bot/helpers/matchRange.js
================================================
module.exports = function matchRange(range, value) {
if(typeof value !== "number")
value = +value;
if(isNaN(value)) return false;
if(typeof range === "number") {
return value >= range;
} else if(typeof range === "string") {
const parts = range.split(",");
if(parts.length > 1) {
return parts.some(part => matchRange(part, value));
}
const regex = /(<|<=|>|>=|==|!=)\s*(\d+)|(\d+)\s*-\s*(\d+)/g;
let match;
do {
match = regex.exec(range);
if(!match) return true;
if(match[1]) {
const cmp = +match[2];
switch(match[1]) {
case "<":
if(value >= cmp) return false;
break;
case "<=":
if(value > cmp) return false;
break;
case ">":
if(value <= cmp) return false;
break;
case ">=":
if(value > cmp) return false;
break;
case "==":
if(value !== cmp) return false;
break;
case "!=":
if(value === cmp) return false;
break;
}
} else if(match[3]) {
const start = +match[3];
const end = +match[4];
if(value < start || value > end) return false;
}
} while(true);
}
return false;
}
================================================
FILE: lib/open-bot/helpers/once.js
================================================
module.exports = function once(factory) {
let result;
return () => {
if(result) return result;
return result = factory();
}
};
================================================
FILE: lib/open-bot/helpers/parseDate.js
================================================
module.exports = date => {
return new Date(date).getTime();
}
================================================
FILE: lib/open-bot/helpers/seq.js
================================================
module.exports = function seq(arr, fn, breakCondition) {
let breaked = false;
let i = 0;
return arr.reduce(
(p, item) => p
.then((arr) => breaked ? arr : Promise.resolve(fn(item, i++))
.then(res => {
if(breakCondition && breakCondition(res))
breaked = true;
return res;
})
.then(res => arr.concat([res]))
),
Promise.resolve([])
);
};
================================================
FILE: lib/open-bot/open-bot.js
================================================
const Github = require("./github");
const Travis = require("./travis");
const Config = require("./Config");
const Queue = require("promise-queue");
const once = require("./helpers/once");
const yaml = require("js-yaml");
function queueAll(queue, array, fn) {
return Promise.all(array.map(item => queue.add(() => fn(item))));
}
class OpenBot {
constructor(config) {
this.config = config;
this.github = new Github(config);
this.travis = new Travis({
cache: config.cache,
simulate: config.simulate
});
this.configCache = Object.create(null);
}
getRepo(owner, repo) {
return this.github.getRepo(owner, repo);
}
getIssue(owner, repo, number) {
return this.github.getIssue(owner, repo, number)
.then(issue => this.getRepo(owner, repo).then(repo => {
issue.repo = repo;
return issue;
}))
.then(issue => {
issue.type = "issue";
issue.full_name = `${owner}/${repo}#${number}`;
return issue;
});
}
getReposOfOrg(org) {
return this.github.getReposOfOrg(org);
}
getIssuesForRepo(org, repo, filter) {
return this.github.getIssuesForRepo(org, repo, filter);
}
getConfig(owner, repo) {
const cacheKey = `${owner}/${repo}`;
let cacheItem = this.configCache[cacheKey];
if(cacheItem) return cacheItem;
let settingsJson;
if(this.config.overrideSettings) {
settingsJson = Promise.resolve(this.config.overrideSettings);
} else {
settingsJson = this.github.getBlob(owner, repo, "/open-bot.yaml")
.catch(() => this.github.getBlob(owner, repo, "/open-bot.yml"))
.then(blob => Buffer.from(blob.content, "base64").toString("utf-8"))
.then(content => yaml.safeLoad(content));
}
return this.configCache[cacheKey] = settingsJson
.then(config => new Config(config))
.catch(err => {
throw new Error(`Cannot read settings file in ${owner}/${repo}: ${err}`)
});
}
process({ workItems, reporter = () => {}, simulate = false, issueFilter = {} }) {
const queue = new Queue(10);
const FETCH_ACTION = "fetch config and data";
const PROCESS_ACTION = "process issue";
workItems.forEach(({ full_name }) => reporter({
item: full_name,
action: FETCH_ACTION,
change: "queued"
}));
return queueAll(queue, workItems, workItem => {
reporter({
item: workItem.full_name,
action: FETCH_ACTION,
change: "start"
});
if(workItem.type === "issue") {
return this.getConfig(workItem.repo.owner.login, workItem.repo.name)
.then(config => [{
config,
repo: workItem.repo,
issue: workItem
}])
.then(workItems => {
reporter({
item: workItem.full_name,
action: FETCH_ACTION,
change: "done"
});
return workItems;
})
.catch(err => {
reporter({
item: workItem.full_name,
error: "Failed to process work item: " + err,
stack: err.stack
});
return [];
});
}
const repo = workItem;
const config = this.getConfig(repo.owner.login, repo.name);
return config
.then(config => {
if(config.bot !== this.config.user)
throw new Error("Reject to process repo of different bot user (config.bot property)");
return this.github.getIssuesForRepo(repo.owner.login, repo.name, issueFilter)
.then(issues => {
reporter({
item: workItem.full_name,
action: FETCH_ACTION,
change: "done"
});
return issues.map(issue => ({
config,
repo,
issue
}));
});
})
.catch(err => {
reporter({
item: workItem.full_name,
error: "Failed to process work item: " + err,
stack: err.stack
});
return [];
});
})
.then(issuesLists => issuesLists.reduce((list, item) => list.concat(item), []))
.then(issues => {
issues.forEach(({ repo: { full_name }, issue: { number }}) => reporter({
item: full_name + "#" + number,
action: PROCESS_ACTION,
change: "queued"
}));
return queueAll(queue, issues, ({ config, repo, issue }) => {
reporter({
item: repo.full_name + "#" + issue.number,
action: PROCESS_ACTION,
change: "start"
});
return this.processIssueWithData({
config,
owner: repo.owner.login,
repo: repo.name,
issue,
reporter,
simulate
})
.then(() => {
reporter({
item: repo.full_name + "#" + issue.number,
action: PROCESS_ACTION,
change: "done"
});
})
.catch(err => {
reporter({
item: repo.full_name + "#" + issue.number,
error: "Failed to process work item: " + err,
stack: err.stack
});
});
});
});
}
processIssue({ owner, repo, number, reporter = () => {}, simulate = false }) {
return this.getConfig(owner, repo).catch(err => null).then(config => {
if(!config) {
reporter({
item: `${owner}/${repo}#${number}`,
action: "skip (no config)"
});
return;
}
if(config.bot !== this.config.user) {
reporter({
item: `${owner}/${repo}#${number}`,
action: "skip (different bot user)"
});
return;
}
return this.github.getIssue(owner, repo, number).then(issue => {
return this.processIssueWithData({ config, owner, repo, issue, reporter, simulate });
});
});
}
processIssueWithData({ config, owner, repo, issue, reporter = () => {}, simulate = false }) {
Object.defineProperty(issue, "timeline", {
get: once(() => this.github.getEventsForIssue(owner, repo, issue.number))
});
Object.defineProperty(issue, "comments", {
get: once(() => this.github.getCommentsForIssue(owner, repo, issue.number))
});
Object.defineProperty(issue, "pull_request_info", {
get: once(() => issue.pull_request ? this.github.getPullRequest(owner, repo, issue.number) : Promise.resolve(null))
});
Object.defineProperty(issue, "pull_request_commits", {
get: once(() => issue.pull_request ? this.github.getCommitsForPullRequest(owner, repo, issue.number) : Promise.resolve([]))
});
Object.defineProperty(issue, "pull_request_reviews", {
get: once(() => issue.pull_request ? this.github.getReviewsForPullRequest(owner, repo, issue.number) : Promise.resolve([]))
});
Object.defineProperty(issue, "pull_request_statuses", {
get: once(() => {
if(!issue.pull_request) return Promise.resolve([]);
return issue.pull_request_info.then(info => {
if(!info.head || !info.head.sha) return [];
return this.github.getStatuses(owner, repo, info.head.sha)
.then(statuses => statuses.reverse());
});
})
});
Object.defineProperty(issue, "pull_request_checks", {
get: once(() => {
if(!issue.pull_request) return Promise.resolve([]);
return issue.pull_request_info.then(info => {
if(!info.head || !info.head.sha) return [];
return this.github.getChecks(owner, repo, info.head.sha)
.then(checks => checks.check_runs.sort((a, b) => {
const aTime = Math.max(+new Date(a.completed_at || 0), +new Date(a.started_at || 0));
const bTime = Math.max(+new Date(b.completed_at || 0), +new Date(b.started_at || 0));
if(aTime > bTime) return 1;
if(bTime > aTime) return -1;
return 0;
}));
});
})
});
Object.defineProperty(issue, "travis_jobs", {
get: once(() => issue.pull_request_statuses.then(pull_request_statuses => {
const travisStatuses = pull_request_statuses.filter(status => status.context === "continuous-integration/travis-ci/pr");
if(travisStatuses.length === 0) return [];
const travisStatus = travisStatuses[travisStatuses.length - 1];
const match = /^https:\/\/travis-ci\.org\/.+builds\/(\d+)/.exec(travisStatus.target_url);
if(!match) return [];
const buildId = match[1];
return this.travis.getBuild(buildId)
.then(buildData => {
return buildData.jobs.map(jobData => {
const job = Object.assign({}, jobData);
Object.defineProperty(job, "log", {
get: once(() => this.travis.getLog(job.id))
});
Object.defineProperty(job, "rawLog", {
get: once(() => this.travis.getRawLog(job.id))
});
return job;
});
});
}))
});
return config.run({
owner,
repo,
item: `${owner}/${repo}#${issue.number}`,
github: this.github,
travis: this.travis,
botUsername: this.config.user,
data: {},
reporter,
simulate
}, issue);
}
}
module.exports = OpenBot;
================================================
FILE: lib/open-bot/package.json
================================================
{
"name": "open-bot",
"private": true,
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"@octokit/rest": "^18.0.12",
"clone": "^2.1.1",
"duration-js": "^3.9.2",
"handlebars": "^4.5.3",
"js-yaml": "^3.8.2",
"mkdirp": "^0.5.1",
"promise-queue": "^2.2.3",
"request": "^2.88.2"
}
}
================================================
FILE: lib/open-bot/travis.js
================================================
const Queue = require("promise-queue");
const ApiUtils = require("./api-utils");
const request = require("request");
const travisUrl = "https://api.travis-ci.org/";
const travisRequest = (url, method, headers, body, callback) => {
if(typeof body === "object") {
body = JSON.stringify(body);
}
const options = {
headers: Object.assign({
accept: "application/vnd.travis-ci.2+json"
}, headers),
method,
url: travisUrl + url,
body
};
request(options, (err, res) => {
if(err) return callback(err);
if(res.statusCode >= 400) {
return callback(res.body || res.statusCode);
}
const resBody = res.headers["content-type"] && res.headers["content-type"].indexOf("application/json") >= 0 ? JSON.parse(res.body) : res.body;
callback(null, {
meta: {
status: res.statusCode,
etag: res.headers["etag"],
link: res.headers["link"]
},
data: resBody
});
});
}
const travisGetBuild = (data, cb) => travisRequest(`builds/${data.id}`, "GET", data.headers, null, cb);
const travisGetLog = (data, cb) => travisRequest(`jobs/${data.id}/log`, "GET", Object.assign({ accept: "text/plain" }, data.headers), null, cb);
class Travis {
constructor({ simulate, cache }) {
this.simulate = !!simulate;
this.apiUtils = new ApiUtils({ cache });
}
getBuild(id) {
return this._fetch("travis-build", travisGetBuild, { id });
}
getRawLog(id) {
return this._fetch("travis-log", travisGetLog, { id });
}
getLog(id) {
return this._fetch("travis-log", travisGetLog, { id })
.then(log => log
.replace(/travis_fold:start:(.+)[\r\n](.+)[\r\n][\s\S]+?travis_fold:end:\1/g, "$2 [...]")
.replace(/travis_time:(start|end):.+[\r\n]/g, "")
.replace(/\u001b\[[\d;]+m|\u001b\[0K/g, "")
.replace(/\r+\n?/g, "\n")
.trim()
)
}
_fetch(name, api, data) {
return this.apiUtils.fetch(name, api, data);
}
}
module.exports = Travis;
================================================
FILE: lib/open-bot-cli/open-bot.js
================================================
const fs = require("fs");
const path = require("path");
const prompt = require("prompt");
const argv = require("minimist")(process.argv.slice(2));
const OpenBot = require("../open-bot/open-bot");
const schedule = require("../open-bot-scheduler").schedule;
const yaml = require("js-yaml");
const configPath = path.resolve("config.json");
if(argv._.length < 1)
helpCommand();
else {
const command = argv._.shift().toLowerCase();
switch(command) {
case "configurate":
configurateCommand();
break;
case "process":
processCommand();
break;
case "schedule":
scheduleCommand();
break;
default:
helpCommand();
break;
}
}
function helpCommand() {
console.log("open-bot help: Display help");
console.log("open-bot configurate: Ask questions to create a configuration file");
console.log("open-bot process <org>[/<repo>] <org>[/<repo>]: Start/Continue processing repos and organizations");
console.log(" --simulate: Don't do modifications, only simulate actions");
console.log(" --settings: Override repo settings with this settings file");
console.log(" --since: Only process issues/PRs updated since this date");
console.log(" --state: Only process issues/PRs with this state (open/closed)");
console.log(" --label: Only process issues/PRs with this label");
console.log("open-bot schedule <org>/<repo>: Schedule all issues with a rate of 1/minute");
console.log(" --since: Only process issues/PRs updated since this date");
console.log(" --state: Only process issues/PRs with this state (open/closed)");
console.log(" --label: Only process issues/PRs with this label");
//console.log("open-bot server: Start server listening to webhooks which are processed");
}
function displayError(err) {
console.error(err.stack);
console.error(err);
process.exit(1);
}
function configurateCommand() {
loadConfig()
.then((config) => promptConfig(config))
.then(config => new Promise((resolve, reject) => {
fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8", err => {
if(err) return reject(err);
resolve();
});
}))
.catch(displayError);
function promptConfig(defaults) {
const schema = {
properties: {
user: {
description: "Github bot user name",
type: "string",
pattern: /^[^\s]+$/,
default: defaults && defaults.user || undefined,
required: true
},
token: {
description: "OAuth token for user",
type: "string",
default: defaults && defaults.token || undefined,
required: true
},
cache: {
description: "Cache directory for caching",
type: "string",
default: defaults && defaults.cache || ".cache",
required: true,
before: value => path.resolve(value)
},
awsAccountId: {
description: "AWS account id",
type: "string"
}
}
};
return new Promise((resolve, reject) => {
prompt.get(schema, (err, result) => {
if(err) return reject(err);
resolve(result);
});
});
}
}
function scheduleCommand() {
const Queue = require("promise-queue");
loadConfig()
.then(config => {
if(!config) throw new Error("No configuration file. Run 'open-bot configurate'.");
const openBot = new OpenBot(config);
const items = argv._;
return Promise.all(items.map(item => {
const idx = item.indexOf("/");
if(idx >= 0) {
const idx2 = item.indexOf("#");
if(idx2 >= 0) {
return [{
owner: item.substr(0, idx),
repo: item.substr(idx + 1, idx2 - idx - 1),
number: +item.substr(idx2 + 1)
}];
} else {
const owner = item.substr(0, idx);
const repo = item.substr(idx + 1);
return openBot.getIssuesForRepo(owner, repo, {
since: argv.since && new Date(argv.since).toISOString(),
state: argv.state,
labels: argv.label
}).then(list => list.map(item => ({
owner,
repo,
number: item.number
})).reverse());
}
} else {
throw new Error(`Invalid argument '${item}`);
}
})).then(workItems => workItems.reduce((list, item) => list.concat(item), []));
}).then(list => {
const queue = new Queue(1);
let duration = 0;
let remaining = list.length;
return Promise.all(list.map(item => queue.add(() => {
console.log(`Scheduling ${item.owner}/${item.repo}#${item.number} (${--remaining} remaining)`)
return schedule(item.owner, item.repo, item.number, (duration++) * 1000 * 15)
.catch(err => {
console.warn(err.message);
})
.then(() => new Promise(resolve => setTimeout(resolve, 1000)));
})));
}).catch(displayError);
}
function processCommand() {
loadConfig()
.then(config => {
if(argv.settings) {
const file = new Promise((resolve, reject) => {
fs.readFile(path.resolve(argv.settings), "utf-8", (err, content) => {
if(err) return reject(err);
resolve(new Promise((resolve) => {
resolve(yaml.safeLoad(content));
}));
});
});
return file.then(settings => {
config.overrideSettings = settings;
return config;
});
}
return config;
})
.then(config => {
if(argv.simulate)
config.simulate = argv.simulate;
return config;
})
.then(config => {
if(!config) throw new Error("No configuration file. Run 'open-bot configurate'.");
const openBot = new OpenBot(config);
const items = argv._;
const start = new Date();
return Promise.all(items.map(item => {
const idx = item.indexOf("/");
if(idx >= 0) {
const idx2 = item.indexOf("#");
if(idx2 >= 0) {
return openBot.getIssue(item.substr(0, idx), item.substr(idx + 1, idx2 - idx - 1), +item.substr(idx2 + 1));
} else {
return openBot.getRepo(item.substr(0, idx), item.substr(idx + 1));
}
} else {
return openBot.getReposOfOrg(item);
}
}))
.then(workItems => workItems.reduce((list, item) => list.concat(item), []))
.then(workItems => {
console.log("Bot will try to process these items:");
workItems.forEach(repo => {
console.log(" * " + repo.full_name);
});
return workItems;
})
.then(workItems => {
const { reporter, finish } = createReporter(argv.debug);
return openBot.process({
workItems,
reporter,
issueFilter: {
since: argv.since && new Date(argv.since).toISOString(),
state: argv.state,
labels: argv.label
},
simulate: argv.simulate
})
.then(() => {
finish();
});
})
.then(() => {
const end = new Date();
console.log(`Finished at ${end.toLocaleString()} in ${Math.round((end.getTime() - start.getTime()) / 1000)}s`);
});
})
.catch(displayError);
}
function createReporter(showDebug) {
let queuedJobs = 0;
let activeJobs = 0;
let completedJobs = 0;
let lastLineLength = 0;
let actions = [];
let errors = [];
const reporter = ({ item, debug, action, change, message, error, stack }) => {
if(!showDebug && debug) return;
if(error) {
errors.push({
item,
error,
stack
});
} else {
switch(change) {
case "queued":
queuedJobs++;
break;
case "start":
queuedJobs--;
activeJobs++;
break;
case "done":
activeJobs--;
completedJobs++;
break;
default:
actions.push({ item, action, message });
break;
}
const totalJobs = completedJobs + queuedJobs + activeJobs;
let line = `${completedJobs}/${totalJobs} completed, ${Math.floor(completedJobs / totalJobs * 100)}%, ${item} ${action}`;
const prevLineLength = lastLineLength;
lastLineLength = line.length;
if(prevLineLength > lastLineLength)
line += Array(prevLineLength - lastLineLength + 1).join(" ");
line += debug ? "\n" : "\r";
process.stdout.write(line);
}
};
const finish = () => {
console.log("");
if(actions.length > 0) {
console.log("Executed Actions:");
actions.forEach(({ item, action, message }) => {
console.log(` * ${item}: ${action} ${message || ""}`);
});
actions.length = 0;
}
if(errors.length > 0) {
console.log("Errors:");
errors.forEach(({ item, error, stack }) => {
console.log(` * ${item}: ${error}`);
console.log(" " + stack);
});
errors.length = 0;
}
queuedJobs = 0;
activeJobs = 0;
completedJobs = 0;
lastLineLength = 0;
};
return { reporter, finish };
}
function loadConfig() {
return new Promise((resolve, reject) => {
fs.exists(configPath, exist => {
if(!exist) return resolve(null);
fs.readFile(configPath, "utf-8", (err, content) => {
if(err) return reject(err);
try {
resolve(JSON.parse(content));
} catch(e) {
reject(e);
}
});
});
});
}
================================================
FILE: lib/open-bot-cli/package.json
================================================
{
"name": "open-bot-cli",
"private": true,
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"prompt": "^1.0.0"
}
}
================================================
FILE: lib/open-bot-handle-github-event/handler.js
================================================
const AWS = require("aws-sdk");
const config = require("../../config.json");
config.cache = "/tmp/.cache";
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports = (event, context, callback) => {
const content = JSON.parse(event.body);
const owner = content.repository && content.repository.owner && content.repository.owner.login;
const repo = content.repository && content.repository.name;
const number = content.issue && content.issue.number ||
content.pull_request && content.pull_request.number;
const sha = content.commit && content.commit.sha;
if(typeof owner !== "string" || !owner)
throw new Error("[400] owner is not valid")
if(typeof repo !== "string" || !repo)
throw new Error("[400] repo is not valid")
if(typeof number !== "number" && typeof sha === "string") {
// we need to find the issue...
console.log(`${owner}/${repo}#${sha}`);
const Github = require("../open-bot/github");
const github = new Github(config);
github.getPullRequestForCommit(owner, repo, sha).then(pr => {
if(!pr) {
console.log("PR not found");
return callback(null, {
statusCode: 200,
body: JSON.stringify({ message: "nothing to do: commit has no PR" })
});
}
console.log(`Commit is part of PR #${pr.number}`);
publish(pr.number);
}).catch(callback);
} else {
publish(number);
}
function publish(number) {
if(typeof number !== "number" || !(number > 0))
throw new Error("[400] issue_number is not valid")
const task = {
item: `${owner}/${repo}#${number}`,
owner: owner,
repo: repo,
number: number
};
console.log(task.item);
const params = {
TableName: `open-bot-${process.env.STAGE}-queue`,
Key: { "item": task.item },
UpdateExpression: "set #owner = :owner, #repo = :repo, #number = :number, #trigger = :timestamp, #event = :timestamp",
ExpressionAttributeNames: {
"#owner": "owner",
"#repo": "repo",
"#number": "number",
"#trigger": "trigger",
"#event": "event"
},
ExpressionAttributeValues: {
":owner": task.owner,
":repo": task.repo,
":number": task.number,
":timestamp": new Date().getTime()
},
ReturnValues: "ALL_NEW"
};
console.log("Update", params);
dynamoDb.update(params, (err, result) => {
if(err) {
console.error(err && err.stack || err);
return callback(err);
}
console.log("Updated", params, result);
callback(null, {
statusCode: 200,
body: JSON.stringify({ message: "processing successfully enqueued", item: task.item })
});
});
}
};
================================================
FILE: lib/open-bot-handle-github-event/package.json
================================================
{
"name": "open-bot-handle-github-event",
"private": true,
"version": "0.0.0",
"license": "MIT",
"dependencies": {}
}
================================================
FILE: lib/open-bot-process-scheduled-tasks/handler.js
================================================
const AWS = require("aws-sdk");
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports = (event, context, callback) => {
const now = Date.now();
const epoch = Math.floor(now / 36000000); // 1 epoch = 10 hours
const queryParams = {
ExpressionAttributeNames: {
"#item": "item",
"#owner": "owner",
"#repo": "repo",
"#number": "number",
"#schedule": "schedule",
"#epoch": "epoch"
},
TableName: `open-bot-${process.env.STAGE}-schedule`,
IndexName: `schedule-index`,
Limit: 50,
ProjectionExpression: "#item, #owner, #repo, #number, #schedule"
}
const query1 = new Promise((resolve, reject) => {
dynamoDb.query(Object.assign({}, queryParams, {
KeyConditionExpression: "#epoch = :epoch",
ExpressionAttributeValues: {
":epoch": epoch - 1,
}
}), (err, result) => {
if(err) return reject(err);
resolve(result);
});
});
const query2 = new Promise((resolve, reject) => {
dynamoDb.query(Object.assign({}, queryParams, {
KeyConditionExpression: "#epoch = :epoch AND #schedule <= :now",
ExpressionAttributeValues: {
":epoch": epoch,
":now": now
}
}), (err, result) => {
if(err) return reject(err);
resolve(result);
});
});
Promise.all([query1, query2]).then(results => {
const items = results[0].Items.concat(results[1].Items).slice(0, 50);
console.log(`Start processing ${items.length} schedules (${results[0].Items.length} from last epoch)`);
return Promise.all(items.map(item => {
const task = {
item: item.item,
owner: item.owner,
repo: item.repo,
number: item.number
};
const params = {
TableName: `open-bot-${process.env.STAGE}-queue`,
Key: { "item": task.item },
UpdateExpression: "set #owner = :owner, #repo = :repo, #number = :number, #trigger = :timestamp, #schedule = :timestamp",
ExpressionAttributeNames: {
"#owner": "owner",
"#repo": "repo",
"#number": "number",
"#trigger": "trigger",
"#schedule": "schedule"
},
ExpressionAttributeValues: {
":owner": task.owner,
":repo": task.repo,
":number": task.number,
":timestamp": now
},
ReturnValues: "ALL_NEW"
};
return new Promise((resolve, reject) => {
dynamoDb.update(params, (err, result) => {
if(err) return reject(err);
console.log(`Task ${task.item} enqueued with ${Math.round((Date.now() - item.schedule) / 1000)}s delay`);
resolve();
});
}).then(() => new Promise((resolve, reject) => {
dynamoDb.delete({
TableName: `open-bot-${process.env.STAGE}-schedule`,
Key: { "item": item.item },
ConditionExpression: "#schedule = :schedule",
ExpressionAttributeNames: {
"#schedule": "schedule"
},
ExpressionAttributeValues: {
":schedule": item.schedule
},
ReturnValues: "NONE"
}, (err, result) => {
if(err) {
if(err.code === "ConditionalCheckFailedException")
return resolve();
return reject(err);
}
console.log(`Schedule ${item.item} cleaned up with ${Math.round((Date.now() - item.schedule) / 1000)}s delay`);
resolve();
});
}));
})).then(() => items.length);
}).then(numberOfItems => {
callback(null, {
statusCode: 200,
body: JSON.stringify({ message: "processing successfully enqueued", numberOfItems: numberOfItems })
});
}).catch(err => {
console.error(err && err.stack || err);
callback(err);
});
};
================================================
FILE: lib/open-bot-process-scheduled-tasks/package.json
================================================
{
"name": "open-bot-process-scheduled-tasks",
"private": true,
"version": "0.0.0",
"license": "MIT",
"dependencies": {}
}
================================================
FILE: lib/open-bot-process-tasks/handler.js
================================================
const OpenBot = require("../open-bot/open-bot");
const config = require("../../config.json");
config.cache = "/tmp/.cache";
function mergeEvents(events) {
const reducedEvents = [];
const processedEvents = Object.create(null);
events.forEach(event => {
const key = event.item;
if(processedEvents[key]) return;
processedEvents[key] = true;
reducedEvents.push(event);
});
return reducedEvents;
}
module.exports = (event, context, callback) => {
const openBot = new OpenBot(config);
console.log(`Received ${event.Records.length} events`);
const rawEvents = event.Records
.map(record => record.dynamodb.NewImage)
.filter(Boolean)
.map(data => ({
item: data.item.S,
owner: data.owner.S,
repo: data.repo.S,
number: +data.number.N
}));
console.log(`${rawEvents.length} events with items`);
const events = mergeEvents(rawEvents);
console.log(`Process ${events.length} events: `, events.map(e => e.item).join(" "));
Promise.all(events.map(event => {
return openBot.getIssue(event.owner, event.repo, event.number)
.catch(e => {
console.log(`Failed to get issue ${event.item}: ${e && e.message}}`);
})
}))
.then(issues => openBot.process({
workItems: issues.filter(Boolean),
reporter: ({ item, debug, action, change, message, error, stack }) => {
if(debug) return;
if(error) {
console.error(`${item} ${error}`);
console.error(stack);
return;
}
console.log(`${item} ${action} ${change || ""} ${message || ""}`);
},
simulate: config.simulate
}))
.then(result => {
callback(null, result);
}, err => {
console.error(err.stack);
callback(err);
});
};
================================================
FILE: lib/open-bot-process-tasks/package.json
================================================
{
"name": "open-bot-process-issue",
"private": true,
"version": "0.0.0",
"license": "MIT"
}
================================================
FILE: lib/open-bot-scheduler/index.js
================================================
let dynamoDbSingleton;
function getDocumentClient() {
if(!dynamoDbSingleton) {
const AWS = require("aws-sdk");
dynamoDbSingleton = new AWS.DynamoDB.DocumentClient();
}
return dynamoDbSingleton;
}
exports.schedule = function schedule(owner, repo, number, inDuration) {
const dynamoDb = getDocumentClient();
const timestamp = Date.now() + inDuration;
const epoch = Math.floor(timestamp / 36000000); // 1 epoch = 10 hours
const item = `${owner}/${repo}#${number}`;
const params = {
TableName: `open-bot-${process.env.STAGE}-schedule`,
Key: { "item": item },
ConditionExpression: "attribute_not_exists(#schedule) OR #schedule > :timestamp",
UpdateExpression: "set #owner = :owner, #repo = :repo, #number = :number, #schedule = :timestamp, #epoch = :epoch",
ExpressionAttributeNames: {
"#owner": "owner",
"#repo": "repo",
"#number": "number",
"#schedule": "schedule",
"#epoch": "epoch"
},
ExpressionAttributeValues: {
":owner": owner,
":repo": repo,
":number": number,
":timestamp": timestamp,
":epoch": epoch
},
ReturnValues: "ALL_NEW"
};
return new Promise((resolve, reject) => {
dynamoDb.update(params, (err, result) => {
if(err) {
if(err.code === "ConditionalCheckFailedException")
return resolve();
return reject(err);
}
resolve(result);
});
});
}
================================================
FILE: lib/open-bot-scheduler/package.json
================================================
{
"name": "open-bot-scheduler",
"private": true,
"version": "0.0.0",
"license": "MIT",
"dependencies": {}
}
================================================
FILE: lib/package.json
================================================
{
"name": "open-bot-shared",
"private": true,
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"aws-sdk": "^2.95.0"
}
}
================================================
FILE: open-bot.js
================================================
require("./lib/open-bot-cli/open-bot.js");
================================================
FILE: open-bot.yaml
================================================
bot: webpack-bot
rules:
- filters:
commit: true
status:
context: "continuous-integration/travis-ci/pr"
travis_job:
state: "failed"
config:
env: JOB_PART=test
fetch: travis_job.log
string_cleanup:
id: logResult
value: "{{{fetch}}}"
remove:
- "^[\\s\\S]+?\\d+\\s+pending\n+"
- "npm ERR!.*\n"
- "\n*=============================================================================\n[\\s\\S]*"
actions:
comment:
identifier: "ci-result"
message: |-
@{{commit.author.login}} Please review the following output log for errors:
``` text
{{{logResult}}}
```
See [complete report here]({{status.target_url}}).
set:
id: report_ci
value: nope
================================================
FILE: package.json
================================================
{
"name": "open-bot",
"version": "1.0.0",
"description": "",
"main": "lib/open-bot/open-bot.js",
"bin": "lib/open-bot-cli/open-bot.js",
"scripts": {
"sync-dependencies": "node scripts/sync-dependencies",
"install": "node scripts/for-each-package yarn",
"deploy-dev": "serverless deploy --stage dev",
"remove-dev": "serverless remove --stage dev",
"deploy-prod": "serverless deploy --stage production"
},
"author": "Tobias Koppers @sokra",
"license": "MIT",
"dependencies": {
"@octokit/rest": "^18.0.12",
"clone": "^2.1.1",
"duration-js": "^3.9.2",
"handlebars": "^4.5.3",
"js-yaml": "^3.8.2",
"minimist": "^1.2.5",
"mkdirp": "^0.5.1",
"promise-queue": "^2.2.3",
"prompt": "^1.0.0",
"request": "^2.88.2",
"serverless": "^1.9.0",
"serverless-offline": "^3.13.1",
"sync-exec": "^0.6.2"
}
}
================================================
FILE: scripts/for-each-package.js
================================================
const fs = require("fs");
const path = require("path");
const exec = require("sync-exec");
const packages = fs.readdirSync(path.resolve(__dirname, "../lib"));
packages.forEach(p => {
const fullPath = path.resolve(__dirname, "../lib/" + p);
const cmd = process.argv.slice(2).join(" ");
console.log(p, cmd);
const output = exec(cmd, { cwd: fullPath });
console.log(output.stdout);
});
================================================
FILE: scripts/sync-dependencies.js
================================================
const fs = require("fs");
const path = require("path");
const libPackage = path.resolve(__dirname, "../lib/open-bot/package.json");
const cliPackage = path.resolve(__dirname, "../lib/open-bot-cli/package.json");
const mainPackage = path.resolve(__dirname, "../package.json");
const lib = JSON.parse(fs.readFileSync(libPackage, "utf-8"));
const cli = JSON.parse(fs.readFileSync(cliPackage, "utf-8"));
const main = JSON.parse(fs.readFileSync(mainPackage, "utf-8"));
const addDependency = (name, value) => {
main.dependencies[name] = value;
}
Object.keys(cli.dependencies).forEach(name => addDependency(name, cli.dependencies[name]));
Object.keys(lib.dependencies).forEach(name => addDependency(name, lib.dependencies[name]));
main.dependencies = Object.keys(main.dependencies).sort().reduce((o, name) => {
o[name] = main.dependencies[name];
return o;
}, {});
fs.writeFileSync(mainPackage, JSON.stringify(main, 0, 2), "utf-8");
================================================
FILE: serverless.yml
================================================
# set environment variable AWS_CLIENT_TIMEOUT to increase timeout
service: open-bot-v1
provider:
name: aws
runtime: nodejs12.x
memorySize: 256
stage: dev
region: us-east-1
logRetentionInDays: 14
iamRoleStatements:
- Effect: "Allow"
Resource: "*"
Action:
- "sns:*"
- Effect: "Allow"
Action:
- "*"
Resource: "arn:aws:dynamodb:us-east-1:*:table/open-bot-${opt:stage}-queue"
- Effect: "Allow"
Action:
- "*"
Resource:
- "arn:aws:dynamodb:us-east-1:*:table/open-bot-${opt:stage}-schedule"
- "arn:aws:dynamodb:us-east-1:*:table/open-bot-${opt:stage}-schedule/index/*"
plugins:
- serverless-offline
package:
individually: true
exclude:
- '**'
include:
- lib/node_modules/**
- handlers.js
functions:
processTasks:
handler: handlers.processTasks
timeout: 120
package:
include:
- lib/open-bot-process-tasks/**
- lib/open-bot/**
- lib/open-bot-scheduler/**
- config.json
environment:
STAGE: "${opt:stage}"
events:
- stream:
type: dynamodb
batchSize: 20
startingPosition: TRIM_HORIZON
arn:
Fn::GetAtt:
- QueueDynamoDbTable
- StreamArn
handleGithubEvent:
handler: handlers.handleGithubEvent
package:
include:
- lib/open-bot-handle-github-event/**
- lib/open-bot/**
- config.json
environment:
STAGE: "${opt:stage}"
events:
- http:
path: github
method: POST
timeout: 20
processScheduledTasks:
handler: handlers.processScheduledTasks
timeout: 300
package:
include:
- lib/open-bot-process-scheduled-tasks/**
- config.json
environment:
STAGE: "${opt:stage}"
events:
- schedule: rate(10 minutes)
resources:
Resources:
QueueDynamoDbTable:
Type: "AWS::DynamoDB::Table"
DeletionPolicy: Delete
Properties:
AttributeDefinitions:
-
AttributeName: item
AttributeType: S
KeySchema:
-
AttributeName: item
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
StreamSpecification:
StreamViewType: NEW_IMAGE
TableName: "open-bot-${opt:stage}-queue"
ScheduleDynamoDbTable:
Type: "AWS::DynamoDB::Table"
DeletionPolicy: Delete
Properties:
AttributeDefinitions:
-
AttributeName: item
AttributeType: S
-
AttributeName: epoch
AttributeType: N
-
AttributeName: schedule
AttributeType: N
KeySchema:
-
AttributeName: item
KeyType: HASH
GlobalSecondaryIndexes:
-
IndexName: schedule-index
KeySchema:
-
AttributeName: epoch
KeyType: HASH
-
AttributeName: schedule
KeyType: RANGE
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
Projection:
ProjectionType: ALL
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: "open-bot-${opt:stage}-schedule"
gitextract_29rk1j_5/ ├── .gitignore ├── README.md ├── doc/ │ └── rules.md ├── handlers.js ├── lib/ │ ├── open-bot/ │ │ ├── Config.js │ │ ├── actions/ │ │ │ ├── close.js │ │ │ ├── comment.js │ │ │ ├── index.js │ │ │ ├── label.js │ │ │ ├── merge.js │ │ │ ├── new_issue.js │ │ │ ├── reopen.js │ │ │ ├── schedule.js │ │ │ ├── set.js │ │ │ └── status.js │ │ ├── api-utils.js │ │ ├── filters/ │ │ │ ├── IssuePullRequestBaseFilter.js │ │ │ ├── TimeFilter.js │ │ │ ├── age.js │ │ │ ├── all.js │ │ │ ├── any.js │ │ │ ├── check.js │ │ │ ├── comment.js │ │ │ ├── commit.js │ │ │ ├── ensure.js │ │ │ ├── fetch.js │ │ │ ├── in_order.js │ │ │ ├── index.js │ │ │ ├── issue.js │ │ │ ├── label.js │ │ │ ├── last_action_age.js │ │ │ ├── match.js │ │ │ ├── not.js │ │ │ ├── number_of_comments.js │ │ │ ├── open.js │ │ │ ├── permission.js │ │ │ ├── pull_request.js │ │ │ ├── review.js │ │ │ ├── status.js │ │ │ ├── string_cleanup.js │ │ │ ├── threshold.js │ │ │ └── travis_job.js │ │ ├── github.js │ │ ├── helpers/ │ │ │ ├── findCount.js │ │ │ ├── findLast.js │ │ │ ├── findLastIndex.js │ │ │ ├── getDate.js │ │ │ ├── interpolate.js │ │ │ ├── matchRange.js │ │ │ ├── once.js │ │ │ ├── parseDate.js │ │ │ └── seq.js │ │ ├── open-bot.js │ │ ├── package.json │ │ └── travis.js │ ├── open-bot-cli/ │ │ ├── open-bot.js │ │ └── package.json │ ├── open-bot-handle-github-event/ │ │ ├── handler.js │ │ └── package.json │ ├── open-bot-process-scheduled-tasks/ │ │ ├── handler.js │ │ └── package.json │ ├── open-bot-process-tasks/ │ │ ├── handler.js │ │ └── package.json │ ├── open-bot-scheduler/ │ │ ├── index.js │ │ └── package.json │ └── package.json ├── open-bot.js ├── open-bot.yaml ├── package.json ├── scripts/ │ ├── for-each-package.js │ └── sync-dependencies.js └── serverless.yml
SYMBOL INDEX (196 symbols across 44 files)
FILE: lib/open-bot-cli/open-bot.js
function helpCommand (line 32) | function helpCommand() {
function displayError (line 48) | function displayError(err) {
function configurateCommand (line 54) | function configurateCommand() {
function scheduleCommand (line 104) | function scheduleCommand() {
function processCommand (line 154) | function processCommand() {
function createReporter (line 229) | function createReporter(showDebug) {
function loadConfig (line 296) | function loadConfig() {
FILE: lib/open-bot-handle-github-event/handler.js
constant AWS (line 1) | const AWS = require("aws-sdk");
function publish (line 37) | function publish(number) {
FILE: lib/open-bot-process-scheduled-tasks/handler.js
constant AWS (line 1) | const AWS = require("aws-sdk");
FILE: lib/open-bot-process-tasks/handler.js
function mergeEvents (line 5) | function mergeEvents(events) {
FILE: lib/open-bot-scheduler/index.js
function getDocumentClient (line 3) | function getDocumentClient() {
FILE: lib/open-bot/Config.js
class Rule (line 5) | class Rule {
method constructor (line 6) | constructor(idx, filters, actions) {
method formatResult (line 12) | static formatResult(result) {
method run (line 16) | run(context, issue) {
method runActions (line 45) | runActions(context, issue, lastFilterImpuls) {
class Config (line 72) | class Config {
method constructor (line 73) | constructor(configJson) {
method parseRule (line 78) | static parseRule(ruleJson, idx) {
method parseItems (line 84) | static parseItems(data, types) {
method parseItem (line 91) | static parseItem(data, type, types, key) {
method run (line 98) | run(context, issue) {
FILE: lib/open-bot/actions/close.js
class CloseAction (line 3) | class CloseAction {
method constructor (line 4) | constructor(options) { }
method findLast (line 6) | findLast({ botUsername }, { state, timeline }) {
method run (line 16) | run({ owner, repo, item, github, reporter }, { number }) {
FILE: lib/open-bot/actions/comment.js
class CommentAction (line 5) | class CommentAction {
method constructor (line 6) | constructor(options) {
method findLast (line 25) | findLast(context, issue) {
method run (line 47) | run(context, issue) {
FILE: lib/open-bot/actions/label.js
class LabelAction (line 3) | class LabelAction {
method constructor (line 4) | constructor(options) {
method findLast (line 19) | findLast({ botUsername }, { timeline, labels }) {
method run (line 39) | run({ owner, repo, item, github, reporter }, { number, labels }) {
FILE: lib/open-bot/actions/merge.js
class MergeAction (line 3) | class MergeAction {
method constructor (line 4) | constructor(options) {
method findLast (line 16) | findLast({ botUsername }, { pull_request_info }) {
method run (line 30) | run(context, issue) {
FILE: lib/open-bot/actions/new_issue.js
class NewIssueAction (line 3) | class NewIssueAction {
method constructor (line 4) | constructor(options) {
method findLast (line 12) | findLast() {
method run (line 16) | run(context, issue) {
FILE: lib/open-bot/actions/reopen.js
class ReopenAction (line 3) | class ReopenAction {
method constructor (line 4) | constructor(options) { }
method findLast (line 6) | findLast({ botUsername }, { state, timeline }) {
method run (line 15) | run({ owner, repo, item, github, reporter }, { number }) {
FILE: lib/open-bot/actions/schedule.js
class ScheduleAction (line 5) | class ScheduleAction {
method constructor (line 6) | constructor(options) {
method findLast (line 13) | findLast() {
method run (line 17) | run(context, issue) {
FILE: lib/open-bot/actions/set.js
class SetAction (line 3) | class SetAction {
method constructor (line 4) | constructor(options) {
method findLast (line 9) | findLast() {
method run (line 13) | run(context, issue) {
FILE: lib/open-bot/actions/status.js
class StatusAction (line 4) | class StatusAction {
method constructor (line 5) | constructor(options, key) {
method findLast (line 12) | findLast(context, issue) {
method run (line 28) | run(context, issue) {
FILE: lib/open-bot/api-utils.js
constant USER_AGENT (line 8) | const USER_AGENT = "github.com/open-bot/open-bot";
class ApiUtils (line 10) | class ApiUtils {
method constructor (line 11) | constructor({ cache }) {
method prepareCache (line 17) | prepareCache(name, cacheDirectory) {
method fetch (line 33) | fetch(name, api, data) {
method fetchAll (line 39) | fetchAll(name, api, data) {
method cached (line 45) | cached(api, cacheDirectory) {
method _fetchAll (line 108) | _fetchAll(api, data) {
method _fetch (line 133) | _fetch(api, data) {
method _parseLinks (line 142) | _parseLinks(link) {
FILE: lib/open-bot/filters/IssuePullRequestBaseFilter.js
class IssuePullRequestBaseFilter (line 3) | class IssuePullRequestBaseFilter {
method constructor (line 4) | constructor(options) {
method findLast (line 23) | findLast({ botUsername }, { created_at, body, title, locked, user: { l...
FILE: lib/open-bot/filters/TimeFilter.js
class TimeFilter (line 3) | class TimeFilter {
method constructor (line 4) | constructor(options) {
method getDate (line 31) | getDate(context, issue) {
method findLast (line 35) | findLast(context, issue) {
FILE: lib/open-bot/filters/age.js
class AgeFilter (line 4) | class AgeFilter extends TimeFilter {
method constructor (line 5) | constructor(options) {
method getDate (line 14) | getDate(context, issue) {
FILE: lib/open-bot/filters/all.js
class AllFilter (line 3) | class AllFilter {
method constructor (line 4) | constructor(options) {
method findLast (line 10) | findLast(context, issue) {
FILE: lib/open-bot/filters/any.js
class AnyFilter (line 3) | class AnyFilter {
method constructor (line 4) | constructor(options) {
method findLast (line 10) | findLast(context, issue) {
FILE: lib/open-bot/filters/check.js
class CheckFilter (line 3) | class CheckFilter {
method constructor (line 4) | constructor(options, key) {
method findLast (line 21) | findLast({ botUsername, data }, { pull_request_checks }) {
FILE: lib/open-bot/filters/comment.js
class CommentFilter (line 3) | class CommentFilter {
method constructor (line 4) | constructor(options, key) {
method findLast (line 23) | findLast({ data, botUsername }, { timeline }) {
FILE: lib/open-bot/filters/commit.js
class CommitFilter (line 3) | class CommitFilter {
method constructor (line 4) | constructor(options, key) {
method findLast (line 26) | findLast({ data, botUsername }, { pull_request_commits }) {
FILE: lib/open-bot/filters/ensure.js
class EnsureFilter (line 5) | class EnsureFilter {
method constructor (line 6) | constructor(options) {
method findLast (line 25) | findLast(context, issue) {
FILE: lib/open-bot/filters/fetch.js
class FetchFilter (line 4) | class FetchFilter {
method constructor (line 5) | constructor(options, key) {
method findLast (line 13) | findLast({ data }) {
FILE: lib/open-bot/filters/in_order.js
class InOrderFilter (line 3) | class InOrderFilter {
method constructor (line 4) | constructor(options) {
method findLast (line 10) | findLast(context, issue) {
FILE: lib/open-bot/filters/issue.js
class IssueFilter (line 4) | class IssueFilter extends IssuePullRequestBaseFilter {
method constructor (line 5) | constructor(options) {
method findLast (line 9) | findLast(context, issue) {
FILE: lib/open-bot/filters/label.js
class LabelFilter (line 4) | class LabelFilter {
method constructor (line 5) | constructor(options, key) {
method findLast (line 20) | findLast({ data, botUsername }, { created_at, labels, timeline }) {
FILE: lib/open-bot/filters/last_action_age.js
constant USER_ACTIONS (line 4) | const USER_ACTIONS = [
class LastActionAgeFilter (line 20) | class LastActionAgeFilter extends TimeFilter {
method constructor (line 21) | constructor(options) {
method getDate (line 28) | getDate({ botUsername }, { created_at, timeline }) {
FILE: lib/open-bot/filters/match.js
class MatchFilter (line 3) | class MatchFilter {
method constructor (line 4) | constructor(options, key) {
method findLast (line 10) | findLast(context, issue) {
FILE: lib/open-bot/filters/not.js
class NotFilter (line 3) | class NotFilter {
method constructor (line 4) | constructor(options) {
method findLast (line 10) | findLast(context, issue) {
FILE: lib/open-bot/filters/number_of_comments.js
class NumberOfCommentsFilter (line 4) | class NumberOfCommentsFilter {
method constructor (line 5) | constructor(options) {
method findLast (line 18) | findLast({ }, { timeline, created_at }) {
FILE: lib/open-bot/filters/open.js
class OpenFilter (line 4) | class OpenFilter {
method constructor (line 5) | constructor(options) {
method findLast (line 9) | findLast({ botUsername }, { created_at, timeline, state }) {
FILE: lib/open-bot/filters/permission.js
class PermissionFilter (line 3) | class PermissionFilter {
method constructor (line 4) | constructor(options, key) {
method findLast (line 18) | findLast(context, issue) {
FILE: lib/open-bot/filters/pull_request.js
class PullRequestFilter (line 5) | class PullRequestFilter extends IssuePullRequestBaseFilter {
method constructor (line 6) | constructor(options, key) {
method findLast (line 43) | findLast(context, issue) {
FILE: lib/open-bot/filters/review.js
class ReviewFilter (line 3) | class ReviewFilter {
method constructor (line 4) | constructor(options, key) {
method findLast (line 29) | findLast({ data, botUsername }, { pull_request_info, pull_request_revi...
FILE: lib/open-bot/filters/status.js
class StatusFilter (line 3) | class StatusFilter {
method constructor (line 4) | constructor(options, key) {
method findLast (line 21) | findLast({ botUsername, data }, { pull_request_statuses }) {
FILE: lib/open-bot/filters/string_cleanup.js
class StringCleanupFilter (line 3) | class StringCleanupFilter {
method constructor (line 4) | constructor(options, key) {
method findLast (line 13) | findLast(context, issue) {
FILE: lib/open-bot/filters/threshold.js
class ThresholdFilter (line 5) | class ThresholdFilter {
method constructor (line 6) | constructor(options) {
method findLast (line 16) | findLast(context, issue) {
FILE: lib/open-bot/filters/travis_job.js
class TravisJobFilter (line 4) | class TravisJobFilter {
method constructor (line 5) | constructor(options, key) {
method findLast (line 22) | findLast({ data, botUsername }, { travis_jobs }) {
FILE: lib/open-bot/github.js
constant USER_AGENT (line 6) | const USER_AGENT = "github.com/open-bot/open-bot";
class Github (line 8) | class Github {
method constructor (line 9) | constructor({ token, cache, simulate }) {
method getReposOfUser (line 21) | getReposOfUser(username) {
method getReposOfOrg (line 27) | getReposOfOrg(org) {
method getRepo (line 33) | getRepo(owner, repo) {
method getIssuesForRepo (line 40) | getIssuesForRepo(owner, repo, { state = "all", since, labels } = {}) {
method getPullRequestForCommit (line 51) | getPullRequestForCommit(owner, repo, sha) {
method getIssue (line 60) | getIssue(owner, repo, issue_number) {
method getEventsForIssue (line 68) | getEventsForIssue(owner, repo, issue_number) {
method getCommentsForIssue (line 76) | getCommentsForIssue(owner, repo, issue_number) {
method getPermissionForRepo (line 84) | getPermissionForRepo(owner, repo, username) {
method getPullRequest (line 92) | getPullRequest(owner, repo, pull_number) {
method getPullRequest (line 100) | getPullRequest(owner, repo, pull_number) {
method getCommitsForPullRequest (line 108) | getCommitsForPullRequest(owner, repo, pull_number) {
method getFilesForPullRequest (line 116) | getFilesForPullRequest(owner, repo, pull_number) {
method getReviewsForPullRequest (line 124) | getReviewsForPullRequest(owner, repo, pull_number) {
method getReviewRequestsForPullRequest (line 132) | getReviewRequestsForPullRequest(owner, repo, pull_number) {
method getStatuses (line 140) | getStatuses(owner, repo, ref) {
method getChecks (line 148) | getChecks(owner, repo, ref) {
method getBlob (line 156) | getBlob(owner, repo, pathInRepo) {
method editIssue (line 164) | editIssue(owner, repo, issue_number, update) {
method addLabels (line 172) | addLabels(owner, repo, issue_number, labels) {
method removeLabels (line 181) | removeLabels(owner, repo, issue_number, labels) {
method createIssue (line 190) | createIssue(owner, repo, title, body) {
method createComment (line 199) | createComment(owner, repo, issue_number, body) {
method editComment (line 208) | editComment(owner, repo, comment_id, body) {
method deleteComment (line 217) | deleteComment(owner, repo, comment_id) {
method createStatus (line 225) | createStatus(owner, repo, sha, context, state, { target_url, descripti...
method merge (line 237) | merge(owner, repo, pull_number, commit_title, commit_message, sha, mer...
method filterUndefined (line 249) | filterUndefined(obj) {
method _action (line 257) | _action(api, data) {
method _fetch (line 262) | _fetch(name, api, data) {
FILE: lib/open-bot/open-bot.js
function queueAll (line 8) | function queueAll(queue, array, fn) {
class OpenBot (line 12) | class OpenBot {
method constructor (line 13) | constructor(config) {
method getRepo (line 23) | getRepo(owner, repo) {
method getIssue (line 27) | getIssue(owner, repo, number) {
method getReposOfOrg (line 40) | getReposOfOrg(org) {
method getIssuesForRepo (line 44) | getIssuesForRepo(org, repo, filter) {
method getConfig (line 48) | getConfig(owner, repo) {
method process (line 68) | process({ workItems, reporter = () => {}, simulate = false, issueFilte...
method processIssue (line 175) | processIssue({ owner, repo, number, reporter = () => {}, simulate = fa...
method processIssueWithData (line 197) | processIssueWithData({ config, owner, repo, issue, reporter = () => {}...
FILE: lib/open-bot/travis.js
class Travis (line 39) | class Travis {
method constructor (line 40) | constructor({ simulate, cache }) {
method getBuild (line 45) | getBuild(id) {
method getRawLog (line 49) | getRawLog(id) {
method getLog (line 53) | getLog(id) {
method _fetch (line 64) | _fetch(name, api, data) {
Condensed preview — 72 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (120K chars).
[
{
"path": ".gitignore",
"chars": 43,
"preview": "node_modules\nconfig.json\n.serverless\n.cache"
},
{
"path": "README.md",
"chars": 1799,
"preview": "# open-bot\n\nAn unopinionated bot driven by a configuration file in the repository.\n\n* Operates on issue/PRs\n* Triggered "
},
{
"path": "doc/rules.md",
"chars": 11043,
"preview": "# Rules\n\nA rule is a pair of `filters` and `actions`.\n\nExample (complete configuration file):\n\n``` yaml\nbot: my-bot\nrule"
},
{
"path": "handlers.js",
"chars": 316,
"preview": "const handlers = [\n\t\"process-tasks\",\n\t\"handle-github-event\",\n\t\"process-scheduled-tasks\"\n];\n\nhandlers.forEach(name => {\n\t"
},
{
"path": "lib/open-bot/Config.js",
"chars": 2837,
"preview": "const actionsList = require(\"./actions\");\nconst filtersList = require(\"./filters\");\nconst seq = require(\"./helpers/seq\")"
},
{
"path": "lib/open-bot/actions/close.js",
"chars": 568,
"preview": "const findLast = require(\"../helpers/findLast\");\n\nclass CloseAction {\n\tconstructor(options) { }\n\n\tfindLast({ botUsername"
},
{
"path": "lib/open-bot/actions/comment.js",
"chars": 2733,
"preview": "const findLast = require(\"../helpers/findLast\");\nconst findLastIndex = require(\"../helpers/findLastIndex\");\nconst interp"
},
{
"path": "lib/open-bot/actions/index.js",
"chars": 391,
"preview": "exports = module.exports = Object.create(null);\n\nexports.close = require(\"./close\");\nexports.reopen = require(\"./reopen\""
},
{
"path": "lib/open-bot/actions/label.js",
"chars": 1834,
"preview": "const findLast = require(\"../helpers/findLast\");\n\nclass LabelAction {\n\tconstructor(options) {\n\t\tthis.add = undefined;\n\t\t"
},
{
"path": "lib/open-bot/actions/merge.js",
"chars": 1651,
"preview": "const interpolate = require(\"../helpers/interpolate\");\n\nclass MergeAction {\n\tconstructor(options) {\n\t\tthis.commit_title "
},
{
"path": "lib/open-bot/actions/new_issue.js",
"chars": 922,
"preview": "const interpolate = require(\"../helpers/interpolate\");\n\nclass NewIssueAction {\n\tconstructor(options) {\n\t\tthis.target = o"
},
{
"path": "lib/open-bot/actions/reopen.js",
"chars": 550,
"preview": "const findLast = require(\"../helpers/findLast\");\n\nclass ReopenAction {\n\tconstructor(options) { }\n\n\tfindLast({ botUsernam"
},
{
"path": "lib/open-bot/actions/schedule.js",
"chars": 757,
"preview": "const schedule = require(\"../../open-bot-scheduler\").schedule;\nconst Duration = require(\"duration-js\");\nconst interpolat"
},
{
"path": "lib/open-bot/actions/set.js",
"chars": 473,
"preview": "const interpolate = require(\"../helpers/interpolate\");\n\nclass SetAction {\n\tconstructor(options) {\n\t\tthis.id = options.id"
},
{
"path": "lib/open-bot/actions/status.js",
"chars": 1584,
"preview": "const interpolate = require(\"../helpers/interpolate\");\nconst findLast = require(\"../helpers/findLast\");\n\nclass StatusAct"
},
{
"path": "lib/open-bot/api-utils.js",
"chars": 4010,
"preview": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst mkdirp = require(\"mkdirp\");\nconst crypto = require(\"crypto"
},
{
"path": "lib/open-bot/filters/IssuePullRequestBaseFilter.js",
"chars": 1249,
"preview": "const parseDate = require(\"../helpers/parseDate\");\n\nclass IssuePullRequestBaseFilter {\n\tconstructor(options) {\n\t\tthis.ma"
},
{
"path": "lib/open-bot/filters/TimeFilter.js",
"chars": 1492,
"preview": "const Duration = require(\"duration-js\");\n\nclass TimeFilter {\n\tconstructor(options) {\n\t\tthis.minimum = undefined;\n\t\tthis."
},
{
"path": "lib/open-bot/filters/age.js",
"chars": 461,
"preview": "const TimeFilter = require(\"./TimeFilter\");\nconst interpolate = require(\"../helpers/interpolate\");\n\nclass AgeFilter exte"
},
{
"path": "lib/open-bot/filters/all.js",
"chars": 541,
"preview": "const seq = require(\"../helpers/seq\");\n\nclass AllFilter {\n\tconstructor(options) {\n\t\tconst filtersList = require(\"../filt"
},
{
"path": "lib/open-bot/filters/any.js",
"chars": 569,
"preview": "const seq = require(\"../helpers/seq\");\n\nclass AnyFilter {\n\tconstructor(options) {\n\t\tconst filtersList = require(\"../filt"
},
{
"path": "lib/open-bot/filters/check.js",
"chars": 1300,
"preview": "const findLast = require(\"../helpers/findLast\");\n\nclass CheckFilter {\n\tconstructor(options, key) {\n\t\tthis.id = options.i"
},
{
"path": "lib/open-bot/filters/comment.js",
"chars": 1186,
"preview": "const findLast = require(\"../helpers/findLast\");\n\nclass CommentFilter {\n\tconstructor(options, key) {\n\t\tthis.id = options"
},
{
"path": "lib/open-bot/filters/commit.js",
"chars": 1445,
"preview": "const findLast = require(\"../helpers/findLast\");\n\nclass CommitFilter {\n\tconstructor(options, key) {\n\t\tthis.id = options."
},
{
"path": "lib/open-bot/filters/ensure.js",
"chars": 1590,
"preview": "const interpolate = require(\"../helpers/interpolate\");\nconst findLast = require(\"../helpers/findLast\");\nconst matchRange"
},
{
"path": "lib/open-bot/filters/fetch.js",
"chars": 651,
"preview": "const findLast = require(\"../helpers/findLast\");\nconst matchRange = require(\"../helpers/matchRange\");\n\nclass FetchFilter"
},
{
"path": "lib/open-bot/filters/in_order.js",
"chars": 696,
"preview": "const seq = require(\"../helpers/seq\");\n\nclass InOrderFilter {\n\tconstructor(options) {\n\t\tconst filtersList = require(\"../"
},
{
"path": "lib/open-bot/filters/index.js",
"chars": 983,
"preview": "exports = module.exports = Object.create(null);\n\nexports.issue = require(\"./issue\");\nexports.pull_request = require(\"./p"
},
{
"path": "lib/open-bot/filters/issue.js",
"chars": 447,
"preview": "const parseDate = require(\"../helpers/parseDate\");\nconst IssuePullRequestBaseFilter = require(\"./IssuePullRequestBaseFil"
},
{
"path": "lib/open-bot/filters/label.js",
"chars": 1586,
"preview": "const findLast = require(\"../helpers/findLast\");\nconst parseDate = require(\"../helpers/parseDate\");\n\nclass LabelFilter {"
},
{
"path": "lib/open-bot/filters/last_action_age.js",
"chars": 935,
"preview": "const findLast = require(\"../helpers/findLast\");\nconst TimeFilter = require(\"./TimeFilter\");\n\nconst USER_ACTIONS = [\n\t\"a"
},
{
"path": "lib/open-bot/filters/match.js",
"chars": 481,
"preview": "const interpolate = require(\"../helpers/interpolate\");\n\nclass MatchFilter {\n\tconstructor(options, key) {\n\t\tthis.id = opt"
},
{
"path": "lib/open-bot/filters/not.js",
"chars": 543,
"preview": "const seq = require(\"../helpers/seq\");\n\nclass NotFilter {\n\tconstructor(options) {\n\t\tconst filtersList = require(\"../filt"
},
{
"path": "lib/open-bot/filters/number_of_comments.js",
"chars": 707,
"preview": "const findCount = require(\"../helpers/findCount\");\nconst parseDate = require(\"../helpers/parseDate\");\n\nclass NumberOfCom"
},
{
"path": "lib/open-bot/filters/open.js",
"chars": 679,
"preview": "const findLast = require(\"../helpers/findLast\");\nconst parseDate = require(\"../helpers/parseDate\");\n\nclass OpenFilter {\n"
},
{
"path": "lib/open-bot/filters/permission.js",
"chars": 839,
"preview": "const interpolate = require(\"../helpers/interpolate\");\n\nclass PermissionFilter {\n\tconstructor(options, key) {\n\t\tthis.id "
},
{
"path": "lib/open-bot/filters/pull_request.js",
"chars": 3062,
"preview": "const parseDate = require(\"../helpers/parseDate\");\nconst IssuePullRequestBaseFilter = require(\"./IssuePullRequestBaseFil"
},
{
"path": "lib/open-bot/filters/review.js",
"chars": 1613,
"preview": "const findLast = require(\"../helpers/findLast\");\n\nclass ReviewFilter {\n\tconstructor(options, key) {\n\t\tthis.id = options."
},
{
"path": "lib/open-bot/filters/status.js",
"chars": 1292,
"preview": "const findLast = require(\"../helpers/findLast\");\n\nclass StatusFilter {\n\tconstructor(options, key) {\n\t\tthis.id = options."
},
{
"path": "lib/open-bot/filters/string_cleanup.js",
"chars": 614,
"preview": "const interpolate = require(\"../helpers/interpolate\");\n\nclass StringCleanupFilter {\n\tconstructor(options, key) {\n\t\tthis."
},
{
"path": "lib/open-bot/filters/threshold.js",
"chars": 983,
"preview": "const seq = require(\"../helpers/seq\");\nconst parseDate = require(\"../helpers/parseDate\");\nconst findCount = require(\"../"
},
{
"path": "lib/open-bot/filters/travis_job.js",
"chars": 1317,
"preview": "const findLast = require(\"../helpers/findLast\");\nconst matchRange = require(\"../helpers/matchRange\");\n\nclass TravisJobFi"
},
{
"path": "lib/open-bot/github.js",
"chars": 5513,
"preview": "const { Octokit } = require(\"@octokit/rest\");\nconst clone = require(\"clone\");\nconst seq = require(\"./helpers/seq\");\ncons"
},
{
"path": "lib/open-bot/helpers/findCount.js",
"chars": 620,
"preview": "const getDate = require(\"./getDate\");\n\nmodule.exports = function findCount(array, min, max, startDate, fn) {\n\tif(typeof "
},
{
"path": "lib/open-bot/helpers/findLast.js",
"chars": 269,
"preview": "const findLastIndex = require(\"./findLastIndex\");\nconst getDate = require(\"./getDate\");\n\nmodule.exports = function findL"
},
{
"path": "lib/open-bot/helpers/findLastIndex.js",
"chars": 157,
"preview": "module.exports = function findLastIndex(array, fn) {\n\tfor(let i = array.length - 1; i >= 0; i--) {\n\t\tif(fn(array[i], i, "
},
{
"path": "lib/open-bot/helpers/getDate.js",
"chars": 287,
"preview": "const parseDate = require(\"./parseDate\");\n\nmodule.exports = item => {\n\tif(typeof item === \"number\") return item;\n\treturn"
},
{
"path": "lib/open-bot/helpers/interpolate.js",
"chars": 740,
"preview": "const handlebars = require(\"handlebars\");\n\nhandlebars.registerHelper(\"quote\", function(value) {\n\treturn new handlebars.S"
},
{
"path": "lib/open-bot/helpers/matchRange.js",
"chars": 1132,
"preview": "module.exports = function matchRange(range, value) {\n\tif(typeof value !== \"number\")\n\t\tvalue = +value;\n\tif(isNaN(value)) "
},
{
"path": "lib/open-bot/helpers/once.js",
"chars": 133,
"preview": "module.exports = function once(factory) {\n\tlet result;\n\treturn () => {\n\t\tif(result) return result;\n\t\treturn result = fac"
},
{
"path": "lib/open-bot/helpers/parseDate.js",
"chars": 62,
"preview": "module.exports = date => {\n\treturn new Date(date).getTime();\n}"
},
{
"path": "lib/open-bot/helpers/seq.js",
"chars": 375,
"preview": "module.exports = function seq(arr, fn, breakCondition) {\n\tlet breaked = false;\n\tlet i = 0;\n\treturn arr.reduce(\n\t\t(p, ite"
},
{
"path": "lib/open-bot/open-bot.js",
"chars": 8418,
"preview": "const Github = require(\"./github\");\nconst Travis = require(\"./travis\");\nconst Config = require(\"./Config\");\nconst Queue "
},
{
"path": "lib/open-bot/package.json",
"chars": 329,
"preview": "{\n \"name\": \"open-bot\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@octokit/r"
},
{
"path": "lib/open-bot/travis.js",
"chars": 1882,
"preview": "const Queue = require(\"promise-queue\");\nconst ApiUtils = require(\"./api-utils\");\nconst request = require(\"request\");\n\nco"
},
{
"path": "lib/open-bot-cli/open-bot.js",
"chars": 8676,
"preview": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst prompt = require(\"prompt\");\nconst argv = require(\"minimist"
},
{
"path": "lib/open-bot-cli/package.json",
"chars": 164,
"preview": "{\n \"name\": \"open-bot-cli\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"minimi"
},
{
"path": "lib/open-bot-handle-github-event/handler.js",
"chars": 2539,
"preview": "const AWS = require(\"aws-sdk\");\nconst config = require(\"../../config.json\");\nconfig.cache = \"/tmp/.cache\";\n\nconst dynamo"
},
{
"path": "lib/open-bot-handle-github-event/package.json",
"chars": 128,
"preview": "{\n \"name\": \"open-bot-handle-github-event\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"license\": \"MIT\",\n \"dependencies"
},
{
"path": "lib/open-bot-process-scheduled-tasks/handler.js",
"chars": 3427,
"preview": "const AWS = require(\"aws-sdk\");\n\nconst dynamoDb = new AWS.DynamoDB.DocumentClient();\n\nmodule.exports = (event, context, "
},
{
"path": "lib/open-bot-process-scheduled-tasks/package.json",
"chars": 132,
"preview": "{\n \"name\": \"open-bot-process-scheduled-tasks\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"license\": \"MIT\",\n \"dependen"
},
{
"path": "lib/open-bot-process-tasks/handler.js",
"chars": 1652,
"preview": "const OpenBot = require(\"../open-bot/open-bot\");\nconst config = require(\"../../config.json\");\nconfig.cache = \"/tmp/.cach"
},
{
"path": "lib/open-bot-process-tasks/package.json",
"chars": 100,
"preview": "{\n \"name\": \"open-bot-process-issue\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"license\": \"MIT\"\n}\n"
},
{
"path": "lib/open-bot-scheduler/index.js",
"chars": 1344,
"preview": "let dynamoDbSingleton;\n\nfunction getDocumentClient() {\n\tif(!dynamoDbSingleton) {\n\t\tconst AWS = require(\"aws-sdk\");\n\n\t\tdy"
},
{
"path": "lib/open-bot-scheduler/package.json",
"chars": 118,
"preview": "{\n \"name\": \"open-bot-scheduler\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"license\": \"MIT\",\n \"dependencies\": {}\n}\n"
},
{
"path": "lib/package.json",
"chars": 143,
"preview": "{\n \"name\": \"open-bot-shared\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"aws"
},
{
"path": "open-bot.js",
"chars": 43,
"preview": "require(\"./lib/open-bot-cli/open-bot.js\");\n"
},
{
"path": "open-bot.yaml",
"chars": 791,
"preview": "bot: webpack-bot\nrules:\n- filters:\n commit: true\n status:\n context: \"continuous-integration/travis-ci/pr\"\n "
},
{
"path": "package.json",
"chars": 880,
"preview": "{\n \"name\": \"open-bot\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"lib/open-bot/open-bot.js\",\n \"bin\": \"lib/o"
},
{
"path": "scripts/for-each-package.js",
"chars": 390,
"preview": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst exec = require(\"sync-exec\");\n\nconst packages = fs.readdirS"
},
{
"path": "scripts/sync-dependencies.js",
"chars": 934,
"preview": "const fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst libPackage = path.resolve(__dirname, \"../lib/open-bot/pac"
},
{
"path": "serverless.yml",
"chars": 3468,
"preview": "# set environment variable AWS_CLIENT_TIMEOUT to increase timeout\n\nservice: open-bot-v1\n\nprovider:\n name: aws\n runtime"
}
]
About this extraction
This page contains the full source code of the open-bot/open-bot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 72 files (101.2 KB), approximately 29.1k tokens, and a symbol index with 196 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.