├── .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 | ![alt gitlab-push-msg-1](./docs/gitlab-push-msg-1.png) 10 | 11 | Gitlab push 新建分支 12 | 13 | ![alt gitlab-push-msg-2](./docs/gitlab-push-msg-2.png) 14 | 15 | Gitlab push 删除分支 16 | 17 | ![alt gitlab-push-msg-3](./docs/gitlab-push-msg-3.png) 18 | 19 | Gitlab push tag 推标签 20 | 21 | ![alt gitlab-push-tag-msg-1](./docs/gitlab-push-tag-msg-1.png) 22 | 23 | Gitlab merge request 合并请求 24 | 25 | ![alt gitlab-mr-msg-1](./docs/gitlab-mr-msg-1.png) 26 | 27 | Gitlab pipeline 流水线 28 | 29 | ![alt gitlab-pipeline-msg](./docs/gitlab-pipeline-msg-1.png) 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 | ![alt gitlab集成图片](./docs/gitlab-integration-1.png) 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 | --------------------------------------------------------------------------------