├── .autod.conf.js
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE
├── MIT-LICENSE.md
├── README.md
├── app
├── controller
│ └── home.js
├── router.js
└── service
│ └── webhook.js
├── appveyor.yml
├── config
├── config.default.js
└── plugin.js
├── docker-compose.yml
├── docs
├── gitlab-integration-1.png
├── gitlab-mr-msg-1.png
├── gitlab-pipeline-msg-1.png
├── gitlab-push-msg-1.png
├── gitlab-push-msg-2.png
├── gitlab-push-msg-3.png
└── gitlab-push-tag-msg-1.png
├── jsconfig.json
├── package.json
├── sample.json
└── test
└── app
└── controller
└── home.test.js
/.autod.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | write: true,
5 | prefix: '^',
6 | plugin: 'autod-egg',
7 | test: [
8 | 'test',
9 | 'benchmark',
10 | ],
11 | dep: [
12 | 'egg',
13 | 'egg-scripts',
14 | ],
15 | devdep: [
16 | 'egg-ci',
17 | 'egg-bin',
18 | 'egg-mock',
19 | 'autod',
20 | 'autod-egg',
21 | 'eslint',
22 | 'eslint-config-egg',
23 | ],
24 | exclude: [
25 | './test/fixtures',
26 | './dist',
27 | ],
28 | };
29 |
30 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | coverage
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-egg"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | strategy:
11 | matrix:
12 | node-version: [8.x, 10.x, 12.x]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - run: npm install
21 | - run: npm run build --if-present
22 | env:
23 | CI: true
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs/
2 | npm-debug.log
3 | yarn-error.log
4 | node_modules/
5 | package-lock.json
6 | yarn.lock
7 | coverage/
8 | .idea/
9 | run/
10 | .DS_Store
11 | *.sw*
12 | *.un~
13 | typings/
14 | .nyc_output/
15 | .vscode/
16 |
17 | .gitlab-ci.yml
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - '10'
5 | before_install:
6 | - npm i npminstall -g
7 | install:
8 | - npminstall
9 | script:
10 | - npm run ci
11 | after_script:
12 | - npminstall codecov && codecov
13 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM library/node:lts-alpine
2 |
3 | COPY . /app
4 | WORKDIR /app
5 |
6 | EXPOSE 7001
7 |
8 | RUN cd /app && \
9 | npm i --production
10 |
11 | CMD ["npm", "start"]
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 名洋集团
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MIT-LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2020 Mingyang Digital
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gitlab通知机器人
2 |
3 | 将`Gitlab`的`push`、`tag push`、`merge request`和`pipeline`推送到企业微信的机器人。
4 |
5 | 具体见下图:
6 |
7 | Gitlab push 代码推送
8 |
9 | 
10 |
11 | Gitlab push 新建分支
12 |
13 | 
14 |
15 | Gitlab push 删除分支
16 |
17 | 
18 |
19 | Gitlab push tag 推标签
20 |
21 | 
22 |
23 | Gitlab merge request 合并请求
24 |
25 | 
26 |
27 | Gitlab pipeline 流水线
28 |
29 | 
30 |
31 | ## 与企业微信对接
32 |
33 | 如何添加群机器人可自行百度。企业微[信群机器人配置说明](https://work.weixin.qq.com/api/doc/90000/90136/91770)。
34 |
35 |
36 | ## 应用部署运行
37 |
38 | 应用通过环境变量添加机器人webhook地址,`WEBHOOK_URL_`作为前缀,后面可接不同的推送组。使用推送组可以将消息推送到不同的群组机器人。
39 |
40 | 如环境变量`WEBHOOK_URL_PROJ`,`PROJ`则为推送组。推送组用于与`Gitlab`的集成时使用。
41 |
42 | 例如:
43 | - 机器人的webhook地址为:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=ABCDEFG
44 | - 推送组为`PROJ`。
45 |
46 | 则环境变量设为:
47 | ```
48 | WEBHOOK_URL_PROJ=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=ABCDEFG
49 | ```
50 |
51 | 一个应用可以添加多个推送组。
52 |
53 | ### 使用Docker部署
54 |
55 | 修改`docker-compose.yml`文件中的`WEBHOOK_URL`环境变量,添加`企业微信机器人`的`webhook`地址。
56 |
57 | ```bash
58 | docker-compose up -d
59 | ```
60 |
61 | 通过`:7001`端口访问服务。
62 |
63 | ### 直接运行
64 |
65 | 首先系统安装了`node`运行环境。
66 |
67 | ```bash
68 | WEBHOOK_URL_PROJ=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=ABCDEFG npm start
69 | ```
70 |
71 | 通过`:7001`端口访问服务。
72 |
73 | ## 与Gitlab集成
74 |
75 | 进到项目,`settings` => `integrations`。
76 |
77 | URL填写服务的地址和端口号+推送组。
78 |
79 | 例如,服务器地址为:https://192.168.100.100:7001,推送组为PROJ。
80 |
81 | URL填写:https://192.168.100.100:7001/proj (不区分大小写)
82 |
83 | 具体设置,参见下图:
84 |
85 | 
86 |
87 | ## Docker Hub 地址
88 | https://hub.docker.com/repository/docker/mingyanggroup/gitlab-wxwork-robot
89 |
--------------------------------------------------------------------------------
/app/controller/home.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Controller = require('egg').Controller;
4 | const S = require('string')
5 |
6 | class HomeController extends Controller {
7 | async index() {
8 | const { ctx } = this;
9 | const { path = '' } = ctx.params
10 |
11 | const webhookUrl = process.env['WEBHOOK_URL' + (path ? '_' + path.toUpperCase() : '')];
12 |
13 | ctx.logger.info('request body: ', ctx.request.body);
14 | const message = await ctx.service.webhook.translateMsg(ctx.request.body);
15 |
16 | if (!message) {
17 | ctx.logger.info('====> message is empty, suppressed.')
18 | ctx.body = { msg: 'message is empty, suppressed.' }
19 | return
20 | }
21 |
22 |
23 | if (!webhookUrl) {
24 | ctx.logger.error('webhook url error, webhookUrl: ' + webhookUrl);
25 | ctx.body = {
26 | error: 'webhook url error, webhookUrl: ' + webhookUrl,
27 | };
28 | return
29 | }
30 |
31 | const result = await ctx.curl(webhookUrl, {
32 | method: 'POST',
33 | headers: {
34 | 'content-type': 'application/json; charset=UTF-8',
35 | },
36 | // 自动解析 JSON response
37 | dataType: 'json',
38 | // 3 秒超时
39 | timeout: 3000,
40 |
41 | data: message,
42 | });
43 |
44 | ctx.body = {
45 | webhook_url: webhookUrl,
46 | webhook_message: message,
47 | status: result.status,
48 | headers: result.headers,
49 | package: result.data,
50 | };
51 |
52 | ctx.logger.info('response body: ', ctx.body);
53 | }
54 | }
55 |
56 | module.exports = HomeController;
57 |
--------------------------------------------------------------------------------
/app/router.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const S = require('string')
4 | const contextPath = process.env.CONTEXT_PATH || '/';
5 | const contextPathEndWithSlash = S(contextPath).endsWith('/') ? contextPath : contextPath + '/'
6 |
7 | /**
8 | * @param {Egg.Application} app - egg application
9 | */
10 | module.exports = app => {
11 | const { router, controller } = app;
12 | app.logger.info('===> contextPath: ', contextPath);
13 |
14 | router.post(`${contextPathEndWithSlash}:path`, controller.home.index);
15 | router.post(`${contextPathEndWithSlash}`, controller.home.index);
16 | };
17 |
--------------------------------------------------------------------------------
/app/service/webhook.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Service = require('egg').Service;
4 | const _ = require('lodash')
5 | const moment = require('moment')
6 | const S = require('string')
7 |
8 | const OBJECT_KIND = {
9 | push: 'push',
10 | tag_push: 'tag_push',
11 | issue: 'issue', // todo
12 | note: 'note', // todo
13 | merge_request: 'merge_request',
14 | wiki_page: 'wiki_page', // todo
15 | pipeline: 'pipeline',
16 | build: 'build',
17 | }
18 |
19 | const REDIS_KEY = {
20 | pipeline: (id) => `gitlab.pipeline.${id}`,
21 | }
22 |
23 | const REDIS_VAL = {
24 | pipeline: ({ pipelineId, stages, status, duration, builds }) => {
25 | return {
26 | type: 'pipeline',
27 | id: pipelineId,
28 | duration: duration,
29 | durationMin: Math.round(duration / 60 - 0.5),
30 | durationSec: duration % 60,
31 | status: status,
32 | stages: stages,
33 | builds: builds
34 | }
35 | }
36 | }
37 |
38 | class WebhookService extends Service {
39 | async translateMsg(data) {
40 | const { object_kind } = data || {};
41 | if (!OBJECT_KIND[object_kind]) {
42 | return {};
43 | }
44 |
45 | let res = true
46 | const content = [];
47 | switch (object_kind) {
48 | case OBJECT_KIND.push:
49 | res = await this.assemblePushMsg(content, data)
50 | break;
51 |
52 | case OBJECT_KIND.pipeline:
53 | res = await this.assemblePipelineMsg(content, data)
54 | break;
55 |
56 |
57 | case OBJECT_KIND.merge_request:
58 | res = await this.assembleMergeMsg(content, data)
59 | break;
60 |
61 | case OBJECT_KIND.tag_push:
62 | res = await this.assembleTagPushMsq(content, data)
63 | break;
64 | }
65 | if (!res) return false
66 |
67 | return {
68 | msgtype: 'markdown',
69 | markdown: { content: content.join(' \n ') },
70 | };
71 | }
72 |
73 | async assemblePushMsg(content, { user_name, ref, project, commits, total_commits_count, before, after }) {
74 | const { name: projName, web_url, path_with_namespace } = project || {};
75 |
76 | const branch = ref.replace('refs/heads/', '')
77 | let op = ''
78 | if (before === '0000000000000000000000000000000000000000') {
79 | // new branch
80 | op = '新建分支'
81 | } else if (after === '0000000000000000000000000000000000000000') {
82 | // remove brance
83 | op = '删除分支'
84 | } else {
85 | // others
86 | op = '将代码推至'
87 | }
88 |
89 | content.push(` ${user_name} ${op}[[${path_with_namespace}/${branch}](${web_url}/tree/${branch})]。`)
90 | content.push(`> 项目 [[${projName} | ${path_with_namespace}](${web_url})]\n`)
91 | total_commits_count && content.push(`**共提交${total_commits_count}次:**\n`)
92 | total_commits_count && content.push(this.generateListItem('', this.formatCommits(commits).text));
93 |
94 | return content
95 | }
96 |
97 | async assemblePipelineMsg(content, { object_attributes, merge_request: mr, user, project, commit, builds }) {
98 | const { id: pipelineId, ref, status, duration, source, stages } = object_attributes || {};
99 | const { name: projName, web_url, path_with_namespace } = project || {};
100 | const { name, username } = user || {};
101 | const pipelineUrl = web_url + '/pipelines/' + pipelineId
102 |
103 | // find any build not finished (success, failed, skipped)
104 | const createdBuilds = _.find(builds, { status: 'created' });
105 | const runningBuilds = _.find(builds, { status: 'running' });
106 | const pendingBuilds = _.find(builds, { status: 'pending' });
107 | this.logger.info('===> createdBuilds', createdBuilds)
108 | this.logger.info('===> runningBuilds', runningBuilds)
109 | this.logger.info('===> pendingBuilds', pendingBuilds)
110 |
111 | if (createdBuilds || runningBuilds || pendingBuilds) {
112 | // suppress msg
113 | return false
114 | }
115 |
116 | const { statusColor, statusString } = this.formatStatus(status)
117 |
118 | let sourceString;
119 | switch (source) {
120 | case 'push':
121 | sourceString = '推送操作'
122 | break
123 | case 'merge_request_event':
124 | sourceString = '合并操作'
125 | break
126 | case 'web':
127 | sourceString = '网页运行'
128 | break
129 | default:
130 | sourceString = `操作(${source})`
131 | }
132 |
133 | content.push(`[[#${pipelineId}流水线](${pipelineUrl})] ${statusString},位于${ref}分支,由${sourceString}触发。`)
134 | content.push(`> 项目 [[${projName} | ${path_with_namespace}](${web_url})]\n`)
135 | content.push('**流水线详情:**\n')
136 |
137 | name && content.push(this.generateListItem('操作人', `${name}`))
138 |
139 | duration && content.push(this.generateListItem('总耗时', `${this.formatDuration(duration)}`))
140 | !_.isEmpty(stages) && content.push(this.generateListItem(`共${stages.length}个阶段`, `${stages.join(' / ')}`))
141 | !_.isEmpty(mr) && content.push(this.generateListItem('合并详情', `[${mr.title}](${mr.url}),\`${mr.source_branch}\`合并至\`${mr.target_branch}\``));
142 | !_.isEmpty(commit) && content.push(this.generateListItem('提交详情', `\n${commit.author.name}: [${S(commit.message).collapseWhitespace()}](${commit.url})`));
143 | !_.isEmpty(builds) && content.push(this.generateListItem(`编译详情`, `\n${this.formatBuilds(builds, username, web_url).join('\n')}`))
144 |
145 | return content
146 | }
147 |
148 | async assembleMergeMsg(content, { user, project, object_attributes }) {
149 | const { name } = user || {};
150 | const { iid: mrId, url: mrUrl, target_branch, source_branch, state, title, description, last_commit: commit, updated_at } = object_attributes || {};
151 | const { name: projName, web_url, path_with_namespace } = project || {};
152 |
153 | let stateString = '', stateEnding = '';
154 | // opened, closed, locked, or merged
155 | switch (state) {
156 | case 'opened':
157 | stateString = '开启了'
158 | stateEnding = ',**请项目管理员确认**'
159 | break
160 |
161 | case 'closed':
162 | stateString = '取消了'
163 | stateEnding = ',**请提交人仔细检查**'
164 | break
165 |
166 | case 'locked':
167 | stateString = '锁定了'
168 | break
169 |
170 | case 'merged':
171 | stateString = '确认了'
172 | break
173 |
174 | }
175 |
176 | content.push(`\`${name}\`**${stateString}**[[#${mrId}合并请求 ${title}](${mrUrl})],\`${source_branch}\`合并至\`${target_branch}\`${stateEnding}。`)
177 | content.push(`> 项目 [[${projName} | ${path_with_namespace}](${web_url})]\n`)
178 | content.push('**MR详情:**\n')
179 |
180 | updated_at && content.push(this.generateListItem('提交时间', moment(updated_at).format('MM-DD HH:mm')))
181 | description && content.push(this.generateListItem('合并详情', description))
182 | !_.isEmpty(commit) && content.push(this.generateListItem('提交详情', `\n${commit.author.name}: [${S(commit.message).collapseWhitespace()}](${commit.url})`));
183 |
184 | return content
185 | }
186 |
187 | async assembleTagPushMsq(content, { ref, user_name, project, message, commits, total_commits_count, before, after }) {
188 | const { name: projName, web_url, path_with_namespace } = project || {};
189 |
190 | const tag = ref.replace('refs/tags/', '')
191 | let op = ''
192 |
193 | if (before === '0000000000000000000000000000000000000000') {
194 | // new
195 | op = '新增'
196 | } else if (after === '0000000000000000000000000000000000000000') {
197 | // remove
198 | op = '删除'
199 | }
200 |
201 | content.push(`\`${user_name}\`${op}标签[[${path_with_namespace}/${tag}](${web_url}/-/tags/${tag})]。`)
202 | content.push(`> 项目 [[${projName} | ${path_with_namespace}](${web_url})]\n`)
203 |
204 | message && content.push(this.generateListItem('说明', message));
205 | total_commits_count && content.push(`**共提交${total_commits_count}次:**\n`)
206 | total_commits_count && content.push(this.generateListItem('', this.formatCommits(commits).text));
207 | return content
208 | }
209 |
210 | formatDuration(duration) {
211 | if (duration < 60) return duration + '秒'
212 | if (duration < 3600) return Math.round(duration / 60 - 0.5) + '分' + (duration % 60) + '秒'
213 | return duration + '秒'
214 | }
215 |
216 | formatBuilds(builds, username, web_url) {
217 | return builds.map(build => {
218 | const { id, name, stage, user } = build
219 | const { statusColor, statusString } = this.formatStatus(build.status)
220 | const buildUrl = web_url + '/-/jobs/' + id
221 | const byWho = (username === user.username ? '' : `,由\`${user.name}\`触发`)
222 | return `\`${stage}\`: [\`${name}\`](${buildUrl}) > ${statusString}${byWho}`
223 | })
224 | }
225 |
226 | formatStatus(status) {
227 | let statusColor = 'comment', statusString, isNotify = true;
228 | switch (status) {
229 | case 'failed':
230 | statusColor = 'warning'
231 | statusString = '执行失败'
232 | break
233 | case 'success':
234 | statusColor = 'info'
235 | statusString = '执行成功'
236 | break
237 | case 'running':
238 | statusString = '运行中'
239 | break
240 | case 'pending':
241 | statusColor = 'warning'
242 | statusString = '准备中'
243 | isNotify = false
244 | break
245 | case 'canceled':
246 | statusString = '已取消'
247 | break
248 | case 'skipped':
249 | statusString = '已跳过'
250 | break
251 | case 'manual':
252 | statusString = '需手动触发'
253 | break
254 | default:
255 | statusString = `状态未知 (${status})`
256 | }
257 |
258 | return { statusColor, statusString }
259 | }
260 |
261 | formatCommits(commits) {
262 | const changes = { added: 0, modified: 0, removed: 0 };
263 | const result = {
264 | commits: commits.map(commit => {
265 | const { author, message, url, added, modified, removed } = commit;
266 | changes.added += added.length || 0;
267 | changes.modified += modified.length || 0;
268 | changes.removed += removed.length || 0;
269 |
270 | return `${author.name}: [${S(message).collapseWhitespace()}](${url})`
271 | }), changes,
272 | };
273 |
274 | result.text = `新增: \`${result.changes.added}\` `
275 | + `修改: \`${result.changes.modified}\` `
276 | + `删除: \`${result.changes.removed}\` \n `
277 | + result.commits.join('\n')
278 |
279 |
280 | return result
281 | }
282 |
283 | generateListItem(label, text, url) {
284 | if (label) label = label + ':'
285 |
286 | if (url) {
287 | return `>${label} [${text}](${url})`
288 | } else {
289 | return `>${label} ${text}`
290 | }
291 | }
292 |
293 | }
294 |
295 | module.exports = WebhookService;
296 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | environment:
2 | matrix:
3 | - nodejs_version: '10'
4 |
5 | install:
6 | - ps: Install-Product node $env:nodejs_version
7 | - npm i npminstall && node_modules\.bin\npminstall
8 |
9 | test_script:
10 | - node --version
11 | - npm --version
12 | - npm run test
13 |
14 | build: off
15 |
--------------------------------------------------------------------------------
/config/config.default.js:
--------------------------------------------------------------------------------
1 | /* eslint valid-jsdoc: "off" */
2 |
3 | 'use strict';
4 |
5 | /**
6 | * @param {Egg.EggAppInfo} appInfo app info
7 | */
8 | module.exports = appInfo => {
9 | /**
10 | * built-in config
11 | * @type {Egg.EggAppConfig}
12 | **/
13 | const config = exports = {};
14 |
15 | // use for cookie sign key, should change to your own and keep security
16 | config.keys = appInfo.name + '_1576723863361_2114';
17 |
18 | config.security = {
19 | csrf: {
20 | enable: false,
21 | },
22 | };
23 |
24 | // add your middleware config here
25 | config.middleware = [];
26 |
27 | // add your user config here
28 | const userConfig = {
29 | // myAppName: 'egg',
30 | };
31 |
32 | return {
33 | ...config,
34 | ...userConfig,
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/config/plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /** @type Egg.EggPlugin */
4 | module.exports = {
5 | // had enabled by egg
6 | // static: {
7 | // enable: true,
8 | // }
9 | };
10 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.0'
2 |
3 | services:
4 | bot:
5 | build: .
6 | ports:
7 | - 7001:7001
8 | environment:
9 | - "CONTEXT_PATH=/"
10 | - "WEBHOOK_URL_PROJ=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=ABCDEFG"
11 | deploy:
12 | replicas: 1
13 | resources:
14 | reservations:
15 | cpus: '1'
16 | memory: 64M
17 | limits:
18 | cpus: '2'
19 | memory: 256M
20 | update_config:
21 | parallelism: 1
22 | delay: 10s
23 | restart_policy:
24 | condition: on-failure
25 |
--------------------------------------------------------------------------------
/docs/gitlab-integration-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MingYangGroup/gitlab-wxwork-robot/d6c089be18c445f3e51cdd34d8f7be3f85526149/docs/gitlab-integration-1.png
--------------------------------------------------------------------------------
/docs/gitlab-mr-msg-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MingYangGroup/gitlab-wxwork-robot/d6c089be18c445f3e51cdd34d8f7be3f85526149/docs/gitlab-mr-msg-1.png
--------------------------------------------------------------------------------
/docs/gitlab-pipeline-msg-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MingYangGroup/gitlab-wxwork-robot/d6c089be18c445f3e51cdd34d8f7be3f85526149/docs/gitlab-pipeline-msg-1.png
--------------------------------------------------------------------------------
/docs/gitlab-push-msg-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MingYangGroup/gitlab-wxwork-robot/d6c089be18c445f3e51cdd34d8f7be3f85526149/docs/gitlab-push-msg-1.png
--------------------------------------------------------------------------------
/docs/gitlab-push-msg-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MingYangGroup/gitlab-wxwork-robot/d6c089be18c445f3e51cdd34d8f7be3f85526149/docs/gitlab-push-msg-2.png
--------------------------------------------------------------------------------
/docs/gitlab-push-msg-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MingYangGroup/gitlab-wxwork-robot/d6c089be18c445f3e51cdd34d8f7be3f85526149/docs/gitlab-push-msg-3.png
--------------------------------------------------------------------------------
/docs/gitlab-push-tag-msg-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MingYangGroup/gitlab-wxwork-robot/d6c089be18c445f3e51cdd34d8f7be3f85526149/docs/gitlab-push-tag-msg-1.png
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "**/*"
4 | ]
5 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gitlab-bot",
3 | "version": "1.0.0",
4 | "description": "",
5 | "private": true,
6 | "egg": {
7 | "declarations": true
8 | },
9 | "dependencies": {
10 | "egg": "^2.15.1",
11 | "egg-scripts": "^2.11.0",
12 | "lodash": "^4.17.15",
13 | "moment": "^2.24.0",
14 | "string": "^3.3.3"
15 | },
16 | "devDependencies": {
17 | "autod": "^3.0.1",
18 | "autod-egg": "^1.1.0",
19 | "egg-bin": "^4.11.0",
20 | "egg-ci": "^1.11.0",
21 | "egg-mock": "^3.21.0",
22 | "eslint": "^5.13.0",
23 | "eslint-config-egg": "^7.1.0"
24 | },
25 | "engines": {
26 | "node": ">=10.0.0"
27 | },
28 | "scripts": {
29 | "start": "egg-scripts start --title=egg-server-gitlab-bot",
30 | "stop": "egg-scripts stop --title=egg-server-gitlab-bot",
31 | "dev": "egg-bin dev",
32 | "debug": "egg-bin debug",
33 | "test": "npm run lint -- --fix && npm run test-local",
34 | "test-local": "egg-bin test",
35 | "cov": "egg-bin cov",
36 | "lint": "eslint .",
37 | "ci": "npm run lint && npm run cov",
38 | "autod": "autod"
39 | },
40 | "ci": {
41 | "version": "10"
42 | },
43 | "repository": {
44 | "type": "git",
45 | "url": ""
46 | },
47 | "author": "",
48 | "license": "MIT"
49 | }
50 |
--------------------------------------------------------------------------------
/sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "object_kind": "tag_push",
3 | "before": "0000000000000000000000000000000000000000",
4 | "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
5 | "ref": "refs/tags/v1.0.0",
6 | "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
7 | "user_id": 1,
8 | "user_name": "John Smith",
9 | "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
10 | "project_id": 1,
11 | "project":{
12 | "id": 1,
13 | "name":"Example",
14 | "description":"",
15 | "web_url":"http://example.com/jsmith/example",
16 | "avatar_url":null,
17 | "git_ssh_url":"git@example.com:jsmith/example.git",
18 | "git_http_url":"http://example.com/jsmith/example.git",
19 | "namespace":"Jsmith",
20 | "visibility_level":0,
21 | "path_with_namespace":"jsmith/example",
22 | "default_branch":"master",
23 | "homepage":"http://example.com/jsmith/example",
24 | "url":"git@example.com:jsmith/example.git",
25 | "ssh_url":"git@example.com:jsmith/example.git",
26 | "http_url":"http://example.com/jsmith/example.git"
27 | },
28 | "repository":{
29 | "name": "Example",
30 | "url": "ssh://git@example.com/jsmith/example.git",
31 | "description": "",
32 | "homepage": "http://example.com/jsmith/example",
33 | "git_http_url":"http://example.com/jsmith/example.git",
34 | "git_ssh_url":"git@example.com:jsmith/example.git",
35 | "visibility_level":0
36 | },
37 | "commits": [],
38 | "total_commits_count": 0
39 | }
40 |
41 |
42 | {
43 | "object_kind": "merge_request",
44 | "user": {
45 | "name": "Administrator",
46 | "username": "root",
47 | "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
48 | },
49 | "project": {
50 | "id": 1,
51 | "name":"Gitlab Test",
52 | "description":"Aut reprehenderit ut est.",
53 | "web_url":"http://example.com/gitlabhq/gitlab-test",
54 | "avatar_url":null,
55 | "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
56 | "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
57 | "namespace":"GitlabHQ",
58 | "visibility_level":20,
59 | "path_with_namespace":"gitlabhq/gitlab-test",
60 | "default_branch":"master",
61 | "homepage":"http://example.com/gitlabhq/gitlab-test",
62 | "url":"http://example.com/gitlabhq/gitlab-test.git",
63 | "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
64 | "http_url":"http://example.com/gitlabhq/gitlab-test.git"
65 | },
66 | "repository": {
67 | "name": "Gitlab Test",
68 | "url": "http://example.com/gitlabhq/gitlab-test.git",
69 | "description": "Aut reprehenderit ut est.",
70 | "homepage": "http://example.com/gitlabhq/gitlab-test"
71 | },
72 | "object_attributes": {
73 | "id": 99,
74 | "target_branch": "master",
75 | "source_branch": "ms-viewport",
76 | "source_project_id": 14,
77 | "author_id": 51,
78 | "assignee_id": 6,
79 | "title": "MS-Viewport",
80 | "created_at": "2013-12-03T17:23:34Z",
81 | "updated_at": "2013-12-03T17:23:34Z",
82 | "milestone_id": null,
83 | "state": "opened",
84 | "merge_status": "unchecked",
85 | "target_project_id": 14,
86 | "iid": 1,
87 | "description": "hello world",
88 | "source": {
89 | "name":"Awesome Project",
90 | "description":"Aut reprehenderit ut est.",
91 | "web_url":"http://example.com/awesome_space/awesome_project",
92 | "avatar_url":null,
93 | "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
94 | "git_http_url":"http://example.com/awesome_space/awesome_project.git",
95 | "namespace":"Awesome Space",
96 | "visibility_level":20,
97 | "path_with_namespace":"awesome_space/awesome_project",
98 | "default_branch":"master",
99 | "homepage":"http://example.com/awesome_space/awesome_project",
100 | "url":"http://example.com/awesome_space/awesome_project.git",
101 | "ssh_url":"git@example.com:awesome_space/awesome_project.git",
102 | "http_url":"http://example.com/awesome_space/awesome_project.git"
103 | },
104 | "target": {
105 | "name":"Awesome Project",
106 | "description":"Aut reprehenderit ut est.",
107 | "web_url":"http://example.com/awesome_space/awesome_project",
108 | "avatar_url":null,
109 | "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
110 | "git_http_url":"http://example.com/awesome_space/awesome_project.git",
111 | "namespace":"Awesome Space",
112 | "visibility_level":20,
113 | "path_with_namespace":"awesome_space/awesome_project",
114 | "default_branch":"master",
115 | "homepage":"http://example.com/awesome_space/awesome_project",
116 | "url":"http://example.com/awesome_space/awesome_project.git",
117 | "ssh_url":"git@example.com:awesome_space/awesome_project.git",
118 | "http_url":"http://example.com/awesome_space/awesome_project.git"
119 | },
120 | "last_commit": {
121 | "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
122 | "message": "fixed readme",
123 | "timestamp": "2012-01-03T23:36:29+02:00",
124 | "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
125 | "author": {
126 | "name": "GitLab dev user",
127 | "email": "gitlabdev@dv6700.(none)"
128 | }
129 | },
130 | "work_in_progress": false,
131 | "url": "http://example.com/diaspora/merge_requests/1",
132 | "action": "open",
133 | "assignee": {
134 | "name": "User1",
135 | "username": "user1",
136 | "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
137 | }
138 | },
139 | "labels": [{
140 | "id": 206,
141 | "title": "API",
142 | "color": "#ffffff",
143 | "project_id": 14,
144 | "created_at": "2013-12-03T17:15:43Z",
145 | "updated_at": "2013-12-03T17:15:43Z",
146 | "template": false,
147 | "description": "API related issues",
148 | "type": "ProjectLabel",
149 | "group_id": 41
150 | }],
151 | "changes": {
152 | "updated_by_id": {
153 | "previous": null,
154 | "current": 1
155 | },
156 | "updated_at": {
157 | "previous": "2017-09-15 16:50:55 UTC",
158 | "current":"2017-09-15 16:52:00 UTC"
159 | },
160 | "labels": {
161 | "previous": [{
162 | "id": 206,
163 | "title": "API",
164 | "color": "#ffffff",
165 | "project_id": 14,
166 | "created_at": "2013-12-03T17:15:43Z",
167 | "updated_at": "2013-12-03T17:15:43Z",
168 | "template": false,
169 | "description": "API related issues",
170 | "type": "ProjectLabel",
171 | "group_id": 41
172 | }],
173 | "current": [{
174 | "id": 205,
175 | "title": "Platform",
176 | "color": "#123123",
177 | "project_id": 14,
178 | "created_at": "2013-12-03T17:15:43Z",
179 | "updated_at": "2013-12-03T17:15:43Z",
180 | "template": false,
181 | "description": "Platform related issues",
182 | "type": "ProjectLabel",
183 | "group_id": 41
184 | }]
185 | }
186 | }
187 | }
188 |
189 | {
190 | "object_kind": "pipeline",
191 | "object_attributes": {
192 | "id": 31,
193 | "ref": "master",
194 | "tag": false,
195 | "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
196 | "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
197 | "source": "merge_request_event",
198 | "status": "success",
199 | "stages": [
200 | "build",
201 | "test",
202 | "deploy"
203 | ],
204 | "created_at": "2016-08-12 15:23:28 UTC",
205 | "finished_at": "2016-08-12 15:26:29 UTC",
206 | "duration": 63,
207 | "variables": [
208 | {
209 | "key": "NESTOR_PROD_ENVIRONMENT",
210 | "value": "us-west-1"
211 | }
212 | ]
213 | },
214 | "merge_request": {
215 | "id": 1,
216 | "iid": 1,
217 | "title": "Test",
218 | "source_branch": "test",
219 | "source_project_id": 1,
220 | "target_branch": "master",
221 | "target_project_id": 1,
222 | "state": "opened",
223 | "merge_status": "can_be_merged",
224 | "url": "http://192.168.64.1:3005/gitlab-org/gitlab-test/merge_requests/1"
225 | },
226 | "user": {
227 | "name": "Administrator",
228 | "username": "root",
229 | "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
230 | },
231 | "project": {
232 | "id": 1,
233 | "name": "Gitlab Test",
234 | "description": "Atque in sunt eos similique dolores voluptatem.",
235 | "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
236 | "avatar_url": null,
237 | "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
238 | "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
239 | "namespace": "Gitlab Org",
240 | "visibility_level": 20,
241 | "path_with_namespace": "gitlab-org/gitlab-test",
242 | "default_branch": "master"
243 | },
244 | "commit": {
245 | "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
246 | "message": "test",
247 | "timestamp": "2016-08-12T17:23:21+02:00",
248 | "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
249 | "author": {
250 | "name": "User",
251 | "email": "user@gitlab.com"
252 | }
253 | },
254 | "builds": [
255 | {
256 | "id": 380,
257 | "stage": "deploy",
258 | "name": "production",
259 | "status": "skipped",
260 | "created_at": "2016-08-12 15:23:28 UTC",
261 | "started_at": null,
262 | "finished_at": null,
263 | "when": "manual",
264 | "manual": true,
265 | "allow_failure": false,
266 | "user": {
267 | "name": "Administrator",
268 | "username": "root",
269 | "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
270 | },
271 | "runner": null,
272 | "artifacts_file": {
273 | "filename": null,
274 | "size": null
275 | }
276 | },
277 | {
278 | "id": 377,
279 | "stage": "test",
280 | "name": "test-image",
281 | "status": "success",
282 | "created_at": "2016-08-12 15:23:28 UTC",
283 | "started_at": "2016-08-12 15:26:12 UTC",
284 | "finished_at": null,
285 | "when": "on_success",
286 | "manual": false,
287 | "allow_failure": false,
288 | "user": {
289 | "name": "Administrator",
290 | "username": "root",
291 | "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
292 | },
293 | "runner": {
294 | "id": 380987,
295 | "description": "shared-runners-manager-6.gitlab.com",
296 | "active": true,
297 | "is_shared": true
298 | },
299 | "artifacts_file": {
300 | "filename": null,
301 | "size": null
302 | }
303 | },
304 | {
305 | "id": 378,
306 | "stage": "test",
307 | "name": "test-build",
308 | "status": "success",
309 | "created_at": "2016-08-12 15:23:28 UTC",
310 | "started_at": "2016-08-12 15:26:12 UTC",
311 | "finished_at": "2016-08-12 15:26:29 UTC",
312 | "when": "on_success",
313 | "manual": false,
314 | "allow_failure": false,
315 | "user": {
316 | "name": "Administrator",
317 | "username": "root",
318 | "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
319 | },
320 | "runner": {
321 | "id": 380987,
322 | "description": "shared-runners-manager-6.gitlab.com",
323 | "active": true,
324 | "is_shared": true
325 | },
326 | "artifacts_file": {
327 | "filename": null,
328 | "size": null
329 | }
330 | },
331 | {
332 | "id": 376,
333 | "stage": "build",
334 | "name": "build-image",
335 | "status": "success",
336 | "created_at": "2016-08-12 15:23:28 UTC",
337 | "started_at": "2016-08-12 15:24:56 UTC",
338 | "finished_at": "2016-08-12 15:25:26 UTC",
339 | "when": "on_success",
340 | "manual": false,
341 | "allow_failure": false,
342 | "user": {
343 | "name": "Administrator",
344 | "username": "root",
345 | "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
346 | },
347 | "runner": {
348 | "id": 380987,
349 | "description": "shared-runners-manager-6.gitlab.com",
350 | "active": true,
351 | "is_shared": true
352 | },
353 | "artifacts_file": {
354 | "filename": null,
355 | "size": null
356 | }
357 | },
358 | {
359 | "id": 379,
360 | "stage": "deploy",
361 | "name": "staging",
362 | "status": "created",
363 | "created_at": "2016-08-12 15:23:28 UTC",
364 | "started_at": null,
365 | "finished_at": null,
366 | "when": "on_success",
367 | "manual": false,
368 | "allow_failure": false,
369 | "user": {
370 | "name": "Administrator",
371 | "username": "root",
372 | "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
373 | },
374 | "runner": null,
375 | "artifacts_file": {
376 | "filename": null,
377 | "size": null
378 | }
379 | }
380 | ]
381 | }
382 |
383 |
384 | {
385 | "object_kind": "push",
386 | "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
387 | "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
388 | "ref": "refs/heads/master",
389 | "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
390 | "user_id": 4,
391 | "user_name": "John Smith",
392 | "user_username": "jsmith",
393 | "user_email": "john@example.com",
394 | "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
395 | "project_id": 15,
396 | "project": {
397 | "id": 15,
398 | "name": "Diaspora",
399 | "description": "",
400 | "web_url": "http://example.com/mike/diaspora",
401 | "avatar_url": null,
402 | "git_ssh_url": "git@example.com:mike/diaspora.git",
403 | "git_http_url": "http://example.com/mike/diaspora.git",
404 | "namespace": "Mike",
405 | "visibility_level": 0,
406 | "path_with_namespace": "mike/diaspora",
407 | "default_branch": "master",
408 | "homepage": "http://example.com/mike/diaspora",
409 | "url": "git@example.com:mike/diaspora.git",
410 | "ssh_url": "git@example.com:mike/diaspora.git",
411 | "http_url": "http://example.com/mike/diaspora.git"
412 | },
413 | "repository": {
414 | "name": "Diaspora",
415 | "url": "git@example.com:mike/diaspora.git",
416 | "description": "",
417 | "homepage": "http://example.com/mike/diaspora",
418 | "git_http_url": "http://example.com/mike/diaspora.git",
419 | "git_ssh_url": "git@example.com:mike/diaspora.git",
420 | "visibility_level": 0
421 | },
422 | "commits": [
423 | {
424 | "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
425 | "message": "Update Catalan translation to e38cb41.",
426 | "timestamp": "2011-12-12T14:27:31+02:00",
427 | "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
428 | "author": {
429 | "name": "Jordi Mallach",
430 | "email": "jordi@softcatala.org"
431 | },
432 | "added": [
433 | "CHANGELOG"
434 | ],
435 | "modified": [
436 | "app/controller/application.rb"
437 | ],
438 | "removed": []
439 | },
440 | {
441 | "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
442 | "message": "fixed readme",
443 | "timestamp": "2012-01-03T23:36:29+02:00",
444 | "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
445 | "author": {
446 | "name": "GitLab dev user",
447 | "email": "gitlabdev@dv6700.(none)"
448 | },
449 | "added": [
450 | "CHANGELOG"
451 | ],
452 | "modified": [
453 | "app/controller/application.rb"
454 | ],
455 | "removed": []
456 | }
457 | ],
458 | "total_commits_count": 4
459 | }
--------------------------------------------------------------------------------
/test/app/controller/home.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { app, assert } = require('egg-mock/bootstrap');
4 |
5 | describe('test/app/controller/home.test.js', () => {
6 | it('should assert', () => {
7 | const pkg = require('../../../package.json');
8 | assert(app.config.keys.startsWith(pkg.name));
9 |
10 | // const ctx = app.mockContext({});
11 | // yield ctx.service.xx();
12 | });
13 |
14 | it('should GET /', () => {
15 | return app.httpRequest()
16 | .get('/')
17 | .expect('hi, egg')
18 | .expect(200);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------