├── tags └── .gitkeep ├── .gitignore ├── process.json ├── package.json ├── config.js ├── README.md ├── server.js └── .eslintrc.js /tags/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /process.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [{ 3 | "script": "server.js", 4 | "instances" : "1", 5 | "error_file": "../log/auto-tag.error.log", 6 | "out_file": "../log/auto-tag.access.log", 7 | "log_date_format": "YYYY-MM-DD HH:mm:ss Z", 8 | "kill_timeout" : 5000 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitlab-autotag", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/xiongwilee/gitlab-autotag.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "moment": "^2.18.1", 18 | "shelljs": "^0.7.8" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const moment = require('moment'); 4 | 5 | const workspacePath = `${__dirname}/tags/`; 6 | 7 | module.exports = { 8 | port: '2999', 9 | repo: { 10 | // Gitlab 的 group名称 11 | '{group名称}': { 12 | // 代码操作目录 13 | workspace: `${workspacePath}{group名称}-tags`, 14 | // 远程地址 15 | remote: '{gitlab地址}:{group名称}/', 16 | // 主分支 17 | master: 'online', 18 | // 获取tagName的方法,可直接使用cmd命令,例如exec(`shell command`) 19 | getTagName(hook_data, project_path) { 20 | return `online_${moment(new Date()).format('YYYYMMDDHHmmss')}`; 21 | }, 22 | // 获取tag message的方法,可直接使用cmd命令,例如exec(`shell command`) 23 | getTagMsg(hook_data, project_path) { 24 | return false; 25 | }, 26 | // GitlabToekn,自定义即可 27 | gitlabToken: '{gitlabToken}', 28 | // 钉钉机器人的token 29 | dingtalkToken: '{钉钉机器人的token}' 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitlab-autotag 2 | 3 | Merge Request后自动打tag并在钉钉群里通知; 4 | 5 | ### 示例 6 | 7 | Merge Request 自动打tag例如: 8 | ``` 9 | online_20171221195930 10 | ``` 11 | 12 | 钉钉群里通知的内容,参考: 13 | ``` 14 | MR合并提示 15 | MR仓储: auto_store 16 | 上线信息: upd : 测试tag 17 | 合并分支: f_wenqi_master 18 | 上线Tag: online_20171221195930 19 | 开发人员: luowenqi 20 | 合并人员: luowenqi 21 | PR详情: view merge request 22 | ``` 23 | 24 | ### 使用说明 25 | 26 | ### 1、 配置(`./config.js`) 27 | 28 | 详细说明如下: 29 | 30 | ``` 31 | // '{group名称}' 为group名称,再如:https://{gitlab地址}/{项目名称} 下的代码,则为'{项目名称}' 32 | '{group名称}': { 33 | // 代码操作目录 34 | workspace: `${workspacePath}{group名称}-tags`, 35 | // 远程地址 36 | remote: '{gitlab地址}:{group名称}/', 37 | // 主分支 38 | master: 'online', 39 | // 获取tagName的方法,可直接使用cmd命令,例如exec(`shell command`) 40 | getTagName(hook_data, project_path) { 41 | return `online_${moment(new Date()).format('YYYYMMDDHHmmss')}`; 42 | }, 43 | // 获取tag message的方法,可直接使用cmd命令,例如exec(`shell command`) 44 | getTagMsg(hook_data, project_path) { 45 | return false; 46 | }, 47 | // GitlabToekn,自定义即可 48 | gitlabToken: '{gitlabToken}', 49 | // 钉钉机器人的token 50 | dingtalkToken: '{钉钉机器人的token}' 51 | } 52 | ``` 53 | 54 | ### 2、添加gitlab的webhook(需至少master权限) 55 | 56 | 访问:https://{gitlab地址}/${group}/${repo_name}/settings/integrations 。**注意:这里需要当前登录用户的在该repo下的master权限**。 57 | 58 | 需填写: 59 | - URL: `http://${部署机器的IP}:2999` ,直接配置为这个地址即可; 60 | - Secret Token: `{gitlabToken}`,对应上述配置的gitlabToken参数; 61 | - Trigger: `Merge Request events` ,选中Merge Request events即可,其他的不需要选择; 62 | 63 | 然后,点击“Add webhook”按钮即可。 64 | 65 | ### 3、添加钉钉群的群机器人 66 | 67 | **第一步:添加机器人** 68 | 69 | 到你想要的添加的群通知机器人的群,点击右上角的机器人的icon:`进入“群机器人”管理界面` → `点击最下方的“自定义(通过Webhook接入自定义服务)` → `点击“添加”按钮`; 70 | 71 | 你会得到一个链接。 72 | 73 | **第二步:获取dingtalkToken** 74 | 75 | 在上一步你得到的链接中的`access_token`参数就对应配置中的`dingtalkToken`,配置即可。 76 | 77 | 78 | 至此,对应仓储merge request之后自动打tag和merge值之后群里通知功能的配置就已经完成了。 79 | 80 | ### 4、启动服务 81 | 82 | ``` 83 | $ pm2 start process.json 84 | ``` 85 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('shelljs/global'); 4 | 5 | const http = require('http'); 6 | const config = require('./config'); 7 | 8 | createServer(); 9 | 10 | /** 11 | * 创建server 12 | * @return undefined 13 | */ 14 | function createServer() { 15 | http.createServer(function (req, res) { 16 | let data = ''; 17 | 18 | req.setEncoding('utf-8'); 19 | req.addListener('data', (chunk) => { 20 | data += chunk; 21 | }); 22 | 23 | req.addListener('end', () => { 24 | console.log(data); 25 | 26 | try { 27 | const result = init(req, JSON.parse(data)); 28 | res.write(JSON.stringify(result)); 29 | } catch (err) { 30 | console.error(err) 31 | } 32 | 33 | res.end(); 34 | }); 35 | 36 | }).listen(config.port); 37 | 38 | console.log('启动tag服务') 39 | } 40 | 41 | /** 42 | * 初始化方法 43 | * @param {Object} data [description] 44 | * @return undefined 45 | */ 46 | function init(req, data) { 47 | if (!data || !req) return; 48 | 49 | // 通过当前请求获取配置 50 | const cfg = getConfig(data, config); 51 | 52 | // 如果token与gitlab中不匹配,则返回 53 | if (req.headers['x-gitlab-token'] !== cfg.gitlabToken) { 54 | return { code: 403, message: 'Check Gitlab Token Error!' }; 55 | } 56 | // 如果当前不是mergerequest则直接返回 57 | if (!isMergedRequest(data, cfg)) { 58 | return { code: 201, message: 'Check Merge Request Status Error!' }; 59 | } 60 | 61 | // 创建tag 62 | const tagName = createTag(data, cfg); 63 | 64 | // 推送钉钉消息 65 | if (cfg.dingtalkToken) { 66 | sendMsg(data, cfg, tagName); 67 | } 68 | 69 | return { code: 200, message: '' } 70 | 71 | /** 72 | * 判断当前的事件是否为merge request 73 | * @param {Object} data [description] 74 | * @param {Object} cfg 配置项 75 | * @return {Boolean} [description] 76 | */ 77 | function isMergedRequest(data, cfg) { 78 | if (data.object_kind !== 'merge_request') return false; 79 | 80 | // 是否为merge request完成 81 | const isMerged = data.object_attributes && data.object_attributes.state === 'merged'; 82 | // 是否为merge到主分支 83 | const isTargetBranch = data.object_attributes && data.object_attributes.target_branch === cfg.master; 84 | 85 | return isMerged && isTargetBranch; 86 | } 87 | } 88 | 89 | /** 90 | * 通过请求内容和整体配置,获取当前repo的配置 91 | * @param {Object} data [description] 92 | * @param {Object} config [description] 93 | * @return {Object} [description] 94 | */ 95 | function getConfig(data, config) { 96 | const namespace = data.project.namespace; 97 | 98 | return config.repo[namespace] || config.repo['fe']; 99 | } 100 | 101 | /** 102 | * 发送钉钉消息 103 | * @param {Object} data 响应数据 104 | * @return 105 | */ 106 | function sendMsg(data, cfg, tagName) { 107 | // 获取文案 108 | const text = getArgs(data, cfg, tagName); 109 | 110 | // 推送钉钉消息 111 | sendDingtalkMessage(text, cfg); 112 | 113 | 114 | /** 115 | * 发送钉钉消息 116 | * @param {String} text [description] 117 | * @return undefined 118 | */ 119 | function sendDingtalkMessage(text, cfg) { 120 | console.log(text); 121 | 122 | const token = cfg.dingtalkToken; 123 | const url = 'https://oapi.dingtalk.com/robot/send?access_token='; 124 | 125 | return exec(`curl -H 'Content-Type: application/json' -X POST -d '${text}' ${url}${token}`); 126 | } 127 | 128 | /** 129 | * 生成钉钉推送的文案 130 | * @param {Object} data [description] 131 | * @return {String} [description] 132 | */ 133 | function getArgs(data, cfg, tagName) { 134 | const obj_attr = data.object_attributes || {}; 135 | const assignee = data.assignee || {}; 136 | const last_commit = obj_attr.last_commit || {}; 137 | const author = last_commit.author || {}; 138 | const resultData = { 139 | msgtype: 'markdown', 140 | markdown: { 141 | title: 'Merge Request', 142 | // 模板字符串页支持换行,但是不支持友好的缩进,所以的通过+连接字符串 143 | text: `MergeRequest提示\n\n` + 144 | `> MR仓储: ${data.project.name}\n\n` + 145 | `> 上线信息: ${obj_attr.title}\n\n` + 146 | `> 合并分支: ${obj_attr.source_branch}\n\n` + 147 | `> 上线Tag: **${tagName}**\n\n` + 148 | `> 开发人员: ${author.name}\n\n` + 149 | `> 合并人员: ${assignee.name || assignee.username || author.name}\n\n` + 150 | `> MR详情: [view merge request](${obj_attr.url})` 151 | } 152 | } 153 | 154 | return JSON.stringify(resultData); 155 | } 156 | } 157 | 158 | /** 159 | * 绑定gitlab的merge_request的钩子,自动创建tag 160 | * 161 | */ 162 | function createTag(data, cfg) { 163 | // 当前的repo名称 164 | const repo = data.project.name; 165 | // 远程地址 166 | const remote = cfg.remote; 167 | // 工作目录 168 | const workspace = cfg.workspace; 169 | // 仓储目录 170 | const project_path = `${workspace}/${repo}`; 171 | // 远程主干 172 | const master = cfg.master; 173 | 174 | if (!test('-d', workspace)) { 175 | exec(`mkdir ${workspace}`); 176 | } 177 | 178 | if (!test('-d', `${project_path}`)) { 179 | exec(`git clone -b ${master} ${remote}${repo}.git ${repo}`, { cwd: `${workspace}` }); 180 | } 181 | 182 | exec(`git fetch`, { cwd: `${project_path}` }); 183 | exec(`git clean -df`, { cwd: `${project_path}` }); 184 | exec(`git checkout ${master}`, { cwd: `${project_path}` }); 185 | exec(`git pull origin ${master}`, { cwd: `${project_path}` }); 186 | 187 | // 获取tag名称 188 | const tagName = cfg.getTagName.call(cfg, data, project_path); 189 | const tagMsg = cfg.getTagMsg.call(cfg, data, project_path); 190 | 191 | let tagCmd = `git tag -a ${tagName}`; 192 | if (tagMsg) tagCmd += ' -m ${tagMsg}'; 193 | 194 | exec(tagCmd, { cwd: `${project_path}` }); 195 | exec(`git push origin ${tagName}`, { cwd: `${project_path}` }); 196 | 197 | return { tagName, tagMsg }; 198 | } 199 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 2017, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "accessor-pairs": "error", 13 | "array-bracket-newline": "error", 14 | "array-bracket-spacing": "error", 15 | "array-callback-return": "error", 16 | "array-element-newline": "error", 17 | "arrow-body-style": "error", 18 | "arrow-parens": [ 19 | "error", 20 | "always" 21 | ], 22 | "arrow-spacing": [ 23 | "error", 24 | { 25 | "after": true, 26 | "before": true 27 | } 28 | ], 29 | "block-scoped-var": "error", 30 | "block-spacing": "error", 31 | "brace-style": [ 32 | "error", 33 | "1tbs" 34 | ], 35 | "callback-return": "error", 36 | "camelcase": "off", 37 | "capitalized-comments": "error", 38 | "class-methods-use-this": "error", 39 | "comma-dangle": "error", 40 | "comma-spacing": [ 41 | "error", 42 | { 43 | "after": true, 44 | "before": false 45 | } 46 | ], 47 | "comma-style": [ 48 | "error", 49 | "last" 50 | ], 51 | "complexity": "error", 52 | "computed-property-spacing": [ 53 | "error", 54 | "never" 55 | ], 56 | "consistent-return": "off", 57 | "consistent-this": "error", 58 | "curly": "off", 59 | "default-case": "error", 60 | "dot-location": "error", 61 | "dot-notation": "off", 62 | "eol-last": "error", 63 | "eqeqeq": "error", 64 | "func-call-spacing": "error", 65 | "func-name-matching": "error", 66 | "func-names": "off", 67 | "func-style": [ 68 | "error", 69 | "declaration" 70 | ], 71 | "function-paren-newline": "error", 72 | "generator-star-spacing": "error", 73 | "global-require": "error", 74 | "guard-for-in": "error", 75 | "handle-callback-err": "error", 76 | "id-blacklist": "error", 77 | "id-length": "error", 78 | "id-match": "error", 79 | "implicit-arrow-linebreak": [ 80 | "error", 81 | "beside" 82 | ], 83 | "indent": "off", 84 | "indent-legacy": "off", 85 | "init-declarations": "error", 86 | "jsx-quotes": "error", 87 | "key-spacing": "error", 88 | "keyword-spacing": [ 89 | "error", 90 | { 91 | "after": true, 92 | "before": true 93 | } 94 | ], 95 | "line-comment-position": "error", 96 | "linebreak-style": [ 97 | "error", 98 | "unix" 99 | ], 100 | "lines-around-comment": "error", 101 | "lines-around-directive": "error", 102 | "lines-between-class-members": "error", 103 | "max-classes-per-file": "error", 104 | "max-depth": "error", 105 | "max-len": "off", 106 | "max-lines": "error", 107 | "max-lines-per-function": "off", 108 | "max-nested-callbacks": "error", 109 | "max-params": "error", 110 | "max-statements": "off", 111 | "max-statements-per-line": "error", 112 | "multiline-comment-style": "error", 113 | "multiline-ternary": "error", 114 | "new-cap": "error", 115 | "new-parens": "error", 116 | "newline-after-var": "off", 117 | "newline-before-return": "error", 118 | "newline-per-chained-call": "error", 119 | "no-alert": "error", 120 | "no-array-constructor": "error", 121 | "no-async-promise-executor": "error", 122 | "no-await-in-loop": "error", 123 | "no-bitwise": "error", 124 | "no-buffer-constructor": "error", 125 | "no-caller": "error", 126 | "no-catch-shadow": "error", 127 | "no-confusing-arrow": "error", 128 | "no-continue": "error", 129 | "no-div-regex": "error", 130 | "no-duplicate-imports": "error", 131 | "no-else-return": "error", 132 | "no-empty-function": "error", 133 | "no-eq-null": "error", 134 | "no-eval": "error", 135 | "no-extend-native": "error", 136 | "no-extra-bind": "error", 137 | "no-extra-label": "error", 138 | "no-extra-parens": "error", 139 | "no-floating-decimal": "error", 140 | "no-implicit-coercion": "error", 141 | "no-implicit-globals": "error", 142 | "no-implied-eval": "error", 143 | "no-inline-comments": "error", 144 | "no-invalid-this": "error", 145 | "no-iterator": "error", 146 | "no-label-var": "error", 147 | "no-labels": "error", 148 | "no-lone-blocks": "error", 149 | "no-lonely-if": "error", 150 | "no-loop-func": "error", 151 | "no-magic-numbers": "error", 152 | "no-misleading-character-class": "error", 153 | "no-mixed-operators": "error", 154 | "no-mixed-requires": "error", 155 | "no-multi-assign": "error", 156 | "no-multi-spaces": "error", 157 | "no-multi-str": "error", 158 | "no-multiple-empty-lines": "error", 159 | "no-native-reassign": "error", 160 | "no-negated-condition": "error", 161 | "no-negated-in-lhs": "error", 162 | "no-nested-ternary": "error", 163 | "no-new": "error", 164 | "no-new-func": "error", 165 | "no-new-object": "error", 166 | "no-new-require": "error", 167 | "no-new-wrappers": "error", 168 | "no-octal-escape": "error", 169 | "no-param-reassign": "error", 170 | "no-path-concat": "error", 171 | "no-plusplus": "error", 172 | "no-process-env": "error", 173 | "no-process-exit": "error", 174 | "no-proto": "error", 175 | "no-prototype-builtins": "error", 176 | "no-restricted-globals": "error", 177 | "no-restricted-imports": "error", 178 | "no-restricted-modules": "error", 179 | "no-restricted-properties": "error", 180 | "no-restricted-syntax": "error", 181 | "no-return-assign": "error", 182 | "no-return-await": "error", 183 | "no-script-url": "error", 184 | "no-self-compare": "error", 185 | "no-sequences": "error", 186 | "no-shadow": "off", 187 | "no-shadow-restricted-names": "error", 188 | "no-spaced-func": "error", 189 | "no-sync": "error", 190 | "no-tabs": "error", 191 | "no-template-curly-in-string": "off", 192 | "no-ternary": "error", 193 | "no-throw-literal": "error", 194 | "no-undef-init": "error", 195 | "no-undefined": "error", 196 | "no-underscore-dangle": "error", 197 | "no-unmodified-loop-condition": "error", 198 | "no-unneeded-ternary": "error", 199 | "no-unused-expressions": "error", 200 | "no-use-before-define": "off", 201 | "no-useless-call": "off", 202 | "no-useless-computed-key": "error", 203 | "no-useless-concat": "error", 204 | "no-useless-constructor": "error", 205 | "no-useless-rename": "error", 206 | "no-useless-return": "error", 207 | "no-var": "error", 208 | "no-void": "error", 209 | "no-warning-comments": "error", 210 | "no-whitespace-before-property": "error", 211 | "no-with": "error", 212 | "nonblock-statement-body-position": "error", 213 | "object-curly-newline": "error", 214 | "object-curly-spacing": [ 215 | "error", 216 | "always" 217 | ], 218 | "object-shorthand": "error", 219 | "one-var": "off", 220 | "one-var-declaration-per-line": "error", 221 | "operator-assignment": [ 222 | "error", 223 | "always" 224 | ], 225 | "operator-linebreak": "error", 226 | "padded-blocks": "off", 227 | "padding-line-between-statements": "error", 228 | "prefer-arrow-callback": "off", 229 | "prefer-const": "error", 230 | "prefer-destructuring": "off", 231 | "prefer-numeric-literals": "error", 232 | "prefer-object-spread": "error", 233 | "prefer-promise-reject-errors": "error", 234 | "prefer-reflect": "off", 235 | "prefer-rest-params": "error", 236 | "prefer-spread": "error", 237 | "prefer-template": "error", 238 | "quote-props": "off", 239 | "quotes": "off", 240 | "radix": "error", 241 | "require-atomic-updates": "error", 242 | "require-await": "error", 243 | "require-jsdoc": "error", 244 | "require-unicode-regexp": "error", 245 | "rest-spread-spacing": "error", 246 | "semi": "off", 247 | "semi-spacing": "error", 248 | "semi-style": [ 249 | "error", 250 | "last" 251 | ], 252 | "sort-imports": "error", 253 | "sort-keys": "off", 254 | "sort-vars": "error", 255 | "space-before-blocks": "error", 256 | "space-before-function-paren": "off", 257 | "space-in-parens": [ 258 | "error", 259 | "never" 260 | ], 261 | "space-infix-ops": "error", 262 | "space-unary-ops": "error", 263 | "spaced-comment": [ 264 | "error", 265 | "always" 266 | ], 267 | "strict": "off", 268 | "switch-colon-spacing": "error", 269 | "symbol-description": "error", 270 | "template-curly-spacing": [ 271 | "error", 272 | "never" 273 | ], 274 | "template-tag-spacing": "error", 275 | "unicode-bom": [ 276 | "error", 277 | "never" 278 | ], 279 | "valid-jsdoc": "off", 280 | "vars-on-top": "error", 281 | "wrap-iife": "error", 282 | "wrap-regex": "error", 283 | "yield-star-spacing": "error", 284 | "yoda": [ 285 | "error", 286 | "never" 287 | ] 288 | } 289 | }; --------------------------------------------------------------------------------