From f6cc9f164a1f3d8a4a6f31388534c130cc433289 Mon Sep 17 00:00:00 2001 From: xuexb Date: Sat, 14 Oct 2017 23:08:42 +0800 Subject: [PATCH] feat: Add modules --- src/app.js | 48 +++++++++++++++++++ src/github.js | 69 +++++++++++++++++++++++++++ src/modules/issues/autoAssign.js | 25 ++++++++++ src/modules/issues/autoReleaseNote.js | 18 +++++++ src/modules/issues/replyInvalid.js | 41 ++++++++++++++++ src/modules/issues/replyNeedDemo.js | 25 ++++++++++ src/utils.js | 54 +++++++++++++++++++++ 7 files changed, 280 insertions(+) create mode 100755 src/app.js create mode 100755 src/github.js create mode 100755 src/modules/issues/autoAssign.js create mode 100644 src/modules/issues/autoReleaseNote.js create mode 100755 src/modules/issues/replyInvalid.js create mode 100755 src/modules/issues/replyNeedDemo.js create mode 100755 src/utils.js diff --git a/src/app.js b/src/app.js new file mode 100755 index 0000000..7fbf385 --- /dev/null +++ b/src/app.js @@ -0,0 +1,48 @@ +/** + * @file github-bot 入口文件 + * @author xuexb + */ + +require('dotenv').config(); + +const EventEmitter = require('events'); +const Koa = require('koa'); +const bodyParser = require('koa-bodyparser'); +const requireDir = require('require-dir'); +const {verifySignature, getRepo} = require('./utils'); +const issueActions = requireDir('./modules/issues'); +const app = new Koa(); +const githubEvent = new EventEmitter(); + +app.use(bodyParser()); + +app.use(ctx => { + let eventName = ctx.request.headers['x-github-event']; + if (eventName && verifySignature(ctx.request)) { + const payload = ctx.request.body; + const action = payload.action || payload.ref_type; + + eventName += `_${action}`; + console.log(`receive event: ${eventName}`); + + if (payload.sender.login !== process.env.GITHUB_BOT_NAME) { + githubEvent.emit(eventName, { + repo: getRepo(payload.repository.full_name), + payload + }); + } + + ctx.body = 'Ok.'; + } + else { + ctx.body = 'Go away.'; + } +}); + +Object.keys(issueActions).forEach((key) => { + issueActions[key](githubEvent.on.bind(githubEvent)); +}); + +const port = 8000; +app.listen(port); +console.log(`Listening on http://0.0.0.0:${port}`); diff --git a/src/github.js b/src/github.js new file mode 100755 index 0000000..0240981 --- /dev/null +++ b/src/github.js @@ -0,0 +1,69 @@ +/** + * @file github 操作库 + * @author xuexb + */ + +const GitHub = require('github'); + +const github = new GitHub({ + debug: process.env.NODE_ENV === 'development' +}); + +github.authenticate({ + type: 'token', + token: process.env.GITHUB_TOKEN +}); + +module.exports = { + commentIssue(payload, body) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.issue.number; + + github.issues.createComment({ + owner, + repo, + number, + body + }); + }, + + closeIssue(payload) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.issue.number; + + github.issues.edit({ + owner, + repo, + number, + state: 'closed' + }); + }, + + addAssigneesToIssue(payload, assign) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.issue.number; + + github.issues.edit({ + owner, + repo, + number, + assignees: Array.isArray(assign) ? assign : [assign] + }); + }, + + addLabelsToIssue(payload, labels) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.issue.number; + + github.issues.addLabels({ + owner, + repo, + number, + labels: Array.isArray(labels) ? labels : [labels] + }); + } +}; diff --git a/src/modules/issues/autoAssign.js b/src/modules/issues/autoAssign.js new file mode 100755 index 0000000..fe26264 --- /dev/null +++ b/src/modules/issues/autoAssign.js @@ -0,0 +1,25 @@ +/** + * @file issue 自动 `assign` 给指定人员 + * @author xuexb + */ + +const {addAssigneesToIssue} = require('../../github'); + +const assignMap = { + bug: 'xuexb', + enhancement: 'xuexb', + question: 'xuexb' +}; + +function autoAssign(on) { + on('issues_labeled', ({payload, repo}) => { + if (assignMap[payload.label.name]) { + addAssigneesToIssue( + payload, + assignMap[payload.label.name] + ); + } + }); +} + +module.exports = autoAssign; diff --git a/src/modules/issues/autoReleaseNote.js b/src/modules/issues/autoReleaseNote.js new file mode 100644 index 0000000..7909dbb --- /dev/null +++ b/src/modules/issues/autoReleaseNote.js @@ -0,0 +1,18 @@ +/** + * @file 根据 tag 自动 release + * @author xuexb + */ + +const {getReleaseByTag} = require('../../github'); +const {cloneRepo, getTags} = require('../../utils'); + +function autoReleaseNote(on) { + on('create_tag', ({payload, repo}) => { + const repoDir = cloneRepo(payload.repository.clone_url, repo); + const tags = getTags(repoDir); + + console.log(repoDir, tags); + }); +} + +module.exports = autoReleaseNote; diff --git a/src/modules/issues/replyInvalid.js b/src/modules/issues/replyInvalid.js new file mode 100755 index 0000000..1fc14e0 --- /dev/null +++ b/src/modules/issues/replyInvalid.js @@ -0,0 +1,41 @@ +/** + * @file 不规范issue则自动关闭 + * @author xuexb + */ + +const format = require('string-template'); +const { + commentIssue, + closeIssue, + addLabelsToIssue +} = require('../../github'); + +const comment = [ + 'hi @{user},非常感谢您的反馈,', + '但是由于您没有使用 [规范的issue](https://github.com/xuexb/github-bot) 格式, 将直接被关闭, 谢谢!' +].join(''); + +const match = str => { + return /node version:\s*(\d\.?)+/.test(str) && /url:\s*(https?:)?\/\/(\w{3,})/.test(str); +}; + +function replyInvalid(on) { + on('issues_opened', ({payload}) => { + const issue = payload.issue; + const opener = issue.user.login; + + if (!match(issue.body)) { + commentIssue( + payload, + format(comment, { + user: opener + }) + ); + + closeIssue(payload); + addLabelsToIssue(payload, 'invalid'); + } + }); +} + +module.exports = replyInvalid; diff --git a/src/modules/issues/replyNeedDemo.js b/src/modules/issues/replyNeedDemo.js new file mode 100755 index 0000000..1506bc0 --- /dev/null +++ b/src/modules/issues/replyNeedDemo.js @@ -0,0 +1,25 @@ +/** + * @file 不规范issue则自动关闭 + * @author xuexb + */ + +const format = require('string-template'); +const {commentIssue} = require('../../github'); + +const comment = 'hi @{user},请提供一个可预览的链接,如: '; + +function replyNeedDemo(on) { + on('issues_labeled', ({payload, repo}) => { + if (payload.label.name === 'need demo') { + commentIssue( + payload, + format(comment, { + user: payload.issue.user.login + }) + ); + } + + }); +} + +module.exports = replyNeedDemo; diff --git a/src/utils.js b/src/utils.js new file mode 100755 index 0000000..a286067 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,54 @@ +const path = require('path'); +const fs = require('fs'); +const crypto = require('crypto'); +const {fixedTimeComparison} = require('cryptiles'); +const {execSync, exec} = require('child_process'); + +const utils = { + mentioned(body) { + return body.includes(`@${process.env.GITHUB_BOT_NAME}`); + }, + + verifySignature(request) { + let signature = crypto.createHmac('sha1', process.env.GITHUB_SECRET_TOKEN) + .update(request.rawBody) + .digest('hex'); + signature = `sha1=${signature}`; + return fixedTimeComparison(signature, request.headers['x-hub-signature']); + }, + + getRepo(url) { + return url.split('/')[1]; + }, + + isDirectory(file) { + try { + return fs.statSync(file).isDirectory(); + } + catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } + + return false; + } + }, + + cloneRepo(url, repo) { + const repoDir = path.resolve(__dirname, '../github/', repo); + + if (!utils.isDirectory(repoDir)) { + throw new Error(`${repoDir} 不是github目录!`); + } + + execSync(`cd ${repoDir} && git pull`); + + return repoDir; + }, + + getTags(dir) { + return execSync(`cd ${dir} && git tag -l`).toString().split(/\n+/).filter(tag => !!tag).reverse(); + } +}; + +module.exports = utils;