Skip to content
This repository has been archived by the owner on Mar 26, 2024. It is now read-only.

Commit

Permalink
Merge pull request #25 from cjsaylor/slack-support
Browse files Browse the repository at this point in the history
Slack support
  • Loading branch information
cjsaylor authored Jul 30, 2018
2 parents 15f6e47 + 78fcc51 commit 9f27da2
Show file tree
Hide file tree
Showing 10 changed files with 1,259 additions and 854 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

[![Build Status](https://travis-ci.org/zumba/drill-sergeant.png?branch=master)](https://travis-ci.org/zumba/drill-sergeant)

Keep track and get notified when github pull requests become stale. This is particularly useful for teams utilizing the github flow. This will send a report to a specified email (or emails) of any pull requests that are over the specified stale time.
Keep track and get notified when github pull requests become stale. This is particularly useful for teams utilizing the github flow. `Drillsergeant` will send a notify of stale PRs via:

As of `0.1.0`, Drill Sergeant can also be configured (with the `-l, --label` command) to label the PRs as `Stale`.
* Email
* Slack
* Github (attaching a `stale` label to the PR)

![Screen shot](https://raw.githubusercontent.com/zumba/drill-sergeant/master/label-screenshot.png)

Expand All @@ -21,8 +23,7 @@ This is intended to be run via a crontab or other scheduled task runner.
A typical command line run:

```bash
$ export GITHUB_TOKEN='<your token here>'
$ drillsergeant -e "youremail@address" -r "user/repository,user/repository2"
$ GITHUB_TOKEN='<your token here>' drillsergeant -e "youremail@address" -r "user/repository,user/repository2"
```

If you want it to label the PR as stale:
Expand All @@ -31,6 +32,12 @@ If you want it to label the PR as stale:
$ drillsergeant -l -r "user/repository"
```

If you want to send it to slack:

```bash
$ drillsergeant -r "user/repository" --slack-webhook https://your-slack-channel-webhook-url
```

## Configuration

The environment variable `GITHUB_TOKEN` must be set with a valid github oauth token in order to read the pull requests.
Expand Down
38 changes: 19 additions & 19 deletions gruntfile.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
/*global module:false*/
module.exports = function(grunt) {
"use strict";
grunt.initConfig({
eslint : {
library: 'lib/**'
},
mochaTest: {
test: {
options: {
reporter: "spec",
require: "test/support/bootstrap"
},
src: ["test/spec/**/*.js"]
}
}
});
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-mocha-test');
'use strict';
grunt.initConfig({
eslint : {
library: 'lib/**'
},
mochaTest: {
test: {
options: {
reporter: 'spec',
require: 'test/support/bootstrap'
},
src: ['test/spec/**/*.js']
}
}
});
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-mocha-test');

// Default task.
grunt.registerTask('default', ['eslint', 'mochaTest']);
// Default task.
grunt.registerTask('default', ['eslint', 'mochaTest']);
};
74 changes: 38 additions & 36 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const command = require('commander');
const co = require('co');
const pkg = require('./package.json');
const GithubClient = require('./lib/github');
const GithubGraph = require('./lib/githubGraph');
Expand All @@ -8,7 +7,8 @@ const notifiers = {
email: require('./lib/notifiers/email'),
github: require('./lib/notifiers/github'),
hipchat: require('./lib/notifiers/hipchat'),
console: require('./lib/notifiers/console')
slack: require('./lib/notifiers/slack'),
console: require('./lib/notifiers/console'),
};
const notify = require('./lib/notify');
const filters = require('./lib/filters');
Expand All @@ -33,6 +33,7 @@ command
.option('--exclude-reviewed [count]', 'Filter PRs without at least [count] approved reviews.', 0)
.option('--hipchat-apikey [api key]', 'Hipchat API integration key. If included, hipchat will attempt to be notified.')
.option('--hipchat-room [room name]', 'Hipchat room ID or name.')
.option('--slack-webhook [url]', 'Slack webhook URL to post messages.')
.parse(process.argv);

if (!process.env.GITHUB_TOKEN) {
Expand All @@ -45,41 +46,42 @@ if (!command.repo.length) {
process.exit(1);
}

function main() {
co(function*() {
const githubGraph = new GithubGraph(process.env.GITHUB_TOKEN);
const githubClient = new GithubClient(process.env.GITHUB_TOKEN);
const notifier = new notify();
const stalerepos = new StaleRepos(githubGraph);
try {
const allPRs = yield stalerepos.retrieve(command.repo, command.staletime);
const results = allPRs
.filter(filters.includeLabels.bind(null, command.includeLabels))
.filter(filters.excludeLabels.bind(null, command.excludeLabels))
.filter(repo => command.includeReviewed === 0 || filters.includeReviewed(command.includeReviewed, repo))
.filter(repo => command.excludeReviewed === 0 || filters.excludeReviewed(command.excludeReviewed, repo));
if (!results.length) {
console.log('No stale pull requests to report.');
return;
}
if (command.email) {
notifier.add(new notifiers.email(command.email, command.replyto, command.subjectTemplate));
}
if (command.label) {
notifier.add(new notifiers.github(githubClient));
}
if (command.hipchatApikey) {
notifier.add(new notifiers.hipchat(command.hipchatApikey, command.hipchatRoom));
}
if (!command.email && !command.label && !command.hipchatApikey) {
notifier.add(notifiers.console);
}
notifier.notifyAll(results);
} catch (e) {
console.error(e);
process.exit(1);
async function main() {
const githubGraph = new GithubGraph(process.env.GITHUB_TOKEN);
const githubClient = new GithubClient(process.env.GITHUB_TOKEN);
const notifier = new notify();
const stalerepos = new StaleRepos(githubGraph);
try {
const allPRs = await stalerepos.retrieve(command.repo, command.staletime);
const results = allPRs
.filter(filters.includeLabels.bind(null, command.includeLabels))
.filter(filters.excludeLabels.bind(null, command.excludeLabels))
.filter(repo => command.includeReviewed === 0 || filters.includeReviewed(command.includeReviewed, repo))
.filter(repo => command.excludeReviewed === 0 || filters.excludeReviewed(command.excludeReviewed, repo));
if (!results.length) {
console.log('No stale pull requests to report.');
return;
}
});
if (command.email) {
notifier.add(new notifiers.email(command.email, command.replyto, command.subjectTemplate));
}
if (command.label) {
notifier.add(new notifiers.github(githubClient));
}
if (command.hipchatApikey) {
notifier.add(new notifiers.hipchat(command.hipchatApikey, command.hipchatRoom));
}
if (command.slackWebhook) {
notifier.add(new notifiers.slack(command.slackWebhook));
}
if (notifier.length() === 0) {
notifier.add(notifiers.console);
}
notifier.notifyAll(results);
} catch (e) {
console.error(e);
process.exit(1);
}
}

main();
Expand Down
31 changes: 13 additions & 18 deletions lib/githubGraph.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
const rp = require('request-promise-native');
const co = require('co');

const GithubGraph = class GithubGraph {
constructor(token, requester) {
this.token = token;
this.requester = requester || rp;
this.requester = requester || require('request-promise-native');
}

graph(query) {
return co.call(this, function*() {
return yield this.requester({
method: 'POST',
uri: 'https://api.github.com/graphql',
body: {
query: query
},
headers: {
'User-Agent': 'Drill-Sergeant-App',
Authorization: `bearer ${this.token}`
},
json: true
});
async graph(query) {
return await this.requester({
method: 'POST',
uri: 'https://api.github.com/graphql',
body: {
query: query
},
headers: {
'User-Agent': 'Drill-Sergeant-App',
Authorization: `bearer ${this.token}`
},
json: true
});
}
};
Expand Down
15 changes: 15 additions & 0 deletions lib/notifiers/slack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const fs = require('fs');
const _ = require('lodash');
const { IncomingWebhook } = require('@slack/client');

module.exports = class Slack {
constructor(webhookURL) {
const template = fs.readFileSync(__dirname + '/../../templates/slack.md').toString();
this.compiledTemplate = _.template(template);
this.hook = new IncomingWebhook(webhookURL);
}

async notify(repos) {
await this.hook.send(this.compiledTemplate({ repos: repos }));
}
};
30 changes: 18 additions & 12 deletions lib/notify.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
var Notifier = module.exports = function() {
this.notifiers = [];
};
module.exports = class Notifier {
constructor() {
this.notifiers = [];
}

Notifier.prototype.add = function(notifier) {
if (!notifier.notify) {
throw new Error('Provided notifier must implemnt a notify method.');
add(notifier) {
if (!notifier.notify) {
throw new Error('Provided notifier must implemnt a notify method.');
}
this.notifiers.push(notifier);
}
this.notifiers.push(notifier);
};

Notifier.prototype.notifyAll = function(repoData) {
this.notifiers.forEach(function(notifier) {
notifier.notify(repoData);
});
length() {
return this.notifiers.length;
}

notifyAll(repoData) {
this.notifiers.forEach(function (notifier) {
notifier.notify(repoData);
});
}
};
45 changes: 21 additions & 24 deletions lib/stalerepos.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const fs = require('fs');
var _ = require('lodash');
const co = require('co');

module.exports = class StaleRepos {
constructor(ghClient) {
Expand All @@ -16,36 +15,34 @@ module.exports = class StaleRepos {
return this.calcStaleTime(pr.created_at) >= staletime;
}

retrieve(repos, staletime) {
async retrieve(repos, staletime) {
const separatedRepos = repos.map(repo => {
const [ owner, name ] = repo.split('/');
return {
owner: owner,
name: name
};
});
return co.call(this, function*() {
const response = yield this.ghClient.graph(this.queryTemplate({ repos: separatedRepos }));
return _.map(response.data, (prs, key) => {
return {
repo: response.data[key].nameWithOwner,
prs: prs.pullRequests.edges
.map(pr => {
const data = pr.node;
return {
user: data.author.login,
created_at: data.createdAt,
html_url: data.url,
title: data.title,
number: data.number,
updated_at: data.updatedAt,
labels: data.labels.edges.map(entry => entry.node.name),
reviewCount: data.reviews.totalCount
};
})
.filter(pr => this.isStale(staletime, pr))
};
});
const response = await this.ghClient.graph(this.queryTemplate({ repos: separatedRepos }));
return _.map(response.data, (prs, key) => {
return {
repo: response.data[key].nameWithOwner,
prs: prs.pullRequests.edges
.map(pr => {
const data = pr.node;
return {
user: data.author.login,
created_at: data.createdAt,
html_url: data.url,
title: data.title,
number: data.number,
updated_at: data.updatedAt,
labels: data.labels.edges.map(entry => entry.node.name),
reviewCount: data.reviews.totalCount
};
})
.filter(pr => this.isStale(staletime, pr))
};
});
}
};
Loading

0 comments on commit 9f27da2

Please sign in to comment.