diff --git a/.editorconfig b/.editorconfig index e291365..9d08a1a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,7 @@ root = true [*] charset = utf-8 indent_style = space -indent_size = 4 +indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..b8d07e3 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,19 @@ +// http://eslint.org/docs/user-guide/configuring + +module.exports = { + root: true, + env: { + 'node': true + }, + // https://github.com/standard/standard/blob/master/docs/RULES-en.md + extends: 'standard', + // add your custom rules here + 'rules': { + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 + } + } diff --git a/package.json b/package.json index 7a32d12..8c533c9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,10 @@ "version": "0.0.1", "main": "src/app.js", "scripts": { - "start": "NODE_ENV=development node src/app" + "start": "NODE_ENV=development node src/app", + "lint": "eslint src/**/* --quiet", + "precommit": "npm run lint", + "commitmsg": "validate-commit-msg" }, "repository": { "type": "git", @@ -27,12 +30,9 @@ "string-template": "^1.0.0" }, "engines": { - "node": ">= 4" + "node": ">= 7.8.0" }, "config": { - "ghooks": { - "commit-msg": "validate-commit-msg" - }, "validate-commit-msg": { "types": [ "feat", @@ -53,10 +53,20 @@ } } }, - "os": ["darwin", "linux"], + "os": [ + "darwin", + "linux" + ], "devDependencies": { - "ghooks": "^2.0.0", + "eslint": "^4.9.0", + "eslint-config-standard": "^10.2.1", + "eslint-friendly-formatter": "^3.0.0", + "eslint-plugin-import": "^2.7.0", + "eslint-plugin-node": "^5.2.0", + "eslint-plugin-promise": "^3.6.0", + "eslint-plugin-standard": "^3.0.1", "git-pull-or-clone": "xuexb/git-pull-or-clone", + "husky": "^0.14.3", "validate-commit-msg": "^2.14.0" } } diff --git a/src/app.js b/src/app.js index 5cbfe84..6957cd6 100755 --- a/src/app.js +++ b/src/app.js @@ -3,52 +3,50 @@ * @author xuexb */ -require('dotenv').config(); +require('dotenv').config() -const EventEmitter = require('events'); -const Koa = require('koa'); -const bodyParser = require('koa-bodyparser'); -const requireDir = require('require-dir'); -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(); +const EventEmitter = require('events') +const Koa = require('koa') +const bodyParser = require('koa-bodyparser') +const requireDir = require('require-dir') +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() -app.use(bodyParser()); +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; + 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 - if (action) { - eventName += `_${action}`; - } - - console.log(`receive event: ${eventName}`); + if (action) { + eventName += `_${action}` + } - githubEvent.emit(eventName, { - repo: payload.repository.name, - payload - }); + console.log(`receive event: ${eventName}`) - ctx.body = 'Ok.'; - } - else { - ctx.body = 'Go away.'; - } -}); + githubEvent.emit(eventName, { + repo: payload.repository.name, + payload + }) + ctx.body = 'Ok.' + } else { + ctx.body = 'Go away.' + } +}) -const actions = Object.assign({}, issueActions, pullRequestActions, releasesActions); +const actions = Object.assign({}, issueActions, pullRequestActions, releasesActions) Object.keys(actions).forEach((key) => { - actions[key](githubEvent.on.bind(githubEvent)); - console.log(`bind ${key} success!`); -}); + actions[key](githubEvent.on.bind(githubEvent)) + console.log(`bind ${key} success!`) +}) -const port = 8000; -app.listen(port); -console.log(`Listening on http://0.0.0.0:${port}`); +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 index 16ad0bc..40a8b88 100755 --- a/src/github.js +++ b/src/github.js @@ -3,293 +3,378 @@ * @author xuexb */ -const GitHub = require('github'); -const {toArray} = require('./utils'); +const GitHub = require('github') +const { toArray } = require('./utils') const github = new GitHub({ - debug: process.env.NODE_ENV === 'development' -}); + debug: process.env.NODE_ENV === 'development' +}) github.authenticate({ - type: 'token', - token: process.env.GITHUB_TOKEN -}); + type: 'token', + token: process.env.GITHUB_TOKEN +}) module.exports = { - github, + 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; + /** + * issue 是否包含某 label + * + * @param {Object} payload data + * @param {string} body 评论内容 + */ + async 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); - }); - }, + try { + const res = github.issues.getIssueLabels({ + owner, + repo, + number + }) - /** - * 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; + if (res.data.map(v => v.name).indexOf(label) === -1) { + Promise.reject(new Error('issue no label')) + } + } catch (e) { + Promise.reject(e) + } + }, - 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 评论内容 + */ + async pullRequestHasLabel (payload, label) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + const number = payload.pull_request.number - /** - * 评论 issue - * - * @param {Object} payload data - * @param {string} body 评论内容 - */ - commentIssue(payload, body) { - const owner = payload.repository.owner.login; - const repo = payload.repository.name; - const number = payload.issue.number; + try { + const res = await github.issues.getIssueLabels({ + owner, + repo, + number + }) + if (res.data.map(v => v.name).indexOf(label) === -1) { + Promise.reject(new Error('pull request no label')) + } + } catch (e) { + Promise.reject(e) + } + }, - github.issues.createComment({ - owner, - repo, - number, - body - }); - }, + /** + * 评论 issue + * + * @param {Object} payload data + * @param {string} body 评论内容 + */ + async commentIssue (payload, body) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + const number = payload.issue.number - /** - * 评论 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; + try { + const res = await github.issues.createComment({ + owner, + repo, + number, + body + }) + return res + } catch (e) { + Promise.reject(e) + } + }, - github.issues.createComment({ - owner, - repo, - number, - body - }); - }, + /** + * 评论 PR + * + * @param {Object} payload data + * @param {string} body 评论内容 + */ + async commentPullRequest (payload, body) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + const number = payload.pull_request.number - /** - * 关闭 issue - * - * @param {Object} payload data - */ - closeIssue(payload) { - const owner = payload.repository.owner.login; - const repo = payload.repository.name; - const number = payload.issue.number; + try { + const res = await github.issues.createComment({ + owner, + repo, + number, + body + }) + return res + } catch (e) { + Promise.reject(e) + } + }, - github.issues.edit({ - owner, - repo, - number, - state: 'closed' - }); - }, + /** + * 关闭 issue + * + * @param {Object} payload data + */ + async closeIssue (payload) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + const number = payload.issue.number - /** - * 分派作者到 issues - * - * @param {Object} payload data - * @param {string | Array} assign 用户id - */ - addAssigneesToIssue(payload, assign) { - const owner = payload.repository.owner.login; - const repo = payload.repository.name; - const number = payload.issue.number; + try { + const res = await github.issues.edit({ + owner, + repo, + number, + state: 'closed' + }) + return res + } catch (e) { + Promise.reject(e) + } + }, - github.issues.edit({ - owner, - repo, - number, - assignees: Array.isArray(assign) ? assign : [assign] - }); - }, + /** + * 分派作者到 issues + * + * @param {Object} payload data + * @param {string | Array} assign 用户id + */ + async addAssigneesToIssue (payload, assign) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + const number = payload.issue.number - /** - * 添加标签到 issue - * - * @param {Object} payload data - * @param {string | Array} labels 标签 - */ - addLabelsToIssue(payload, labels) { - const owner = payload.repository.owner.login; - const repo = payload.repository.name; - const number = payload.issue.number; + try { + const res = await github.issues.edit({ + owner, + repo, + number, + assignees: Array.isArray(assign) ? assign : [assign] + }) + return res + } catch (e) { + Promise.reject(e) + } + }, - github.issues.addLabels({ - owner, - repo, - number, - labels: Array.isArray(labels) ? labels : [labels] - }); - }, + /** + * 添加标签到 issue + * + * @param {Object} payload data + * @param {string | Array} labels 标签 + */ + async addLabelsToIssue (payload, labels) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + const number = payload.issue.number - /** - * 添加标签到 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; + try { + const res = await github.issues.addLabels({ + owner, + repo, + number, + labels: Array.isArray(labels) ? labels : [labels] + }) + return res + } catch (e) { + Promise.reject(e) + } + }, - github.issues.addLabels({ - owner, - repo, - number, - labels: Array.isArray(labels) ? labels : [labels] - }); - }, + /** + * 添加标签到 PR + * + * @param {Object} payload data + * @param {string | Array} labels 标签 + */ + async addLabelsToPullRequest (payload, labels) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + const number = payload.pull_request.number - /** - * 删除 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; + try { + const res = await github.issues.addLabels({ + owner, + repo, + number, + labels: Array.isArray(labels) ? labels : [labels] + }) + return res + } catch (e) { + Promise.reject(e) + } + }, - github.issues.removeLabel({ - owner, - repo, - number, - name - }); - }, + /** + * 删除 PR 标签 + * + * @param {Object} payload data + * @param {string} name 标签名 + */ + async removeLabelsToPullRequest (payload, name) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + const number = payload.pull_request.number - /** - * 删除 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; + try { + const res = await Fgithub.issues.removeLabel({ + owner, + repo, + number, + name + }) + return res + } catch (e) { + Promise.reject(e) + } + }, - github.issues.removeLabel({ - owner, - repo, - number, - name - }); - }, + /** + * 删除 issue 标签 + * + * @param {Object} payload data + * @param {string} name 标签名 + */ + async removeLabelsToIssue (payload, name) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + const number = payload.issues.number + try { + const res = await github.issues.removeLabel({ + owner, + repo, + number, + name + }) + return res + } catch (e) { + Promise.reject(e) + } + }, - /** - * 创建发布 - * - * @param {Object} payload data - * @param {string} options.tag_name tag名 - * @param {string} options.target_commitish tag hash - * @param {string} options.name 标题 - * @param {string} options.body 内容 - * @param {boolean} options.draft 是否为草稿 - * @param {boolean} options.prerelease 是否预发布 - */ - createRelease(payload, {tag_name, target_commitish, name, body, draft, prerelease}) { - const owner = payload.repository.owner.login; - const repo = payload.repository.name; + /** + * 创建发布 + * + * @param {Object} payload data + * @param {string} options.tag_name tag名 + * @param {string} options.target_commitish tag hash + * @param {string} options.name 标题 + * @param {string} options.body 内容 + * @param {boolean} options.draft 是否为草稿 + * @param {boolean} options.prerelease 是否预发布 + */ + async createRelease (payload, { tag_name, target_commitish, name, body, draft, prerelease }) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + try { + const res = await github.repos.createRelease({ + owner, + repo, + tag_name, + target_commitish, + name, + body, + draft, + prerelease + }) + return res + } catch (e) { + Promise.reject(e) + } + }, - github.repos.createRelease({ - owner, - repo, - tag_name, - target_commitish, - name, - body, - draft, - prerelease - }); - }, + /** + * 根据tag获取发布信息 + * + * @param {Object} payload data + * @param {string} options.tag_name tag名 + * + * @return {Promise} + */ + async getReleaseByTag (payload, { tag_name }) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + try { + const res = await github.repos.getReleaseByTag({ + owner, + repo, + tag: tag_name + }) + return res + } catch (e) { + return false + } + }, - /** - * 根据tag获取发布信息 - * - * @param {Object} payload data - * @param {string} options.tag_name tag名 - * - * @return {Promise} - */ - getReleaseByTag(payload, {tag_name}) { - const owner = payload.repository.owner.login; - const repo = payload.repository.name; + /** + * 创建 review 请求 + * + * @param {Object} payload data + * @param {Array | string} options.reviewers reviewer + * @param {Array | string} options.team_reviewers team_reviewers + * + * @return {Promise} + */ + async createReviewRequest (payload, { reviewers, team_reviewers }) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + const number = payload.pull_request.number + try { + const res = await github.pullRequests.createReviewRequest({ + owner, + repo, + number, + reviewers: toArray(reviewers), + team_reviewers: toArray(team_reviewers) + }) + return res + } catch (e) { + Promise.reject(e) + } + }, - return github.repos.getReleaseByTag({ - owner, - repo, - tag: tag_name - }); - }, + /** + * 获得 repo 所有的tag + * + * @param {any} payload data + * @returns + */ + async getTags (payload) { + const owner = payload.repository.owner.login + const repo = payload.repository.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; + try { + const res = await github.repos.getTags({ + owner, + repo + }) + return res.data + } catch (e) { + Promise.reject(e) + } + }, - return github.pullRequests.createReviewRequest({ - owner, - repo, - number, - reviewers: toArray(reviewers), - team_reviewers: toArray(team_reviewers) - }); - }, -}; + async compareCommits (payload, { base, head }) { + const owner = payload.repository.owner.login + const repo = payload.repository.name + try { + const res = await github.repos.compareCommits({ + owner, + repo, + base, + head + }) + return res.data + } catch (e) { + Promise.reject(e) + } + } +} diff --git a/src/modules/issues/autoAssign.js b/src/modules/issues/autoAssign.js index ab14901..c2c7b37 100755 --- a/src/modules/issues/autoAssign.js +++ b/src/modules/issues/autoAssign.js @@ -3,21 +3,21 @@ * @author xuexb */ -const {getPkgConfig} = require('../../utils'); -const {addAssigneesToIssue} = require('../../github'); +const { getPkgConfig } = require('../../utils') +const { addAssigneesToIssue } = require('../../github') -const config = getPkgConfig(); -const assignMap = config.labelToAuthor || {}; +const config = getPkgConfig() +const assignMap = config.labelToAuthor || {} -function autoAssign(on) { - on('issues_labeled', ({payload, repo}) => { - if (assignMap[payload.label.name]) { - addAssigneesToIssue( - payload, - assignMap[payload.label.name] - ); - } - }); +function autoAssign (on) { + on('issues_labeled', ({ payload, repo }) => { + if (assignMap[payload.label.name]) { + addAssigneesToIssue( + payload, + assignMap[payload.label.name] + ) + } + }) } -module.exports = autoAssign; +module.exports = autoAssign diff --git a/src/modules/issues/replyInvalid.js b/src/modules/issues/replyInvalid.js index 7dcfeaf..bbfaa82 100755 --- a/src/modules/issues/replyInvalid.js +++ b/src/modules/issues/replyInvalid.js @@ -3,39 +3,39 @@ * @author xuexb */ -const format = require('string-template'); +const format = require('string-template') const { - commentIssue, - closeIssue, - addLabelsToIssue -} = require('../../github'); + commentIssue, + closeIssue, + addLabelsToIssue +} = require('../../github') const comment = [ - 'hi @{user},非常感谢您的反馈,', - '但是由于您没有使用 [规范的issue](https://github.com/xuexb/github-bot#issue-规则) 格式, 将直接被关闭, 谢谢!' -].join(''); + 'hi @{user},非常感谢您的反馈,', + '但是由于您没有使用 [规范的issue](https://github.com/xuexb/github-bot#issue-规则) 格式, 将直接被关闭, 谢谢!' +].join('') const match = str => { - return /node version:\s*[vV]?(\d\.?)+/.test(str); -}; + return /node version:\s*[vV]?(\d\.?)+/.test(str) +} -function replyInvalid(on) { - on('issues_opened', ({payload}) => { - const issue = payload.issue; - const opener = issue.user.login; +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 - }) - ); + if (!match(issue.body)) { + commentIssue( + payload, + format(comment, { + user: opener + }) + ) - closeIssue(payload); - addLabelsToIssue(payload, 'invalid'); - } - }); + closeIssue(payload) + addLabelsToIssue(payload, 'invalid') + } + }) } -module.exports = replyInvalid; +module.exports = replyInvalid diff --git a/src/modules/issues/replyNeedDemo.js b/src/modules/issues/replyNeedDemo.js index 1506bc0..25b428a 100755 --- a/src/modules/issues/replyNeedDemo.js +++ b/src/modules/issues/replyNeedDemo.js @@ -3,23 +3,22 @@ * @author xuexb */ -const format = require('string-template'); -const {commentIssue} = require('../../github'); +const format = require('string-template') +const { commentIssue } = require('../../github') -const comment = 'hi @{user},请提供一个可预览的链接,如: '; +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 - }) - ); - } - - }); +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; +module.exports = replyNeedDemo diff --git a/src/modules/pull_request/autoReviewRequest.js b/src/modules/pull_request/autoReviewRequest.js index 8f39e50..8133d5e 100644 --- a/src/modules/pull_request/autoReviewRequest.js +++ b/src/modules/pull_request/autoReviewRequest.js @@ -3,21 +3,21 @@ * @author xuexb */ -const {getPkgConfig} = require('../../utils'); -const {createReviewRequest} = require('../../github'); +const { getPkgConfig } = require('../../utils') +const { createReviewRequest } = require('../../github') -const config = getPkgConfig(); -const assignMap = config.labelToAuthor || {}; +const config = getPkgConfig() +const assignMap = config.labelToAuthor || {} module.exports = on => { - on('pull_request_labeled', ({payload, repo}) => { - if (assignMap[payload.label.name]) { - createReviewRequest( - payload, - { - reviewers: assignMap[payload.label.name] - } - ); + 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 index 59405f8..1e01e1d 100644 --- a/src/modules/pull_request/replyInvalidTitle.js +++ b/src/modules/pull_request/replyInvalidTitle.js @@ -3,59 +3,57 @@ * @author xuexb */ -const format = require('string-template'); -const {getPkgCommitPrefix} = require('../../utils'); +const format = require('string-template') +const { getPkgCommitPrefix } = require('../../utils') const { - commentPullRequest, - addLabelsToPullRequest, - removeLabelsToPullRequest, - pullRequestHasLabel -} = require('../../github'); + commentPullRequest, + addLabelsToPullRequest, + removeLabelsToPullRequest, + pullRequestHasLabel +} = require('../../github') -const actions = getPkgCommitPrefix(); +const actions = getPkgCommitPrefix() const match = title => { - return actions.some(action => title.indexOf(`${action}:`) === 0); -}; + return actions.some(action => title.indexOf(`${action}:`) === 0) +} const commentSuccess = [ - 'hi @{user},非常感谢您及时修正标题格式,祝您玩的开心!' -].join(''); + 'hi @{user},非常感谢您及时修正标题格式,祝您玩的开心!' +].join('') const commentError = [ - 'hi @{user},非常感谢您的 PR ,', - '但是您没有使用 [PR 标题规则](https://github.com/xuexb/github-bot#commit-log-和-pr-标题规则) 格式,', - '请及时修改, 谢谢!' -].join(''); + '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'); - }); - } - }); - } - -}; + 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', async ({ payload, repo }) => { + if (match(payload.pull_request.title)) { + await pullRequestHasLabel(payload, 'invalid') + 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 index d498adb..57d7c09 100644 --- a/src/modules/pull_request/titlePrefixToLabel.js +++ b/src/modules/pull_request/titlePrefixToLabel.js @@ -4,31 +4,31 @@ */ const { - addLabelsToPullRequest, - pullRequestHasLabel -} = require('../../github'); + addLabelsToPullRequest, + pullRequestHasLabel +} = require('../../github') const getAction = title => { - return (title.match(/^(\w+?):/) || [])[1]; -}; + return (title.match(/^(\w+?):/) || [])[1] +} const ACTION_TO_LABEL_MAP = { - feat: 'enhancement', - fix: 'bug', - docs: 'document' -}; + feat: 'enhancement', + fix: 'bug', + docs: 'document' +} -const handle = ({payload, repo}) => { - const action = getAction(payload.pull_request.title); +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]); - }); - } -}; + 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); -}; + on('pull_request_edited', handle) + on('pull_request_opened', handle) +} diff --git a/src/modules/releases/autoReleaseNote.js b/src/modules/releases/autoReleaseNote.js index bdb10e8..c6b44d5 100644 --- a/src/modules/releases/autoReleaseNote.js +++ b/src/modules/releases/autoReleaseNote.js @@ -3,79 +3,92 @@ * @author xuexb */ -const {getReleaseByTag, createRelease} = require('../../github'); -const {updateRepo, getTags, getFirstCommitHash, getCommitLog} = require('../../utils'); +const { getTags, compareCommits, getReleaseByTag, createRelease } = require('../../github') const RELEASE_CHANGE_MAP = { - document: 'docs', - feature: 'feat', - bugfix: 'fix', - close: 'close' -}; + document: 'docs', + feature: 'feat', + bugfix: 'fix', + close: 'close' +} module.exports = on => { - on('create_tag', ({payload, repo}) => { - getReleaseByTag(payload, { - tag_name: payload.ref - }).then(() => {}, () => { - updateRepo({ - url: payload.repository.clone_url, - repo - }).then(repoDir => { - const tags = getTags({ - dir: repoDir - }); - const after = tags[0]; - const before = tags.length > 1 ? tags[1] : getFirstCommitHash({ - dir: repoDir - }); - const log = getCommitLog({ - dir: repoDir, - before, - after - }); + on('create_tag', async ({ payload, repo }) => { + const tag = await getReleaseByTag(payload, { + tag_name: payload.ref + }) + // 如果该 tag 存在则直接返回 + if (tag !== false) { + return + } + + // 创建 release note + try { + const tags = await getTags(payload) + const head = tags[0].name + const base = tags.length > 1 ? tags[1].name : tags[0].name + + const commitsLog = await compareCommits(payload, { + base, + head + }) - const hash = getCommitLog({ - dir: repoDir, - before, - after, - html_url: payload.repository.html_url, - hash: true - }); + const commits = commitsLog.commits + const changes = Object.keys(RELEASE_CHANGE_MAP).map(title => { + let data = [] + commits.map((commit) => { + if (commit.commit.message.indexOf(`${RELEASE_CHANGE_MAP[title]}:`) === 0) { + let message = commit.commit.message + // 处理 squash merge 的 commit message + // 后期看看有没有更好的解决办法? + if (message.indexOf('\n') !== -1) { + message = message.substr(0, message.indexOf('\n')) + } + data.push(`- ${message}, by @${commit.commit.author.name} <<${commit.commit.author.email}>>`) + } + }) + return { + title, + data + } + }).filter(v => v.data.length) - const changes = Object.keys(RELEASE_CHANGE_MAP).map(title => { - return { - title, - data: log.filter(log => log.indexOf(`- ${RELEASE_CHANGE_MAP[title]}:`) === 0) - } - }).filter(v => v.data.length); + const hashChanges = commits.map((commit) => { + let message = commit.commit.message + // 处理 squash merge 的 commit message + if (message.indexOf('\n') !== -1) { + message = message.substr(0, message.indexOf('\n')) + } + return `- [${commit.sha.substr(0, 7)}](${commit.html_url}) - ${message}, by @${commit.commit.author.name} <<${commit.commit.author.email}>>` + }) - let body = []; + let body = [] - if (changes.length) { - body.push('## Notable changes\n'); - changes.forEach(v => { - body.push([ - `- ${v.title}` - ]); + if (changes.length) { + body.push('## Notable changes\n') + changes.forEach(v => { + body.push([ + `- ${v.title}` + ]) - v.data.forEach(line => body.push(' ' + line)); - }); - } + v.data.forEach(line => body.push(' ' + line)) + }) + } - if (hash.length) { - body.push('\n## Commits\n'); - body = body.concat(hash); - } + if (hashChanges.length) { + body.push('\n## Commits\n') + body = body.concat(hashChanges) + } - if (body.length) { - createRelease(payload, { - tag_name: payload.ref, - name: `${payload.ref} @${payload.repository.owner.login}`, - body: body.join('\n') - }); - } - }).catch(err => console.error(err)); - }); - }); + if (body.length) { + createRelease(payload, { + tag_name: payload.ref, + name: `${payload.ref} @${payload.repository.owner.login}`, + body: body.join('\n') + }) + } + } catch (err) { + console.error(err) + } + }) } diff --git a/src/utils.js b/src/utils.js index 6ea42a7..c1f7afd 100755 --- a/src/utils.js +++ b/src/utils.js @@ -3,168 +3,167 @@ * @author xuexb */ -const format = require('string-template'); -const path = require('path'); -const fs = require('fs'); -const crypto = require('crypto'); -const {fixedTimeComparison} = require('cryptiles'); -const {execSync, exec} = require('child_process'); -const gitPullOrClone = require('git-pull-or-clone'); +const format = require('string-template') +const path = require('path') +const fs = require('fs') +const crypto = require('crypto') +const { fixedTimeComparison } = require('cryptiles') +const { execSync } = require('child_process') +const gitPullOrClone = require('git-pull-or-clone') const utils = { - /** - * 验证请求 - * - * @param {Object} request req - * - * @return {boolean} - */ - 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']); - }, - - /** - * 目录是否存在 - * - * @param {string} file 路径 - * - * @return {boolean} - */ - isDirectory(file) { - try { - return fs.statSync(file).isDirectory(); - } - catch (e) { - if (e.code !== 'ENOENT') { - throw e; - } - - return false; - } - }, - - /** - * 获取本地 git 目录的第一个提交,主要处理当第一次给项目打标签时不能用2个标签去 .. - * - * @param {string} options.dir git 目录 - * - * @return {string} - */ - getFirstCommitHash({dir}) { - return execSync(`cd ${dir} && git log --oneline --pretty=format:"%h"`).toString() - .split(/\n+/).slice(-1)[0]; - }, - - /** - * 获取本地 git 目录的日志 - * - * @param {Object} options 配置数据 - * @param {string} options.dir git 目录 - * @param {string} options.before 开始版本号 - * @param {string} options.after 结束版本号 - * @param {string} options.html_url 预览的html地址, 用来拼 hash commit - * @param {boolean} [options.hash=false] 是否携带 commit hash log - * - * @return {Array} - */ - getCommitLog(options) { - const shell = [ - 'cd {dir}', - options.hash - ? 'git log {before}..{after} --no-merges --pretty=format:"- [%h]({html_url}/commit/%H) - %s, by @%aN <<%ae>>"' - : 'git log {before}..{after} --no-merges --pretty=format:"- %s, by @%aN <<%ae>>"' - ].join(' && '); - - return execSync(format(shell, options)).toString().split(/\n+/); - }, - - /** - * 更新 github 项目 - * - * @param {string} options.repo 项目名 - * - * @return {Promise} - */ - updateRepo({url, repo}) { - const repoDir = path.resolve(__dirname, '../github/', repo); - - return new Promise((resolve, reject) => { - gitPullOrClone(url, repoDir, err => { - if (err) { - return reject(err); - } - - resolve(repoDir); - }); - }); - }, - - /** - * 获取本地 git 目录的 tag 列表 - * - * @param {Object} options 配置 - * @param {string} options.dir git 目录 - * - * @return {Array} - */ - getTags(options) { - const shell = [ - 'cd {dir}', - 'git describe --tags `git rev-list --tags --abbrev=0` --abbrev=0 | uniq' - ].join(' && '); - - return execSync(format(shell, options)).toString().split(/\n+/).filter(tag => !!tag); - }, - - /** - * 获取 package.json 里的 config.github-bot - * - * @return {Object} - */ - getPkgConfig() { - const pkg = require('../package.json'); - const config = Object.assign({ - 'github-bot': {} - }, 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]; + /** + * 验证请求 + * + * @param {Object} request req + * + * @return {boolean} + */ + 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']) + }, + + /** + * 目录是否存在 + * + * @param {string} file 路径 + * + * @return {boolean} + */ + isDirectory (file) { + try { + return fs.statSync(file).isDirectory() + } catch (e) { + if (e.code !== 'ENOENT') { + throw e + } + + return false + } + }, + + /** + * 获取本地 git 目录的第一个提交,主要处理当第一次给项目打标签时不能用2个标签去 .. + * + * @param {string} options.dir git 目录 + * + * @return {string} + */ + getFirstCommitHash ({ dir }) { + return execSync(`cd ${dir} && git log --oneline --pretty=format:"%h"`).toString() + .split(/\n+/).slice(-1)[0] + }, + + /** + * 获取本地 git 目录的日志 + * + * @param {Object} options 配置数据 + * @param {string} options.dir git 目录 + * @param {string} options.before 开始版本号 + * @param {string} options.after 结束版本号 + * @param {string} options.html_url 预览的html地址, 用来拼 hash commit + * @param {boolean} [options.hash=false] 是否携带 commit hash log + * + * @return {Array} + */ + getCommitLog (options) { + const shell = [ + 'cd {dir}', + options.hash + ? 'git log {before}..{after} --no-merges --pretty=format:"- [%h]({html_url}/commit/%H) - %s, by @%aN <<%ae>>"' + : 'git log {before}..{after} --no-merges --pretty=format:"- %s, by @%aN <<%ae>>"' + ].join(' && ') + + return execSync(format(shell, options)).toString().split(/\n+/) + }, + + /** + * 更新 github 项目 + * + * @param {string} options.repo 项目名 + * + * @return {Promise} + */ + updateRepo ({ url, repo }) { + const repoDir = path.resolve(__dirname, '../github/', repo) + + return new Promise((resolve, reject) => { + gitPullOrClone(url, repoDir, err => { + if (err) { + return reject(err) } - return str; + resolve(repoDir) + }) + }) + }, + + /** + * 获取本地 git 目录的 tag 列表 + * + * @param {Object} options 配置 + * @param {string} options.dir git 目录 + * + * @return {Array} + */ + getTags (options) { + const shell = [ + 'cd {dir}', + 'git describe --tags `git rev-list --tags --abbrev=0` --abbrev=0 | uniq' + ].join(' && ') + + return execSync(format(shell, options)).toString().split(/\n+/).filter(tag => !!tag) + }, + + /** + * 获取 package.json 里的 config.github-bot + * + * @return {Object} + */ + getPkgConfig () { + const pkg = require('../package.json') + const config = Object.assign({ + 'github-bot': {} + }, 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] } -}; -module.exports = utils; + return str + } +} + +module.exports = utils