(_需要在 .env 里配置_)
80 |
81 | ### 2. 创建 webhook
82 |
83 | https://github.com/用户名/项目名/settings/hooks/new
84 |
85 | - Payload URL: www.example.com:8000
86 | - Content type: application/json
87 | - trigger: Send me everything.
88 | - Secret: xxx (_需要在 .env 里配置_)
89 |
90 | ### 3. 开发运行
91 |
92 | ```bash
93 | npm install
94 | cp env .env
95 | vim .env
96 | npm start
97 | ```
98 |
99 | ### 4. 部署
100 |
101 | 本项目使用 [pm2](https://github.com/Unitech/pm2) 进行服务管理,发布前请先全局安装 [pm2](https://github.com/Unitech/pm2)
102 |
103 | ```bash
104 | npm install pm2 -g
105 | npm run deploy
106 | ```
107 |
108 | 后台启动该服务后,可以通过 `pm2 ls` 来查看服务名称为 `github-bot` 的运行状态。具体 [pm2](https://github.com/Unitech/pm2) 使用,请访问:https://github.com/Unitech/pm2
109 |
110 | ### 5. 日志系统说明
111 |
112 | 本系统 `logger` 服务基于 [log4js](https://github.com/log4js-node/log4js-node)。
113 | 在根目录的 `.env` 文件中有个参数 `LOG_TYPE` 默认为 `console`,参数值说明:
114 |
115 | ```
116 | console - 通过 console 输出log。
117 | file - 将所有相关log输出到更根目录的 `log` 文件夹中。
118 | ```
119 |
120 | ## contributors
121 |
122 | > [用户贡献指南](.github/CONTRIBUTING.md)
123 |
124 | - [@yugasun](https://github.com/yugasun/)
125 | - [@ddhhz](https://github.com/ddhhz)
126 | - [@xuexb](https://github.com/xuexb/)
127 |
128 | ## Liscense
129 |
130 | MIT
131 |
--------------------------------------------------------------------------------
/create-issue.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 创建 issue - github-bot
6 |
7 |
8 |
109 |
110 |
111 |
112 |
113 | 接受任何建议和意见,请认真填写下面表单,感谢您的反馈!
114 |
115 |
144 |
145 |
146 |
147 |
198 |
199 |
200 |
--------------------------------------------------------------------------------
/env:
--------------------------------------------------------------------------------
1 | # rename this file to .env
2 |
3 | # Bot's personal access tokens, get from https://github.com/settings/tokens
4 | GITHUB_TOKEN=token
5 |
6 | # Webhook secret token, see https://developer.github.com/webhooks/securing/
7 | GITHUB_SECRET_TOKEN=secret
8 |
9 | # Logger type: default is console, if you want write log to file, set 'file'
10 | LOG_TYPE=console
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github-bot",
3 | "description": "Github bot",
4 | "version": "0.0.1",
5 | "main": "src/app.js",
6 | "scripts": {
7 | "start": "NODE_ENV=development node src/app",
8 | "lint": "eslint src/**/*.js test/**/*.js --quiet",
9 | "deploy": "pm2 start src/app.js --name=github-bot",
10 | "precommit": "npm run lint",
11 | "commitmsg": "validate-commit-msg",
12 | "test:watch": "npm run test -- --watch",
13 | "test:cov": "istanbul cover node_modules/mocha/bin/_mocha -- -t 5000 --recursive -R spec test/",
14 | "test": "mocha --reporter spec --timeout 5000 --recursive test/"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/xuexb/github-bot.git"
19 | },
20 | "author": "xuexb ",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/xuexb/github-bot/issues"
24 | },
25 | "homepage": "https://github.com/xuexb/github-bot#readme",
26 | "dependencies": {
27 | "cryptiles": "3.1.2",
28 | "crypto": "^1.0.1",
29 | "dotenv": "^4.0.0",
30 | "github": "^11.0.0",
31 | "koa": "^2.3.0",
32 | "koa-bodyparser": "^4.2.0",
33 | "log4js": "^2.3.10",
34 | "require-dir": "^0.3.2",
35 | "string-template": "^1.0.0"
36 | },
37 | "engines": {
38 | "node": ">= 7.8.0"
39 | },
40 | "config": {
41 | "validate-commit-msg": {
42 | "types": [
43 | "feat",
44 | "fix",
45 | "docs",
46 | "style",
47 | "refactor",
48 | "test",
49 | "chore",
50 | "revert",
51 | "release",
52 | "close"
53 | ]
54 | },
55 | "github-bot": {
56 | "labelToAuthor": {
57 | "bug": "xuexb",
58 | "enhancement": "xuexb",
59 | "question": "xuexb"
60 | }
61 | }
62 | },
63 | "devDependencies": {
64 | "chai": "^4.1.2",
65 | "chai-as-promised": "^7.1.1",
66 | "eslint": "^4.9.0",
67 | "eslint-config-standard": "^10.2.1",
68 | "eslint-friendly-formatter": "^3.0.0",
69 | "eslint-plugin-import": "^2.7.0",
70 | "eslint-plugin-node": "^5.2.0",
71 | "eslint-plugin-promise": "^3.6.0",
72 | "eslint-plugin-standard": "^3.0.1",
73 | "husky": "^0.14.3",
74 | "istanbul": ">=1.0.0-alpha.2",
75 | "mocha": "^4.0.1",
76 | "mock-require": "^2.0.2",
77 | "sinon": "^4.0.2",
78 | "sinon-chai": "^2.14.0",
79 | "validate-commit-msg": "^2.14.0"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file github-bot 入口文件
3 | * @author xuexb
4 | */
5 |
6 | require('dotenv').config()
7 |
8 | const EventEmitter = require('events')
9 | const Koa = require('koa')
10 | const bodyParser = require('koa-bodyparser')
11 | const requireDir = require('require-dir')
12 | const { verifySignature } = require('./utils')
13 | const issueActions = requireDir('./modules/issues')
14 | const pullRequestActions = requireDir('./modules/pull_request')
15 | const releasesActions = requireDir('./modules/releases')
16 | const app = new Koa()
17 | const githubEvent = new EventEmitter()
18 | const { appLog, accessLog } = require('./logger')
19 |
20 | app.use(bodyParser())
21 |
22 | app.use(ctx => {
23 | let eventName = ctx.request.headers['x-github-event']
24 | if (eventName && verifySignature(ctx.request)) {
25 | const payload = ctx.request.body
26 | const action = payload.action || payload.ref_type
27 |
28 | if (action) {
29 | eventName += `_${action}`
30 | }
31 |
32 | accessLog.info(`receive event: ${eventName}`)
33 |
34 | githubEvent.emit(eventName, {
35 | repo: payload.repository.name,
36 | payload
37 | })
38 |
39 | ctx.body = 'Ok.'
40 | } else {
41 | ctx.body = 'Go away.'
42 | }
43 | })
44 |
45 | const actions = Object.assign({}, issueActions, pullRequestActions, releasesActions)
46 | Object.keys(actions).forEach((key) => {
47 | actions[key](githubEvent.on.bind(githubEvent))
48 | appLog.info(`bind ${key} success!`)
49 | })
50 |
51 | const port = 8000
52 | app.listen(port)
53 | appLog.info('Listening on http://0.0.0.0:', port)
54 |
--------------------------------------------------------------------------------
/src/github.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file github 操作库
3 | * @author xuexb
4 | */
5 |
6 | /* eslint-disable camelcase */
7 | const GitHub = require('github')
8 | const { toArray } = require('./utils')
9 | const { appLog } = require('./logger')
10 |
11 | const github = new GitHub({
12 | debug: process.env.NODE_ENV === 'development'
13 | })
14 |
15 | github.authenticate({
16 | type: 'token',
17 | token: process.env.GITHUB_TOKEN
18 | })
19 |
20 | module.exports = {
21 | /**
22 | * issue 是否包含某 label
23 | *
24 | * @param {Object} payload data
25 | * @param {string} body 评论内容
26 | * @return {boolean}
27 | */
28 | async issueHasLabel (payload, label) {
29 | const owner = payload.repository.owner.login
30 | const repo = payload.repository.name
31 | const number = payload.issue.number
32 |
33 | try {
34 | const res = await github.issues.getIssueLabels({
35 | owner,
36 | repo,
37 | number
38 | })
39 | return res.data.map(v => v.name).indexOf(label) > -1
40 | } catch (e) {
41 | appLog.error(new Error(e))
42 | return false
43 | }
44 | },
45 |
46 | /**
47 | * PR 是否包含某 label
48 | *
49 | * @param {Object} payload data
50 | * @param {string} body 评论内容
51 | * @return {boolean}
52 | */
53 | async pullRequestHasLabel (payload, label) {
54 | const owner = payload.repository.owner.login
55 | const repo = payload.repository.name
56 | const number = payload.pull_request.number
57 |
58 | try {
59 | const res = await github.issues.getIssueLabels({
60 | owner,
61 | repo,
62 | number
63 | })
64 | return res.data.map(v => v.name).indexOf(label) > -1
65 | } catch (e) {
66 | appLog.error(new Error(e))
67 | return false
68 | }
69 | },
70 |
71 | /**
72 | * 评论 issue
73 | *
74 | * @param {Object} payload data
75 | * @param {string} body 评论内容
76 | * @return {boolean} 是否成功
77 | */
78 | async commentIssue (payload, body) {
79 | const owner = payload.repository.owner.login
80 | const repo = payload.repository.name
81 | const number = payload.issue.number
82 |
83 | try {
84 | await github.issues.createComment({
85 | owner,
86 | repo,
87 | number,
88 | body
89 | })
90 | return true
91 | } catch (e) {
92 | appLog.error(new Error(e))
93 | return false
94 | }
95 | },
96 |
97 | /**
98 | * 评论 PR
99 | *
100 | * @param {Object} payload data
101 | * @param {string} body 评论内容
102 | * @return {boolean} 是否成功
103 | */
104 | async commentPullRequest (payload, body) {
105 | const owner = payload.repository.owner.login
106 | const repo = payload.repository.name
107 | const number = payload.pull_request.number
108 |
109 | try {
110 | await github.issues.createComment({
111 | owner,
112 | repo,
113 | number,
114 | body
115 | })
116 | return true
117 | } catch (e) {
118 | appLog.error(new Error(e))
119 | return false
120 | }
121 | },
122 |
123 | /**
124 | * 关闭 issue
125 | *
126 | * @param {Object} payload data
127 | * @return {boolean} 是否成功
128 | */
129 | async closeIssue (payload) {
130 | const owner = payload.repository.owner.login
131 | const repo = payload.repository.name
132 | const number = payload.issue.number
133 |
134 | try {
135 | await github.issues.edit({
136 | owner,
137 | repo,
138 | number,
139 | state: 'closed'
140 | })
141 | return true
142 | } catch (e) {
143 | appLog.error(new Error(e))
144 | return false
145 | }
146 | },
147 |
148 | /**
149 | * 分派作者到 issues
150 | *
151 | * @param {Object} payload data
152 | * @param {string | Array} assign 用户id
153 | * @return {boolean} 是否成功
154 | */
155 | async addAssigneesToIssue (payload, assign) {
156 | const owner = payload.repository.owner.login
157 | const repo = payload.repository.name
158 | const number = payload.issue.number
159 |
160 | try {
161 | await github.issues.edit({
162 | owner,
163 | repo,
164 | number,
165 | assignees: toArray(assign)
166 | })
167 | return true
168 | } catch (e) {
169 | appLog.error(new Error(e))
170 | return false
171 | }
172 | },
173 |
174 | /**
175 | * 添加标签到 issue
176 | *
177 | * @param {Object} payload data
178 | * @param {string | Array} labels 标签
179 | * @return {boolean} 是否成功
180 | */
181 | async addLabelsToIssue (payload, labels) {
182 | const owner = payload.repository.owner.login
183 | const repo = payload.repository.name
184 | const number = payload.issue.number
185 |
186 | try {
187 | await github.issues.addLabels({
188 | owner,
189 | repo,
190 | number,
191 | labels: toArray(labels)
192 | })
193 | return true
194 | } catch (e) {
195 | appLog.error(new Error(e))
196 | return false
197 | }
198 | },
199 |
200 | /**
201 | * 添加标签到 PR
202 | *
203 | * @param {Object} payload data
204 | * @param {string | Array} labels 标签
205 | * @return {boolean} 是否成功
206 | */
207 | async addLabelsToPullRequest (payload, labels) {
208 | const owner = payload.repository.owner.login
209 | const repo = payload.repository.name
210 | const number = payload.pull_request.number
211 |
212 | try {
213 | await github.issues.addLabels({
214 | owner,
215 | repo,
216 | number,
217 | labels: toArray(labels)
218 | })
219 | return true
220 | } catch (e) {
221 | appLog.error(new Error(e))
222 | return false
223 | }
224 | },
225 |
226 | /**
227 | * 删除 PR 标签
228 | *
229 | * @param {Object} payload data
230 | * @param {string} name 标签名
231 | * @return {boolean} 是否成功
232 | */
233 | async removeLabelsToPullRequest (payload, name) {
234 | const owner = payload.repository.owner.login
235 | const repo = payload.repository.name
236 | const number = payload.pull_request.number
237 |
238 | try {
239 | await github.issues.removeLabel({
240 | owner,
241 | repo,
242 | number,
243 | name
244 | })
245 | return true
246 | } catch (e) {
247 | appLog.error(new Error(e))
248 | return false
249 | }
250 | },
251 |
252 | /**
253 | * 删除 issue 标签
254 | *
255 | * @param {Object} payload data
256 | * @param {string} name 标签名
257 | * @return {boolean} 是否成功
258 | */
259 | async removeLabelsToIssue (payload, name) {
260 | const owner = payload.repository.owner.login
261 | const repo = payload.repository.name
262 | const number = payload.issue.number
263 | try {
264 | await github.issues.removeLabel({
265 | owner,
266 | repo,
267 | number,
268 | name
269 | })
270 | return true
271 | } catch (e) {
272 | appLog.error(new Error(e))
273 | return false
274 | }
275 | },
276 |
277 | /**
278 | * 创建发布
279 | *
280 | * @param {Object} payload data
281 | * @param {string} options.tag_name tag名
282 | * @param {string} options.target_commitish tag hash
283 | * @param {string} options.name 标题
284 | * @param {string} options.body 内容
285 | * @param {boolean} options.draft 是否为草稿
286 | * @param {boolean} options.prerelease 是否预发布
287 | * @return {boolean} 是否成功
288 | */
289 | async createRelease (payload, { tag_name, target_commitish, name, body, draft, prerelease } = {}) {
290 | const owner = payload.repository.owner.login
291 | const repo = payload.repository.name
292 | try {
293 | await github.repos.createRelease({
294 | owner,
295 | repo,
296 | tag_name,
297 | target_commitish,
298 | name,
299 | body,
300 | draft,
301 | prerelease
302 | })
303 | return true
304 | } catch (e) {
305 | appLog.error(new Error(e))
306 | return false
307 | }
308 | },
309 |
310 | /**
311 | * 根据tag获取发布信息
312 | *
313 | * @param {Object} payload data
314 | * @param {string} options.tag_name tag名
315 | *
316 | * @return {Object | null}
317 | */
318 | async getReleaseByTag (payload, { tag_name } = {}) {
319 | const owner = payload.repository.owner.login
320 | const repo = payload.repository.name
321 | try {
322 | const res = await github.repos.getReleaseByTag({
323 | owner,
324 | repo,
325 | tag: tag_name
326 | })
327 | return res.data
328 | } catch (e) {
329 | appLog.error(new Error(e))
330 | return null
331 | }
332 | },
333 |
334 | /**
335 | * 创建 review 请求
336 | *
337 | * @param {Object} payload data
338 | * @param {Array | string} options.reviewers reviewer
339 | * @param {Array | string} options.team_reviewers team_reviewers
340 | *
341 | * @return {boolean} 是否成功
342 | */
343 | async createReviewRequest (payload, { reviewers, team_reviewers } = {}) {
344 | const owner = payload.repository.owner.login
345 | const repo = payload.repository.name
346 | const number = payload.pull_request.number
347 | try {
348 | await github.pullRequests.createReviewRequest({
349 | owner,
350 | repo,
351 | number,
352 | reviewers: toArray(reviewers),
353 | team_reviewers: toArray(team_reviewers)
354 | })
355 | return true
356 | } catch (e) {
357 | appLog.error(new Error(e))
358 | return false
359 | }
360 | },
361 |
362 | /**
363 | * 获得 repo 所有的tag
364 | *
365 | * @param {any} payload data
366 | * @return {Array}
367 | */
368 | async getTags (payload) {
369 | const owner = payload.repository.owner.login
370 | const repo = payload.repository.name
371 | try {
372 | const res = await github.repos.getTags({
373 | owner,
374 | repo
375 | })
376 | return res.data
377 | } catch (e) {
378 | appLog.error(new Error(e))
379 | return []
380 | }
381 | },
382 |
383 | /**
384 | * 对比2个提交
385 | *
386 | * @param {Object} payload data
387 | * @param {string} options.base 基点
388 | * @param {string} options.head diff
389 | * @return {Array | null}
390 | */
391 | async compareCommits (payload, { base, head } = {}) {
392 | const owner = payload.repository.owner.login
393 | const repo = payload.repository.name
394 | try {
395 | const res = await github.repos.compareCommits({
396 | owner,
397 | repo,
398 | base,
399 | head
400 | })
401 | return res.data
402 | } catch (e) {
403 | appLog.error(new Error(e))
404 | return null
405 | }
406 | }
407 | }
408 |
--------------------------------------------------------------------------------
/src/logger.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created Date: Wednesday, November 1st 2017, 4:03:55 pm
3 | * Author: yugasun
4 | * Email: yuga.sun.bj@gmail.com
5 | * -----
6 | * Last Modified:
7 | * Modified By:
8 | * -----
9 | * Copyright (c) 2017 yugasun
10 | */
11 |
12 | const log4js = require('log4js')
13 |
14 | const fileConfig = {
15 | pm2: true,
16 | appenders: {
17 | app: {
18 | 'type': 'file',
19 | 'filename': 'log/app.log',
20 | 'maxLogSize': 10485760,
21 | 'numBackups': 3
22 | },
23 | access: {
24 | 'type': 'dateFile',
25 | 'filename': 'log/access.log',
26 | 'pattern': '-yyyy-MM-dd',
27 | 'category': 'http'
28 | },
29 | errorFile: { type: 'file', filename: 'log/errors.log' },
30 | errors: {
31 | 'type': 'logLevelFilter',
32 | 'level': 'error',
33 | 'appender': 'errorFile'
34 | }
35 | },
36 | categories: {
37 | default: { appenders: ['app', 'errors'], level: 'trace' },
38 | http: { appenders: ['access'], level: 'info' }
39 | }
40 | }
41 | const consoleConfig = {
42 | pm2: true,
43 | appenders: {
44 | console: { type: 'console' }
45 | },
46 | categories: {
47 | default: { appenders: ['console'], level: 'info' },
48 | http: { appenders: ['console'], level: 'info' }
49 | }
50 | }
51 |
52 | const config = process.env.LOG_TYPE === 'file' ? fileConfig : consoleConfig
53 |
54 | log4js.configure(config)
55 |
56 | const appLog = log4js.getLogger('app')
57 | const accessLog = log4js.getLogger('http')
58 |
59 | module.exports = {
60 | appLog,
61 | accessLog
62 | }
63 |
--------------------------------------------------------------------------------
/src/modules/issues/autoAssign.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file issue 自动 `assign` 给指定人员
3 | * @author xuexb
4 | */
5 |
6 | const { getPkgConfig } = require('../../utils')
7 | const { addAssigneesToIssue } = require('../../github')
8 |
9 | const config = getPkgConfig()
10 | const assignMap = config.labelToAuthor || {}
11 |
12 | function autoAssign (on) {
13 | on('issues_labeled', ({ payload, repo }) => {
14 | if (assignMap[payload.label.name]) {
15 | addAssigneesToIssue(
16 | payload,
17 | assignMap[payload.label.name]
18 | )
19 | }
20 | })
21 | }
22 |
23 | module.exports = autoAssign
24 |
--------------------------------------------------------------------------------
/src/modules/issues/autoLabel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 自动根据创建的 issue 内标识创建对应 label
3 | * @author xuexb
4 | */
5 |
6 | const { addLabelsToIssue } = require('../../github')
7 |
8 | function autoAssign (on) {
9 | on('issues_opened', ({ payload, repo }) => {
10 | const label = (payload.issue.body.match(//) || [])[1]
11 | if (label) {
12 | addLabelsToIssue(payload, label)
13 | }
14 | })
15 | }
16 |
17 | module.exports = autoAssign
18 |
--------------------------------------------------------------------------------
/src/modules/issues/replyInvalid.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 不规范issue则自动关闭
3 | * @author xuexb
4 | */
5 |
6 | const format = require('string-template')
7 | const {
8 | commentIssue,
9 | closeIssue,
10 | addLabelsToIssue
11 | } = require('../../github')
12 |
13 | const comment = [
14 | 'hi @{user},非常感谢您的反馈,',
15 | '但是由于您没有使用 [创建 issue](https://xuexb.github.io/github-bot/create-issue.html) 页面提交, 将直接被关闭, 谢谢!'
16 | ].join('')
17 |
18 | function replyInvalid (on) {
19 | on('issues_opened', ({ payload }) => {
20 | const issue = payload.issue
21 | const opener = issue.user.login
22 |
23 | if (issue.body.indexOf('') === -1) {
24 | commentIssue(
25 | payload,
26 | format(comment, {
27 | user: opener
28 | })
29 | )
30 |
31 | closeIssue(payload)
32 | addLabelsToIssue(payload, 'invalid')
33 | }
34 | })
35 | }
36 |
37 | module.exports = replyInvalid
38 |
--------------------------------------------------------------------------------
/src/modules/issues/replyNeedDemo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 当有 need demo 标签时自动回复需要相关预览链接
3 | * @author xuexb
4 | */
5 |
6 | const format = require('string-template')
7 | const { commentIssue } = require('../../github')
8 |
9 | const comment = 'hi @{user},请提供一个可预览的链接,如: '
10 |
11 | function replyNeedDemo (on) {
12 | on('issues_labeled', ({ payload, repo }) => {
13 | if (payload.label.name === 'need demo') {
14 | commentIssue(
15 | payload,
16 | format(comment, {
17 | user: payload.issue.user.login
18 | })
19 | )
20 | }
21 | })
22 | }
23 |
24 | module.exports = replyNeedDemo
25 |
--------------------------------------------------------------------------------
/src/modules/pull_request/autoReviewRequest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file PR 自动根据 tag 去添加 reviewer
3 | * @author xuexb
4 | */
5 |
6 | const { getPkgConfig } = require('../../utils')
7 | const { createReviewRequest } = require('../../github')
8 |
9 | const config = getPkgConfig()
10 | const assignMap = config.labelToAuthor || {}
11 |
12 | module.exports = on => {
13 | on('pull_request_labeled', ({ payload, repo }) => {
14 | if (assignMap[payload.label.name]) {
15 | createReviewRequest(
16 | payload,
17 | {
18 | reviewers: assignMap[payload.label.name]
19 | }
20 | )
21 | }
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/src/modules/pull_request/replyInvalidTitle.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file PR 提示标题正确性
3 | * @author xuexb
4 | */
5 |
6 | const format = require('string-template')
7 | const { getPkgCommitPrefix } = require('../../utils')
8 | const {
9 | commentPullRequest,
10 | addLabelsToPullRequest,
11 | removeLabelsToPullRequest,
12 | pullRequestHasLabel
13 | } = require('../../github')
14 |
15 | const actions = getPkgCommitPrefix()
16 | const match = title => {
17 | return actions.some(action => title.indexOf(`${action}:`) === 0)
18 | }
19 |
20 | const commentSuccess = [
21 | 'hi @{user},非常感谢您及时修正标题格式,祝您玩的开心!'
22 | ].join('')
23 |
24 | const commentError = [
25 | 'hi @{user},非常感谢您的 PR ,',
26 | '但是您没有使用 [PR 标题规则](https://github.com/xuexb/github-bot#commit-log-和-pr-标题规则) 格式,',
27 | '请及时修改, 谢谢!'
28 | ].join('')
29 |
30 | module.exports = on => {
31 | if (actions.length) {
32 | on('pull_request_opened', ({ payload, repo }) => {
33 | if (!match(payload.pull_request.title)) {
34 | commentPullRequest(
35 | payload,
36 | format(commentError, {
37 | user: payload.pull_request.user.login
38 | })
39 | )
40 |
41 | addLabelsToPullRequest(payload, 'invalid')
42 | }
43 | })
44 |
45 | on('pull_request_edited', async ({ payload, repo }) => {
46 | if (match(payload.pull_request.title) && await pullRequestHasLabel(payload, 'invalid')) {
47 | commentPullRequest(
48 | payload,
49 | format(commentSuccess, {
50 | user: payload.pull_request.user.login
51 | })
52 | )
53 |
54 | removeLabelsToPullRequest(payload, 'invalid')
55 | }
56 | })
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/modules/pull_request/titlePrefixToLabel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file PR 标题自动打标签
3 | * @author xuexb
4 | */
5 |
6 | const {
7 | addLabelsToPullRequest,
8 | pullRequestHasLabel
9 | } = require('../../github')
10 |
11 | const getAction = title => {
12 | return (title.match(/^(\w+?):/) || [])[1]
13 | }
14 |
15 | const ACTION_TO_LABEL_MAP = {
16 | feat: 'enhancement',
17 | fix: 'bug',
18 | docs: 'document'
19 | }
20 |
21 | const handle = async ({ payload, repo }) => {
22 | const action = getAction(payload.pull_request.title)
23 | if (action && ACTION_TO_LABEL_MAP[action]) {
24 | const exist = await pullRequestHasLabel(payload, ACTION_TO_LABEL_MAP[action])
25 | if (!exist) {
26 | addLabelsToPullRequest(payload, ACTION_TO_LABEL_MAP[action])
27 | }
28 | }
29 | }
30 |
31 | module.exports = on => {
32 | on('pull_request_edited', handle)
33 | on('pull_request_opened', handle)
34 | }
35 |
--------------------------------------------------------------------------------
/src/modules/releases/autoReleaseNote.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 根据 tag 自动 release
3 | * @author xuexb
4 | */
5 |
6 | const {
7 | getTags,
8 | compareCommits,
9 | getReleaseByTag,
10 | createRelease
11 | } = require('../../github')
12 |
13 | const RELEASE_CHANGE_MAP = {
14 | document: 'docs',
15 | feature: 'feat',
16 | bugfix: 'fix',
17 | close: 'close'
18 | }
19 |
20 | module.exports = on => {
21 | on('create_tag', async ({ payload, repo }) => {
22 | const tag = await getReleaseByTag(payload, {
23 | tag_name: payload.ref
24 | })
25 | // 如果该 tag 存在则直接返回
26 | if (tag !== null) {
27 | return
28 | }
29 |
30 | const tags = await getTags(payload)
31 |
32 | // 如果只有一个 tag 则没法对比,忽略
33 | if (tags.length < 2) {
34 | return
35 | }
36 |
37 | const head = tags[0].name
38 | const base = tags[1].name
39 |
40 | const commitsLog = await compareCommits(payload, {
41 | base,
42 | head
43 | })
44 |
45 | const commits = commitsLog.commits
46 | const changes = Object.keys(RELEASE_CHANGE_MAP).map(title => {
47 | return {
48 | title,
49 | data: commits
50 | .filter((commit) => commit.commit.message.indexOf(`${RELEASE_CHANGE_MAP[title]}:`) === 0)
51 | .map((commit) => {
52 | let message = commit.commit.message
53 | // 处理 squash merge 的 commit message
54 | if (message.indexOf('\n') !== -1) {
55 | message = message.substr(0, message.indexOf('\n'))
56 | }
57 | return `- ${message}, by @${commit.author.login} <<${commit.commit.author.email}>>`
58 | })
59 | }
60 | }).filter(v => v.data.length)
61 |
62 | const hashChanges = commits.map((commit) => {
63 | let message = commit.commit.message
64 | // 处理 squash merge 的 commit message
65 | if (message.indexOf('\n') !== -1) {
66 | message = message.substr(0, message.indexOf('\n'))
67 | }
68 | return `- [${commit.sha.substr(0, 7)}](${commit.html_url}) - ${message}, by @${commit.author.login} <<${commit.commit.author.email}>>`
69 | })
70 |
71 | let body = []
72 |
73 | if (changes.length) {
74 | body.push('## Notable changes\n')
75 | changes.forEach(v => {
76 | body.push(`- ${v.title}`)
77 |
78 | v.data.forEach(line => body.push(' ' + line))
79 | })
80 | }
81 |
82 | if (hashChanges.length) {
83 | body.push('\n## Commits\n')
84 | body = body.concat(hashChanges)
85 | }
86 |
87 | if (body.length) {
88 | createRelease(payload, {
89 | tag_name: payload.ref,
90 | name: `${payload.ref} @${payload.repository.owner.login}`,
91 | body: body.join('\n')
92 | })
93 | }
94 | })
95 | }
96 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 工具集
3 | * @author xuexb
4 | */
5 | const crypto = require('crypto')
6 | const { fixedTimeComparison } = require('cryptiles')
7 |
8 | const utils = {
9 |
10 | /**
11 | * 验证请求
12 | *
13 | * @param {Object} request req
14 | *
15 | * @return {boolean}
16 | */
17 | verifySignature (request) {
18 | let signature = crypto.createHmac('sha1', process.env.GITHUB_SECRET_TOKEN)
19 | .update(request.rawBody)
20 | .digest('hex')
21 | signature = `sha1=${signature}`
22 | return fixedTimeComparison(signature, request.headers['x-hub-signature'])
23 | },
24 |
25 | /**
26 | * 获取 package.json 里的 config.github-bot
27 | *
28 | * @return {Object}
29 | */
30 | getPkgConfig () {
31 | const pkg = require('../package.json')
32 | const config = Object.assign({
33 | 'github-bot': {}
34 | }, pkg.config)
35 |
36 | return config['github-bot']
37 | },
38 |
39 | /**
40 | * 获取 commit log 前缀白名单
41 | *
42 | * @return {Array}
43 | */
44 | getPkgCommitPrefix () {
45 | const pkg = require('../package.json')
46 | const config = Object.assign({
47 | 'validate-commit-msg': {
48 | 'types': []
49 | }
50 | }, pkg.config)
51 |
52 | return config['validate-commit-msg'].types
53 | },
54 |
55 | /**
56 | * 转化成 Array
57 | *
58 | * @param {string | Array} str 目标值
59 | *
60 | * @return {Array}
61 | */
62 | toArray (str) {
63 | if (str) {
64 | return Array.isArray(str) ? str : [str]
65 | }
66 |
67 | return str
68 | }
69 | }
70 |
71 | module.exports = utils
72 |
--------------------------------------------------------------------------------
/test/github.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file github.js test case
3 | * @author xuexb
4 | */
5 |
6 | /* eslint-disable camelcase */
7 | const mock = require('mock-require')
8 | mock.stopAll()
9 | const chai = require('chai')
10 | const expect = chai.expect
11 | const chaiAsPromised = require('chai-as-promised')
12 | const clean = require('./utils/clean')
13 | chai.use(chaiAsPromised)
14 |
15 | const payload = {
16 | repository: {
17 | owner: {
18 | login: 'xuexb'
19 | },
20 | name: 'github-bot'
21 | },
22 | pull_request: {
23 | number: 1
24 | },
25 | issue: {
26 | number: 1
27 | }
28 | }
29 |
30 | const createClass = (constructor, prototype) => {
31 | function Class (...args) {
32 | if ('function' === typeof constructor) {
33 | constructor.apply(this, args)
34 | }
35 | }
36 | Object.assign(Class.prototype, {
37 | authenticate() {}
38 | }, prototype)
39 |
40 | return Class
41 | }
42 |
43 | const mockGithub = (...args) => {
44 | return mock('github', createClass(...args))
45 | }
46 |
47 | describe('github.js', () => {
48 | beforeEach('clear node cache', () => {
49 | clean('src/github')
50 | })
51 |
52 | describe('.issueHasLabel', () => {
53 | it('should be a method', () => {
54 | mockGithub(null, {
55 | issues: {
56 | getIssueLabels() {
57 | return Promise.resolve({
58 | data: []
59 | })
60 | }
61 | }
62 | })
63 | const github = require('../src/github')
64 | expect(github.issueHasLabel).to.be.a('function')
65 | })
66 |
67 | describe('should return boolean', () => {
68 | it('true', () => {
69 | mockGithub(null, {
70 | issues: {
71 | getIssueLabels() {
72 | return Promise.resolve({
73 | data: [
74 | {
75 | name: 'nofond'
76 | }
77 | ]
78 | })
79 | }
80 | }
81 | })
82 | const github = require('../src/github')
83 | expect(github.issueHasLabel(payload, 'nofond')).to.eventually.be.true
84 | })
85 |
86 | it('false', () => {
87 | mockGithub(null, {
88 | issues: {
89 | getIssueLabels() {
90 | return Promise.resolve({
91 | data: [
92 | {
93 | name: 'test'
94 | }
95 | ]
96 | })
97 | }
98 | }
99 | })
100 | const github = require('../src/github')
101 | expect(github.issueHasLabel(payload, 'nofond')).to.eventually.be.false
102 | })
103 |
104 | it('error', () => {
105 | mock('../src/logger', {
106 | appLog: {
107 | error(err) {
108 | expect(err).to.not.be.undefined
109 | }
110 | }
111 | })
112 | mockGithub(null)
113 | const github = require('../src/github')
114 | expect(github.issueHasLabel(payload, 'nofond')).to.eventually.be.false
115 | })
116 | })
117 |
118 | it('check param', () => {
119 | mockGithub(null, {
120 | issues: {
121 | getIssueLabels({owner, repo, number}) {
122 | expect(owner).to.equal('xuexb')
123 | expect(repo).to.equal('github-bot')
124 | expect(number).to.equal(1)
125 | return Promise.resolve({
126 | data: []
127 | })
128 | }
129 | }
130 | })
131 | const github = require('../src/github')
132 | return github.issueHasLabel(payload)
133 | })
134 |
135 | })
136 |
137 | describe('.pullRequestHasLabel', () => {
138 | it('should be a method', () => {
139 | mockGithub(null, {
140 | issues: {
141 | getIssueLabels() {
142 | return Promise.resolve({
143 | data: []
144 | })
145 | }
146 | }
147 | })
148 | const github = require('../src/github')
149 | expect(github.pullRequestHasLabel).to.be.a('function')
150 | })
151 |
152 | describe('should return boolean', () => {
153 | it('true', () => {
154 | mockGithub(null, {
155 | issues: {
156 | getIssueLabels() {
157 | return Promise.resolve({
158 | data: [
159 | {
160 | name: 'nofond'
161 | }
162 | ]
163 | })
164 | }
165 | }
166 | })
167 | const github = require('../src/github')
168 | expect(github.pullRequestHasLabel(payload, 'nofond')).to.eventually.be.true
169 | })
170 |
171 | it('false', () => {
172 | mockGithub(null, {
173 | issues: {
174 | getIssueLabels() {
175 | return Promise.resolve({
176 | data: [
177 | {
178 | name: 'test'
179 | }
180 | ]
181 | })
182 | }
183 | }
184 | })
185 | const github = require('../src/github')
186 | expect(github.pullRequestHasLabel(payload, 'nofond')).to.eventually.be.false
187 | })
188 |
189 | it('error', () => {
190 | mock('../src/logger', {
191 | appLog: {
192 | error(err) {
193 | expect(err).to.not.be.undefined
194 | }
195 | }
196 | })
197 | mockGithub(null)
198 | const github = require('../src/github')
199 | expect(github.pullRequestHasLabel(payload)).to.eventually.be.false
200 | })
201 | })
202 |
203 | it('check param', () => {
204 | mockGithub(null, {
205 | issues: {
206 | getIssueLabels({owner, repo, number}) {
207 | expect(owner).to.equal('xuexb')
208 | expect(repo).to.equal('github-bot')
209 | expect(number).to.equal(1)
210 | return Promise.resolve({
211 | data: []
212 | })
213 | }
214 | }
215 | })
216 | const github = require('../src/github')
217 | return github.pullRequestHasLabel(payload)
218 | })
219 | })
220 |
221 | describe('.commentIssue', () => {
222 | it('should be a method', () => {
223 | mockGithub(null, {
224 | issues: {
225 | createComment() {
226 | return Promise.resolve()
227 | }
228 | }
229 | })
230 | const github = require('../src/github')
231 | expect(github.commentIssue).to.be.a('function')
232 | })
233 |
234 | describe('should return boolean', () => {
235 | it('true', () => {
236 | mockGithub(null, {
237 | issues: {
238 | createComment() {
239 | return Promise.resolve()
240 | }
241 | }
242 | })
243 | const github = require('../src/github')
244 | expect(github.commentIssue(payload, 'message')).to.eventually.be.true
245 | })
246 |
247 | it('false', () => {
248 | mockGithub(null, {
249 | issues: {
250 | createComment() {
251 | throw new TypeError('error')
252 | }
253 | }
254 | })
255 | const github = require('../src/github')
256 | expect(github.commentIssue(payload)).to.eventually.be.false
257 | })
258 |
259 | it('error', () => {
260 | mock('../src/logger', {
261 | appLog: {
262 | error(err) {
263 | expect(err).to.not.be.undefined
264 | }
265 | }
266 | })
267 | mockGithub(null)
268 | const github = require('../src/github')
269 | expect(github.commentIssue(payload)).to.eventually.be.false
270 | })
271 | })
272 |
273 | it('check param', () => {
274 | mockGithub(null, {
275 | issues: {
276 | createComment({owner, repo, number}) {
277 | expect(owner).to.equal('xuexb')
278 | expect(repo).to.equal('github-bot')
279 | expect(number).to.equal(1)
280 | expect(body).to.equal('message')
281 | return Promise.resolve()
282 | }
283 | }
284 | })
285 | const github = require('../src/github')
286 | return github.commentIssue(payload, 'message')
287 | })
288 | })
289 |
290 | describe('.commentPullRequest', () => {
291 | it('should be a method', () => {
292 | mockGithub(null, {
293 | issues: {
294 | createComment() {
295 | return Promise.resolve()
296 | }
297 | }
298 | })
299 | const github = require('../src/github')
300 | expect(github.commentPullRequest).to.be.a('function')
301 | })
302 |
303 | describe('should return boolean', () => {
304 | it('true', () => {
305 | mockGithub(null, {
306 | issues: {
307 | createComment() {
308 | return Promise.resolve()
309 | }
310 | }
311 | })
312 | const github = require('../src/github')
313 | expect(github.commentPullRequest(payload, 'message')).to.eventually.be.true
314 | })
315 |
316 | it('false', () => {
317 | mockGithub(null, {
318 | issues: {
319 | createComment() {
320 | throw new TypeError('error')
321 | }
322 | }
323 | })
324 | const github = require('../src/github')
325 | expect(github.commentPullRequest(payload)).to.eventually.be.false
326 | })
327 |
328 | it('error', () => {
329 | mock('../src/logger', {
330 | appLog: {
331 | error(err) {
332 | expect(err).to.not.be.undefined
333 | }
334 | }
335 | })
336 | mockGithub(null)
337 | const github = require('../src/github')
338 | expect(github.commentPullRequest(payload)).to.eventually.be.false
339 | })
340 | })
341 |
342 | it('check param', () => {
343 | mockGithub(null, {
344 | issues: {
345 | createComment({owner, repo, number}) {
346 | expect(owner).to.equal('xuexb')
347 | expect(repo).to.equal('github-bot')
348 | expect(number).to.equal(1)
349 | expect(body).to.equal('message')
350 | return Promise.resolve()
351 | }
352 | }
353 | })
354 | const github = require('../src/github')
355 | return github.commentPullRequest(payload, 'message')
356 | })
357 | })
358 |
359 | describe('.closeIssue', () => {
360 | it('should be a method', () => {
361 | mockGithub(null, {
362 | issues: {
363 | edit() {
364 | return Promise.resolve()
365 | }
366 | }
367 | })
368 | const github = require('../src/github')
369 | expect(github.closeIssue).to.be.a('function')
370 | })
371 |
372 | describe('should return boolean', () => {
373 | it('true', () => {
374 | mockGithub(null, {
375 | issues: {
376 | edit() {
377 | return Promise.resolve()
378 | }
379 | }
380 | })
381 | const github = require('../src/github')
382 | expect(github.closeIssue(payload)).to.eventually.be.true
383 | })
384 |
385 | it('false', () => {
386 | mockGithub(null, {
387 | issues: {
388 | edit() {
389 | throw new TypeError('error')
390 | }
391 | }
392 | })
393 | const github = require('../src/github')
394 | expect(github.closeIssue(payload)).to.eventually.be.false
395 | })
396 |
397 | it('error', () => {
398 | mock('../src/logger', {
399 | appLog: {
400 | error(err) {
401 | expect(err).to.not.be.undefined
402 | }
403 | }
404 | })
405 | mockGithub(null)
406 | const github = require('../src/github')
407 | expect(github.closeIssue(payload)).to.eventually.be.false
408 | })
409 | })
410 |
411 | it('check param', () => {
412 | mockGithub(null, {
413 | issues: {
414 | edit({owner, repo, number, state}) {
415 | expect(owner).to.equal('xuexb')
416 | expect(repo).to.equal('github-bot')
417 | expect(number).to.equal(1)
418 | expect(state).to.equal('closed')
419 | return Promise.resolve()
420 | }
421 | }
422 | })
423 | const github = require('../src/github')
424 | return github.closeIssue(payload)
425 | })
426 | })
427 |
428 | describe('.addAssigneesToIssue', () => {
429 | it('should be a method', () => {
430 | mockGithub(null, {
431 | issues: {
432 | edit() {
433 | return Promise.resolve()
434 | }
435 | }
436 | })
437 | const github = require('../src/github')
438 | expect(github.addAssigneesToIssue).to.be.a('function')
439 | })
440 |
441 | describe('should return boolean', () => {
442 | it('true', () => {
443 | mockGithub(null, {
444 | issues: {
445 | edit() {
446 | return Promise.resolve()
447 | }
448 | }
449 | })
450 | const github = require('../src/github')
451 | expect(github.addAssigneesToIssue(payload)).to.eventually.be.true
452 | })
453 |
454 | it('false', () => {
455 | mockGithub(null, {
456 | issues: {
457 | edit() {
458 | throw new TypeError('error')
459 | }
460 | }
461 | })
462 | const github = require('../src/github')
463 | expect(github.addAssigneesToIssue(payload)).to.eventually.be.false
464 | })
465 |
466 | it('error', () => {
467 | mock('../src/logger', {
468 | appLog: {
469 | error(err) {
470 | expect(err).to.not.be.undefined
471 | }
472 | }
473 | })
474 | mockGithub(null, null)
475 | const github = require('../src/github')
476 | expect(github.addAssigneesToIssue(payload)).to.eventually.be.false
477 | })
478 | })
479 |
480 | it('check param', () => {
481 | mockGithub(null, {
482 | issues: {
483 | edit({owner, repo, number, state}) {
484 | expect(owner).to.equal('xuexb')
485 | expect(repo).to.equal('github-bot')
486 | expect(number).to.equal(1)
487 | expect(assignees).to.deep.equal(['ok'])
488 | return Promise.resolve()
489 | }
490 | }
491 | })
492 | const github = require('../src/github')
493 | return github.addAssigneesToIssue(payload, 'ok')
494 | })
495 | })
496 |
497 | describe('.addLabelsToIssue', () => {
498 | it('should be a method', () => {
499 | mockGithub(null, {
500 | issues: {
501 | addLabels() {
502 | return Promise.resolve()
503 | }
504 | }
505 | })
506 | const github = require('../src/github')
507 | expect(github.addLabelsToIssue).to.be.a('function')
508 | })
509 |
510 | describe('should return boolean', () => {
511 | it('true', () => {
512 | mockGithub(null, {
513 | issues: {
514 | addLabels() {
515 | return Promise.resolve()
516 | }
517 | }
518 | })
519 | const github = require('../src/github')
520 | expect(github.addLabelsToIssue(payload, 'label')).to.eventually.be.true
521 | })
522 |
523 | it('false', () => {
524 | mockGithub(null, {
525 | issues: {
526 | addLabels() {
527 | throw new TypeError('error')
528 | }
529 | }
530 | })
531 | const github = require('../src/github')
532 | expect(github.addLabelsToIssue(payload)).to.eventually.be.false
533 | })
534 |
535 | it('error', () => {
536 | mock('../src/logger', {
537 | appLog: {
538 | error(err) {
539 | expect(err).to.not.be.undefined
540 | }
541 | }
542 | })
543 | mockGithub(null)
544 | const github = require('../src/github')
545 | expect(github.addLabelsToIssue(payload)).to.eventually.be.false
546 | })
547 | })
548 |
549 | it('check param', () => {
550 | mockGithub(null, {
551 | issues: {
552 | addLabels({owner, repo, number, state}) {
553 | expect(owner).to.equal('xuexb')
554 | expect(repo).to.equal('github-bot')
555 | expect(number).to.equal(1)
556 | expect(assignees).to.deep.equal(['ok'])
557 | return Promise.resolve()
558 | }
559 | }
560 | })
561 | const github = require('../src/github')
562 | return github.addLabelsToIssue(payload, 'ok')
563 | })
564 | })
565 |
566 | describe('.addLabelsToPullRequest', () => {
567 | it('should be a method', () => {
568 | mockGithub(null, {
569 | issues: {
570 | addLabels() {
571 | return Promise.resolve()
572 | }
573 | }
574 | })
575 | const github = require('../src/github')
576 | expect(github.addLabelsToPullRequest).to.be.a('function')
577 | })
578 |
579 | describe('should return boolean', () => {
580 | it('true', () => {
581 | mockGithub(null, {
582 | issues: {
583 | addLabels() {
584 | return Promise.resolve()
585 | }
586 | }
587 | })
588 | const github = require('../src/github')
589 | expect(github.addLabelsToPullRequest(payload, 'label')).to.eventually.be.true
590 | })
591 |
592 | it('false', () => {
593 | mockGithub(null, {
594 | issues: {
595 | addLabels() {
596 | throw new TypeError('error')
597 | }
598 | }
599 | })
600 | const github = require('../src/github')
601 | expect(github.addLabelsToPullRequest(payload)).to.eventually.be.false
602 | })
603 |
604 | it('error', () => {
605 | mock('../src/logger', {
606 | appLog: {
607 | error(err) {
608 | expect(err).to.not.be.undefined
609 | }
610 | }
611 | })
612 | mockGithub(null)
613 | const github = require('../src/github')
614 | expect(github.addLabelsToPullRequest(payload)).to.eventually.be.false
615 | })
616 | })
617 |
618 | it('check param', () => {
619 | mockGithub(null, {
620 | issues: {
621 | addLabels({owner, repo, number, state}) {
622 | expect(owner).to.equal('xuexb')
623 | expect(repo).to.equal('github-bot')
624 | expect(number).to.equal(1)
625 | expect(assignees).to.deep.equal(['ok'])
626 | return Promise.resolve()
627 | }
628 | }
629 | })
630 | const github = require('../src/github')
631 | return github.addLabelsToPullRequest(payload, 'ok')
632 | })
633 | })
634 |
635 | describe('.removeLabelsToPullRequest', () => {
636 | it('should be a method', () => {
637 | mockGithub(null, {
638 | issues: {
639 | removeLabel() {
640 | return Promise.resolve()
641 | }
642 | }
643 | })
644 | const github = require('../src/github')
645 | expect(github.removeLabelsToPullRequest).to.be.a('function')
646 | })
647 |
648 | describe('should return boolean', () => {
649 | it('true', () => {
650 | mockGithub(null, {
651 | issues: {
652 | removeLabel() {
653 | return Promise.resolve()
654 | }
655 | }
656 | })
657 | const github = require('../src/github')
658 | expect(github.removeLabelsToPullRequest(payload, 'label')).to.eventually.be.true
659 | })
660 |
661 | it('false', () => {
662 | mockGithub(null, {
663 | issues: {
664 | removeLabel() {
665 | throw new TypeError('error')
666 | }
667 | }
668 | })
669 | const github = require('../src/github')
670 | expect(github.removeLabelsToPullRequest(payload)).to.eventually.be.false
671 | })
672 |
673 | it('error', () => {
674 | mock('../src/logger', {
675 | appLog: {
676 | error(err) {
677 | expect(err).to.not.be.undefined
678 | }
679 | }
680 | })
681 | mockGithub(null)
682 | const github = require('../src/github')
683 | expect(github.removeLabelsToPullRequest(payload)).to.eventually.be.false
684 | })
685 | })
686 |
687 | it('check param', () => {
688 | mockGithub(null, {
689 | issues: {
690 | removeLabel({owner, repo, number, state}) {
691 | expect(owner).to.equal('xuexb')
692 | expect(repo).to.equal('github-bot')
693 | expect(number).to.equal(1)
694 | expect(assignees).to.equal('ok')
695 | return Promise.resolve()
696 | }
697 | }
698 | })
699 | const github = require('../src/github')
700 | return github.removeLabelsToPullRequest(payload, 'ok')
701 | })
702 | })
703 |
704 | describe('.removeLabelsToIssue', () => {
705 | it('should be a method', () => {
706 | mockGithub(null, {
707 | issues: {
708 | removeLabel() {
709 | return Promise.resolve()
710 | }
711 | }
712 | })
713 | const github = require('../src/github')
714 | expect(github.removeLabelsToIssue).to.be.a('function')
715 | })
716 |
717 | describe('should return boolean', () => {
718 | it('true', () => {
719 | mockGithub(null, {
720 | issues: {
721 | removeLabel() {
722 | return Promise.resolve()
723 | }
724 | }
725 | })
726 | const github = require('../src/github')
727 | expect(github.removeLabelsToIssue(payload, 'label')).to.eventually.be.true
728 | })
729 |
730 | it('false', () => {
731 | mockGithub(null, {
732 | issues: {
733 | removeLabel() {
734 | throw new TypeError('error')
735 | }
736 | }
737 | })
738 | const github = require('../src/github')
739 | expect(github.removeLabelsToIssue(payload)).to.eventually.be.false
740 | })
741 |
742 | it('error', () => {
743 | mock('../src/logger', {
744 | appLog: {
745 | error(err) {
746 | expect(err).to.not.be.undefined
747 | }
748 | }
749 | })
750 | mockGithub(null)
751 | const github = require('../src/github')
752 | expect(github.removeLabelsToIssue(payload)).to.eventually.be.false
753 | })
754 | })
755 |
756 | it('check param', () => {
757 | mockGithub(null, {
758 | issues: {
759 | removeLabel({owner, repo, number, state}) {
760 | expect(owner).to.equal('xuexb')
761 | expect(repo).to.equal('github-bot')
762 | expect(number).to.equal(1)
763 | expect(assignees).to.equal('ok')
764 | return Promise.resolve()
765 | }
766 | }
767 | })
768 | const github = require('../src/github')
769 | return github.removeLabelsToIssue(payload, 'ok')
770 | })
771 | })
772 |
773 | describe('.createRelease', () => {
774 | it('should be a method', () => {
775 | mockGithub(null, {
776 | repos: {
777 | createRelease() {
778 | return Promise.resolve()
779 | }
780 | }
781 | })
782 | const github = require('../src/github')
783 | expect(github.createRelease).to.be.a('function')
784 | })
785 |
786 | describe('should return boolean', () => {
787 | it('true', () => {
788 | mockGithub(null, {
789 | repos: {
790 | createRelease() {
791 | return Promise.resolve()
792 | }
793 | }
794 | })
795 | const github = require('../src/github')
796 | expect(github.createRelease(payload, {})).to.eventually.be.true
797 | })
798 |
799 | it('false', () => {
800 | mockGithub(null, {
801 | repos: {
802 | createRelease() {
803 | throw new TypeError('error')
804 | }
805 | }
806 | })
807 | const github = require('../src/github')
808 | expect(github.createRelease(payload)).to.eventually.be.false
809 | })
810 |
811 | it('error', () => {
812 | mock('../src/logger', {
813 | appLog: {
814 | error(err) {
815 | expect(err).to.not.be.undefined
816 | }
817 | }
818 | })
819 | mockGithub(null)
820 | const github = require('../src/github')
821 | expect(github.createRelease(payload)).to.eventually.be.false
822 | })
823 | })
824 |
825 | it('check param', () => {
826 | mockGithub(null, {
827 | repos: {
828 | createRelease({owner, repo, number, tag_name, target_commitish, name, body, draft, prerelease}) {
829 | expect(owner).to.equal('xuexb')
830 | expect(repo).to.equal('github-bot')
831 | expect(number).to.equal(1)
832 | expect(tag_name).to.equal('tag_name')
833 | expect(target_commitish).to.equal('target_commitish')
834 | expect(name).to.equal('name')
835 | expect(body).to.equal('body')
836 | expect(draft).to.equal('draft')
837 | expect(prerelease).to.equal('prerelease')
838 | return Promise.resolve()
839 | }
840 | }
841 | })
842 | const github = require('../src/github')
843 | return github.createRelease(payload, {
844 | tag_name: 'tag_name',
845 | target_commitish: 'target_commitish',
846 | name: 'name',
847 | body: 'body',
848 | draft: 'draft',
849 | prerelease: 'prerelease'
850 | })
851 | })
852 | })
853 |
854 | describe('.getReleaseByTag', () => {
855 | it('should be a method', () => {
856 | mockGithub(null, {
857 | repos: {
858 | getReleaseByTag() {
859 | return Promise.resolve()
860 | }
861 | }
862 | })
863 | const github = require('../src/github')
864 | expect(github.getReleaseByTag).to.be.a('function')
865 | })
866 |
867 | describe('should return boolean', () => {
868 | it('true', () => {
869 | mockGithub(null, {
870 | repos: {
871 | getReleaseByTag() {
872 | return Promise.resolve({
873 | data: true
874 | })
875 | }
876 | }
877 | })
878 | const github = require('../src/github')
879 | expect(github.getReleaseByTag(payload, {
880 | tag_name: 'ok'
881 | })).to.eventually.be.true
882 | })
883 |
884 | it('false', () => {
885 | mockGithub(null, {
886 | repos: {
887 | getReleaseByTag() {
888 | return Promise.resolve({
889 | data: false
890 | })
891 | }
892 | }
893 | })
894 | const github = require('../src/github')
895 | expect(github.getReleaseByTag(payload)).to.eventually.be.false
896 | })
897 |
898 | it('error', () => {
899 | mock('../src/logger', {
900 | appLog: {
901 | error(err) {
902 | expect(err).to.not.be.undefined
903 | }
904 | }
905 | })
906 | mockGithub(null)
907 | const github = require('../src/github')
908 | expect(github.getReleaseByTag(payload)).to.eventually.be.null
909 | })
910 | })
911 |
912 | it('check param', () => {
913 | mockGithub(null, {
914 | repos: {
915 | getReleaseByTag({owner, repo, number, name}) {
916 | expect(owner).to.equal('xuexb')
917 | expect(repo).to.equal('github-bot')
918 | expect(number).to.equal(1)
919 | expect(name).to.equal('tag_name')
920 | return Promise.resolve()
921 | }
922 | }
923 | })
924 | const github = require('../src/github')
925 | return github.getReleaseByTag(payload, {
926 | tag_name: 'tag_name'
927 | })
928 | })
929 | })
930 |
931 | describe('.createReviewRequest', () => {
932 | it('should be a method', () => {
933 | mockGithub(null, {
934 | pullRequests: {
935 | createReviewRequest() {
936 | return Promise.resolve()
937 | }
938 | }
939 | })
940 | const github = require('../src/github')
941 | expect(github.createReviewRequest).to.be.a('function')
942 | })
943 |
944 | describe('should return boolean', () => {
945 | it('true', () => {
946 | mockGithub(null, {
947 | pullRequests: {
948 | createReviewRequest() {
949 | return Promise.resolve()
950 | }
951 | }
952 | })
953 | const github = require('../src/github')
954 | expect(github.createReviewRequest(payload, {
955 | reviewers: 'reviewers',
956 | team_reviewers: 'team_reviewers'
957 | })).to.eventually.be.true
958 | })
959 |
960 | it('false', () => {
961 | mockGithub(null, {
962 | pullRequests: {
963 | createReviewRequest() {
964 | throw new TypeError('error')
965 | }
966 | }
967 | })
968 | const github = require('../src/github')
969 | expect(github.createReviewRequest(payload)).to.eventually.be.false
970 | })
971 |
972 | it('error', () => {
973 | mock('../src/logger', {
974 | appLog: {
975 | error(err) {
976 | expect(err).to.not.be.undefined
977 | }
978 | }
979 | })
980 | mockGithub(null)
981 | const github = require('../src/github')
982 | expect(github.createReviewRequest(payload)).to.eventually.be.false
983 | })
984 | })
985 |
986 | it('check param', () => {
987 | mockGithub(null, {
988 | pullRequests: {
989 | createReviewRequest({owner, repo, number, team_reviewers, reviewers}) {
990 | expect(owner).to.equal('xuexb')
991 | expect(repo).to.equal('github-bot')
992 | expect(number).to.equal(1)
993 | expect(reviewers).to.equal('reviewers')
994 | expect(team_reviewers).to.equal('team_reviewers')
995 | return Promise.resolve()
996 | }
997 | }
998 | })
999 | const github = require('../src/github')
1000 | return github.createReviewRequest(payload, {
1001 | reviewers: 'reviewers',
1002 | team_reviewers: 'team_reviewers'
1003 | })
1004 | })
1005 | })
1006 |
1007 | describe('.getTags', () => {
1008 | it('should be a method', () => {
1009 | mockGithub(null, {
1010 | repos: {
1011 | getTags() {
1012 | return Promise.resolve()
1013 | }
1014 | }
1015 | })
1016 | const github = require('../src/github')
1017 | expect(github.getTags).to.be.a('function')
1018 | })
1019 |
1020 | describe('should return boolean', () => {
1021 | it('true', () => {
1022 | mockGithub(null, {
1023 | repos: {
1024 | getTags() {
1025 | return Promise.resolve({
1026 | data: true
1027 | })
1028 | }
1029 | }
1030 | })
1031 | const github = require('../src/github')
1032 | expect(github.getTags(payload)).to.eventually.be.true
1033 | })
1034 |
1035 | it('false', () => {
1036 | mockGithub(null, {
1037 | repos: {
1038 | getTags() {
1039 | return Promise.resolve({
1040 | data: false
1041 | })
1042 | }
1043 | }
1044 | })
1045 | const github = require('../src/github')
1046 | expect(github.getTags(payload)).to.eventually.be.false
1047 | })
1048 |
1049 | it('error', () => {
1050 | mock('../src/logger', {
1051 | appLog: {
1052 | error(err) {
1053 | expect(err).to.not.be.undefined
1054 | }
1055 | }
1056 | })
1057 | mockGithub(null)
1058 | const github = require('../src/github')
1059 | expect(github.getTags(payload)).to.eventually.be.deep.equal([])
1060 | })
1061 | })
1062 |
1063 | it('check param', () => {
1064 | mockGithub(null, {
1065 | repos: {
1066 | getTags({owner, repo}) {
1067 | expect(owner).to.equal('xuexb')
1068 | expect(repo).to.equal('github-bot')
1069 | return Promise.resolve()
1070 | }
1071 | }
1072 | })
1073 | const github = require('../src/github')
1074 | return github.getTags(payload)
1075 | })
1076 | })
1077 |
1078 | describe('.compareCommits', () => {
1079 | it('should be a method', () => {
1080 | mockGithub(null, {
1081 | repos: {
1082 | compareCommits() {
1083 | return Promise.resolve()
1084 | }
1085 | }
1086 | })
1087 | const github = require('../src/github')
1088 | expect(github.compareCommits).to.be.a('function')
1089 | })
1090 |
1091 | describe('should return boolean', () => {
1092 | it('true', () => {
1093 | mockGithub(null, {
1094 | repos: {
1095 | compareCommits() {
1096 | return Promise.resolve({
1097 | data: true
1098 | })
1099 | }
1100 | }
1101 | })
1102 | const github = require('../src/github')
1103 | expect(github.compareCommits(payload, {
1104 | base: 'base',
1105 | head: 'head'
1106 | })).to.eventually.be.true
1107 | })
1108 |
1109 | it('false', () => {
1110 | mockGithub(null, {
1111 | repos: {
1112 | compareCommits() {
1113 | return Promise.resolve({
1114 | data: false
1115 | })
1116 | }
1117 | }
1118 | })
1119 | const github = require('../src/github')
1120 | expect(github.compareCommits(payload)).to.eventually.be.false
1121 | })
1122 |
1123 | it('error', () => {
1124 | mock('../src/logger', {
1125 | appLog: {
1126 | error(err) {
1127 | expect(err).to.not.be.undefined
1128 | }
1129 | }
1130 | })
1131 | mockGithub(null)
1132 | const github = require('../src/github')
1133 | expect(github.compareCommits(payload)).to.eventually.be.null
1134 | })
1135 | })
1136 |
1137 | it('check param', () => {
1138 | mockGithub(null, {
1139 | repos: {
1140 | compareCommits({owner, repo, base, head}) {
1141 | expect(owner).to.equal('xuexb')
1142 | expect(repo).to.equal('github-bot')
1143 | expect(base).to.equal('base')
1144 | expect(head).to.equal('head')
1145 | return Promise.resolve()
1146 | }
1147 | }
1148 | })
1149 | const github = require('../src/github')
1150 | return github.compareCommits(payload, {
1151 | base: 'base',
1152 | head: 'head'
1153 | })
1154 | })
1155 | })
1156 | })
1157 |
--------------------------------------------------------------------------------
/test/modules/issues/autoAssign.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file modules/issues/autoAssign.js test case
3 | * @author xuexb
4 | */
5 |
6 | const expect = require('chai').expect
7 | const mock = require('mock-require')
8 | mock.stopAll()
9 | const clean = require('../../utils/clean')
10 |
11 | describe('modules/issues/autoAssign.js', () => {
12 | beforeEach('clear node cache', () => {
13 | clean('src/github')
14 | clean('src/utils')
15 | clean('src/modules/issues/autoAssign')
16 |
17 | mock('../../../src/utils', {
18 | getPkgConfig() {
19 | return {}
20 | }
21 | })
22 | mock('../../../src/github', {
23 | addAssigneesToIssue() {
24 | }
25 | })
26 | })
27 |
28 | it('event name', () => {
29 | const autoAssign = require('../../../src/modules/issues/autoAssign')
30 | autoAssign(name => {
31 | expect(name).to.equal('issues_labeled')
32 | })
33 | })
34 |
35 | describe('set label', () => {
36 | it('is ok', (done) => {
37 | mock('../../../src/utils', {
38 | getPkgConfig() {
39 | return {
40 | labelToAuthor: {
41 | autoAssign: 'github-bot'
42 | }
43 | }
44 | }
45 | })
46 | mock('../../../src/github', {
47 | addAssigneesToIssue(payload, label) {
48 | expect(payload).to.be.a('object').and.not.empty
49 | expect(label).to.equal('github-bot')
50 | done()
51 | }
52 | })
53 |
54 | const autoAssign = require('../../../src/modules/issues/autoAssign')
55 | autoAssign(function (name, callback) {
56 | callback({
57 | payload: {
58 | label: {
59 | name: 'autoAssign'
60 | }
61 | }
62 | })
63 | })
64 | })
65 |
66 | it('is false', (done) => {
67 | mock('../../../src/github', {
68 | addAssigneesToIssue() {
69 | done('error')
70 | }
71 | })
72 |
73 | const autoAssign = require('../../../src/modules/issues/autoAssign')
74 | autoAssign(function (name, callback) {
75 | callback({
76 | payload: {
77 | label: {
78 | name: 'error'
79 | }
80 | }
81 | })
82 | })
83 | setTimeout(done)
84 | })
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/test/modules/issues/autoLabel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file modules/issues/autoLabel.js test case
3 | * @author xuexb
4 | */
5 |
6 | const expect = require('chai').expect
7 | const mock = require('mock-require')
8 | mock.stopAll()
9 | const clean = require('../../utils/clean')
10 |
11 | describe('modules/issues/autoLabel.js', () => {
12 | beforeEach('clear node cache', () => {
13 | clean('src/github')
14 | clean('src/modules/issues/autoLabel')
15 |
16 | mock('../../../src/github', {
17 | addLabelsToIssue() {
18 | }
19 | })
20 | })
21 |
22 | it('event name', () => {
23 | const autoLabel = require('../../../src/modules/issues/autoLabel')
24 | autoLabel(name => {
25 | expect(name).to.equal('issues_opened')
26 | })
27 | })
28 |
29 | it('get label success', (done) => {
30 | mock('../../../src/github', {
31 | addLabelsToIssue(payload, label) {
32 | expect(payload).to.be.a('object').and.not.empty
33 | expect(label).to.equal('github-bot')
34 | done()
35 | }
36 | })
37 |
38 | const autoLabel = require('../../../src/modules/issues/autoLabel')
39 | autoLabel((name, callback) => {
40 | callback({
41 | payload: {
42 | issue: {
43 | body: '我是测试内容\n测试'
44 | }
45 | }
46 | })
47 | })
48 | })
49 |
50 | it('get label error', (done) => {
51 | mock('../../../src/github', {
52 | addLabelsToIssue() {
53 | done('error')
54 | }
55 | })
56 |
57 | const autoLabel = require('../../../src/modules/issues/autoLabel')
58 | autoLabel((name, callback) => {
59 | callback({
60 | payload: {
61 | issue: {
62 | body: '我是测试内容'
63 | }
64 | }
65 | })
66 | })
67 | setTimeout(done)
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file utils.js test case
3 | * @author xuexb
4 | */
5 | require('mock-require').stopAll()
6 | const utils = require('../src/utils')
7 | const expect = require('chai').expect
8 |
9 | describe('utils.js', () => {
10 | describe('.toArray', () => {
11 | it('should return self if empty', () => {
12 | expect(utils.toArray()).to.be.undefined
13 | expect(utils.toArray('')).to.equal('')
14 | expect(utils.toArray(null)).to.be.null
15 | })
16 | it('should return array if not the empty string', () => {
17 | expect(utils.toArray(['string'])).to.be.a('array').and.to.deep.equal(['string'])
18 | expect(utils.toArray('string')).to.be.a('array').and.to.deep.equal(['string'])
19 | })
20 | })
21 |
22 | describe('.getPkgConfig', () => {
23 | it('should return object', () => {
24 | expect(utils.getPkgConfig()).to.be.a('object').and.to.not.empty
25 | })
26 | })
27 |
28 | describe('.getPkgCommitPrefix', () => {
29 | it('should return array', () => {
30 | expect(utils.getPkgCommitPrefix()).to.be.a('array').and.to.not.empty
31 | })
32 | })
33 |
34 | it('.verifySignature', () => {
35 | const GITHUB_SECRET_TOKEN = process.env['GITHUB_SECRET_TOKEN']
36 |
37 | process.env['GITHUB_SECRET_TOKEN'] = 'test'
38 | const flag = utils.verifySignature({
39 | rawBody: 'test',
40 | headers: {
41 | 'x-hub-signature': 'test'
42 | }
43 | })
44 | process.env['GITHUB_SECRET_TOKEN'] = GITHUB_SECRET_TOKEN
45 |
46 | expect(flag).to.be.false
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/test/utils/clean.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file 清除 node 缓存,以根目录为基础路径
3 | * @author xuexb
4 | */
5 |
6 | const resolve = require('path').resolve
7 | const extname = require('path').extname
8 |
9 | module.exports = path => {
10 | delete require.cache[resolve(__dirname, '../../', path) + (extname(path) === '' ? '.js' : '')]
11 | }
12 |
--------------------------------------------------------------------------------