diff --git a/src/app.js b/src/app.js index 7823392..5cbfe84 100755 --- a/src/app.js +++ b/src/app.js @@ -9,8 +9,10 @@ const EventEmitter = require('events'); const Koa = require('koa'); const bodyParser = require('koa-bodyparser'); const requireDir = require('require-dir'); -const {verifySignature, getRepo} = require('./utils'); +const {verifySignature} = require('./utils'); const issueActions = requireDir('./modules/issues'); +const pullRequestActions = requireDir('./modules/pull_request'); +const releasesActions = requireDir('./modules/releases'); const app = new Koa(); const githubEvent = new EventEmitter(); @@ -28,12 +30,10 @@ app.use(ctx => { console.log(`receive event: ${eventName}`); - if (payload.sender.login !== process.env.GITHUB_BOT_NAME) { - githubEvent.emit(eventName, { - repo: getRepo(payload.repository.full_name), - payload - }); - } + githubEvent.emit(eventName, { + repo: payload.repository.name, + payload + }); ctx.body = 'Ok.'; } @@ -42,8 +42,11 @@ app.use(ctx => { } }); -Object.keys(issueActions).forEach((key) => { - issueActions[key](githubEvent.on.bind(githubEvent)); + +const actions = Object.assign({}, issueActions, pullRequestActions, releasesActions); +Object.keys(actions).forEach((key) => { + actions[key](githubEvent.on.bind(githubEvent)); + console.log(`bind ${key} success!`); }); const port = 8000; diff --git a/src/github.js b/src/github.js index 47f05f7..8d5d7e5 100755 --- a/src/github.js +++ b/src/github.js @@ -4,9 +4,10 @@ */ const GitHub = require('github'); +const {toArray} = require('./utils'); const github = new GitHub({ - debug: process.env.NODE_ENV === 'development' + debug: false//process.env.NODE_ENV === 'development' }); github.authenticate({ @@ -16,6 +17,62 @@ github.authenticate({ module.exports = { + github, + + /** + * issue 是否包含某 label + * + * @param {Object} payload data + * @param {string} body 评论内容 + */ + issueHasLabel(payload, label) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.issue.number; + + return new Promise((resolve, reject) => { + github.issues.getIssueLabels({ + owner, + repo, + number + }).then(res => { + if (res.data.map(v => v.name).indexOf(label) > -1) { + resolve(); + } + else { + reject(); + } + }, reject); + }); + }, + + /** + * PR 是否包含某 label + * + * @param {Object} payload data + * @param {string} body 评论内容 + */ + pullRequestHasLabel(payload, label) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.pull_request.number; + + return new Promise((resolve, reject) => { + github.issues.getIssueLabels({ + owner, + repo, + number + }).then(res => { + if (res.data.map(v => v.name).indexOf(label) > -1) { + resolve(); + } + else { + reject(); + } + }, reject); + }); + }, + /** * 评论 issue * @@ -35,6 +92,25 @@ module.exports = { }); }, + /** + * 评论 PR + * + * @param {Object} payload data + * @param {string} body 评论内容 + */ + commentPullRequest(payload, body) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.pull_request.number; + + github.issues.createComment({ + owner, + repo, + number, + body + }); + }, + /** * 关闭 issue * @@ -91,6 +167,63 @@ module.exports = { }); }, + /** + * 添加标签到 PR + * + * @param {Object} payload data + * @param {string | Array} labels 标签 + */ + addLabelsToPullRequest(payload, labels) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.pull_request.number; + + github.issues.addLabels({ + owner, + repo, + number, + labels: Array.isArray(labels) ? labels : [labels] + }); + }, + + /** + * 删除 PR 标签 + * + * @param {Object} payload data + * @param {string} name 标签名 + */ + removeLabelsToPullRequest(payload, name) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.pull_request.number; + + github.issues.removeLabel({ + owner, + repo, + number, + name + }); + }, + + /** + * 删除 issue 标签 + * + * @param {Object} payload data + * @param {string} name 标签名 + */ + removeLabelsToIssue(payload, name) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.issues.number; + + github.issues.removeLabel({ + owner, + repo, + number, + name + }); + }, + /** * 创建发布 * @@ -135,5 +268,28 @@ module.exports = { repo, tag: tag_name }); - } + }, + + /** + * 创建 review 请求 + * + * @param {Object} payload data + * @param {Array | string} options.reviewers reviewer + * @param {Array | string} options.team_reviewers team_reviewers + * + * @return {Promise} + */ + createReviewRequest(payload, {reviewers, team_reviewers}) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const number = payload.pull_request.number; + + return github.pullRequests.createReviewRequest({ + owner, + repo, + number, + reviewers: toArray(reviewers), + team_reviewers: toArray(team_reviewers) + }); + }, }; diff --git a/src/modules/pull_request/autoReviewRequest.js b/src/modules/pull_request/autoReviewRequest.js new file mode 100644 index 0000000..b4e3ec3 --- /dev/null +++ b/src/modules/pull_request/autoReviewRequest.js @@ -0,0 +1,23 @@ +/** + * @file PR 自动根据 tag 去添加 reviewer + * @author xuexb + */ + +const {getPkgConfig} = require('../../utils'); +const {createReviewRequest} = require('../../github'); + +const config = getPkgConfig(); +const assignMap = config.lableToAuthor || {}; + +module.exports = on => { + on('pull_request_labeled', ({payload, repo}) => { + if (assignMap[payload.label.name]) { + createReviewRequest( + payload, + { + reviewers: assignMap[payload.label.name] + } + ); + } + }); +} diff --git a/src/modules/pull_request/replyInvalidTitle.js b/src/modules/pull_request/replyInvalidTitle.js new file mode 100644 index 0000000..59405f8 --- /dev/null +++ b/src/modules/pull_request/replyInvalidTitle.js @@ -0,0 +1,61 @@ +/** + * @file PR 提示标题正确性 + * @author xuexb + */ + +const format = require('string-template'); +const {getPkgCommitPrefix} = require('../../utils'); +const { + commentPullRequest, + addLabelsToPullRequest, + removeLabelsToPullRequest, + pullRequestHasLabel +} = require('../../github'); + +const actions = getPkgCommitPrefix(); +const match = title => { + return actions.some(action => title.indexOf(`${action}:`) === 0); +}; + +const commentSuccess = [ + 'hi @{user},非常感谢您及时修正标题格式,祝您玩的开心!' +].join(''); + +const commentError = [ + 'hi @{user},非常感谢您的 PR ,', + '但是您没有使用 [PR 标题规则](https://github.com/xuexb/github-bot#commit-log-和-pr-标题规则) 格式,', + '请及时修改, 谢谢!' +].join(''); + +module.exports = on => { + if (actions.length) { + on('pull_request_opened', ({payload, repo}) => { + if (!match(payload.pull_request.title)) { + commentPullRequest( + payload, + format(commentError, { + user: payload.pull_request.user.login + }) + ); + + addLabelsToPullRequest(payload, 'invalid'); + } + }); + + on('pull_request_edited', ({payload, repo}) => { + if (match(payload.pull_request.title)) { + pullRequestHasLabel(payload, 'invalid').then(() => { + commentPullRequest( + payload, + format(commentSuccess, { + user: payload.pull_request.user.login + }) + ); + + removeLabelsToPullRequest(payload, 'invalid'); + }); + } + }); + } + +}; diff --git a/src/modules/pull_request/titlePrefixToLabel.js b/src/modules/pull_request/titlePrefixToLabel.js new file mode 100644 index 0000000..2c9009e --- /dev/null +++ b/src/modules/pull_request/titlePrefixToLabel.js @@ -0,0 +1,33 @@ +/** + * @file PR 标题自动打标签 + * @author xuexb + */ + +const { + addLabelsToPullRequest, + pullRequestHasLabel +} = require('../../github'); + +const getAction = title => { + return (title.match(/^(\w+?):/) || [])[1]; +}; + +const ACTION_TO_LABEL_MAP = { + feat: 'enhancement', + fix: 'bug' +}; + +const handle = ({payload, repo}) => { + const action = getAction(payload.pull_request.title); + + if (action && ACTION_TO_LABEL_MAP[action]) { + pullRequestHasLabel(payload, ACTION_TO_LABEL_MAP[action]).catch(() => { + addLabelsToPullRequest(payload, ACTION_TO_LABEL_MAP[action]); + }); + } +}; + +module.exports = on => { + on('pull_request_edited', handle); + on('pull_request_opened', handle); +}; diff --git a/src/modules/releases/autoNote.js b/src/modules/releases/autoReleaseNote.js similarity index 97% rename from src/modules/releases/autoNote.js rename to src/modules/releases/autoReleaseNote.js index e3036cb..ee59d99 100644 --- a/src/modules/releases/autoNote.js +++ b/src/modules/releases/autoReleaseNote.js @@ -13,7 +13,7 @@ const RELEASE_CHANGE_MAP = { close: 'close' }; -function autoReleaseNote(on) { +module.exports = on => { on('create_tag', ({payload, repo}) => { getReleaseByTag(payload, { tag_name: payload.ref @@ -78,5 +78,3 @@ function autoReleaseNote(on) { }); }); } - -module.exports = autoReleaseNote; diff --git a/src/utils.js b/src/utils.js index 55d1616..1d88795 100755 --- a/src/utils.js +++ b/src/utils.js @@ -23,17 +23,6 @@ const utils = { return fixedTimeComparison(signature, request.headers['x-hub-signature']); }, - /** - * 获取项目名 - * - * @param {string} url xuexb/repo - * - * @return {string} repo - */ - getRepo(url) { - return url.split('/')[1]; - }, - /** * 目录是否存在 * @@ -124,6 +113,37 @@ const utils = { }, pkg.config); return config['github-bot']; + }, + + /** + * 获取 commit log 前缀白名单 + * + * @return {Array} + */ + getPkgCommitPrefix() { + const pkg = require('../package.json'); + const config = Object.assign({ + 'validate-commit-msg': { + 'types': [] + } + }, pkg.config); + + return config['validate-commit-msg'].types; + }, + + /** + * 转化成 Array + * + * @param {string | Array} str 目标值 + * + * @return {Array} + */ + toArray(str) { + if (str) { + return Array.isArray(str) ? str : [str]; + } + + return str; } };