diff --git a/lib/github-client.js b/lib/github-client.js index 361261ca..ff487385 100644 --- a/lib/github-client.js +++ b/lib/github-client.js @@ -3,6 +3,7 @@ const GitHub = require('github') const githubClient = new GitHub({ + debug: require('debug')('github').enabled, version: '3.0.0', protocol: 'https', host: 'api.github.com', @@ -17,4 +18,11 @@ githubClient.authenticate({ token: process.env.GITHUB_TOKEN }) +// store/cache info about the authenticated user +githubClient.user.get({}, (error, user) => { + if (error) return console.error(error) + console.log('authenticated as:', user.login) + githubClient.user = user +}) + module.exports = githubClient diff --git a/package.json b/package.json index 6187c5c2..d5c5e9c9 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,11 @@ "body-parser": "^1.15.0", "debug": "^2.2.0", "dotenv": "^2.0.0", + "effd": "^1.4.8", "express": "^4.13.4", "github": "^0.2.4", "glob": "^7.0.3", + "mentions-regex": "^2.0.3", "travis-ci": "^2.1.0" }, "devDependencies": { diff --git a/scripts/manage-members.js b/scripts/manage-members.js new file mode 100644 index 00000000..631baa69 --- /dev/null +++ b/scripts/manage-members.js @@ -0,0 +1,168 @@ +'use strict' + +require('dotenv').load({ silent: true }) + +const ƒ = require('effd') +const request = require('request') +const debug = require('debug')('manage-members') +const githubClient = require('../lib/github-client.js') +const github = { + issues: ƒ.promisify(githubClient.issues), + pullRequests: ƒ.promisify(githubClient.pullRequests), + orgs: ƒ.promisify(githubClient.orgs) +} + +// we need this RegExp to have the global flag +const mentionsRegex = new RegExp(require('mentions-regex')().source, 'g') +const exclude = (a, b) => a.filter((x) => !b.find((y) => y === x)) +const error = (e) => typeof e === 'string' ? debug(e) : console.error(e) +const findREADME = (array) => array.find((name) => /readme.md/i.test(name)) + +module.exports = function (app) { + app.on('push', (event, org, repo) => { + const head = event.head_commit + const readme = findREADME(head.modified) || findREADME(head.added) + if (!readme) return + + download(`https://github.com/${org}/${repo}/raw/${head.id}/${readme}`) + .then((content) => + findChangedMembers(org, repo, content) + ) + .then((changes) => + updateMembers(org, changes) || ƒ.reject('No members changed') + ) + .catch(error) + }) + + app.on('pull_request.opened', comment) + app.on('pull_request.synchronize', comment) + + app.on('pull_request.assigned', (event, user, repo) => { + if (event.assignee.login !== githubClient.user.login) return + + const number = event.number + github.issues.edit({ user, repo, number, assignee: '' }) + .catch(console.error) + + comment(event, user, repo) + }) +} + +function updateMembers (org, changes) { + debug('updateMembers %s %j', org, changes) + if (!changes.added.length && !changes.removed.length) return + + return ƒ.all([].concat( + changes.added.map((user) => + github.orgs.addTeamMembership({ id: changes.id, user }).catch(error) + ), + changes.removed.map((user) => + github.orgs.deleteTeamMember({ id: changes.id, user }).catch(error) + ))) +} + +function comment (event, user, repo) { + var number = event.number + debug('comment %s %s %s', user, repo, number) + return ( + getREADME(user, repo, number) + .then((readme) => + findChangedMembers(user, repo, readme) + ) + .then((changes) => + createMessage(user, changes) || ƒ.reject('No members changed') + ) + .then((message) => + findExistingComment(user, repo, number) + .then((comment) => + comment + ? github.issues.editComment({ user, repo, id: comment.id, body: message + updated() }) + : github.issues.createComment({ user, repo, number, body: message }) // TODO: test this path + ) + ) + .then((comment) => { debug('comment posted') }) + .catch(error) + ) +} + +function updated () { + return `\nupdated on ${new Date().toISOString()}` +} + +function getREADME (org, repo, number) { + debug('getREADME %s %s %d', org, repo, number) + + return github.pullRequests.getFiles({ user: org, repo, number, per_page: 100 }) + .then((files) => + files.find((file) => /^README.md$/i.test(file.filename)) || ƒ.reject('Not Applicable') + ) + .then((readme) => + download(readme.raw_url) + ) +} + +function findChangedMembers (org, repo, content) { + debug('findChangedMembers %s %s', org, repo) + + const match = content.match(/