├── .gitignore ├── LICENSE ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Richard Yang 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitlab-bot 2 | 使用gitlab 进行持续集成 3 | 4 | 5 | Gitlab Community Edition 有一套比较完善的[自动构建系统](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/ci/quick_start/README.md) 以及非常多的[hooks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/web_hooks/web_hooks.md)([还有这个](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/system_hooks/system_hooks.md)) 加上[trigger](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/ci/triggers/README.md) 机制可以灵活的控制build任务 6 | 7 | 那么利用这些个接口我们可以弄一个简单的自动构建机器人来提高团队之间的开发效率 8 | 9 | 首先进入项目的Settting页面 10 | 11 | ![add web hooks](http://ww1.sinaimg.cn/large/74311666jw1f1q9twodw5j20j60h8acf.jpg) 12 | 13 | 这儿订阅 push 事件 comments事件 和 merge Request 事件。 如果有需要还可以开启https支持 然后填写一个url(这个就是自己实现的一个bot) 点击添加就好 14 | 15 | 然后进入个人的Setting 页面获取一个private token 后面会用这个token来发消息 (建议创建一个 bot的帐号用独立的token) 16 | 17 | ![private token](http://ww1.sinaimg.cn/large/74311666jw1f1q9z5b8z0j21kw0j40vl.jpg) 18 | 19 | 最后进入项目的 Settings > Triggers页面 单击 Add trigger 按钮创建一个新的trigger token 20 | 这个token可以通过api来触发你的build任务从而实现自动化 21 | 22 | ![trigger](http://ww4.sinaimg.cn/large/74311666jw1f1qa39zq28j20u1094754.jpg) 23 | 24 | 这几个条件满足之后就可以开始开发机器人了,这儿我就用nodejs 写一个最简单的实现 25 | 26 | 我们可能想在开发发起merge request 合并到主分支的时候 只要review者通过且单元测试顺利则自动去部署(这儿可以是内部的测试环境, 生产环境则不建议) 27 | 28 | 那我们设计的机器人就这样,监听merge request 只要有人发起则回复一个消息询问是否要自动部署。当下次merge后则判断之前是否有需要自动部署的评论 如果有则用trigger 触发depoly的构建 : ) 29 | 30 | 大致的代码如下 31 | 32 | 33 | ``` 34 | app.use('/trigger-ci', function(req, res, next) { 35 | var object_kind = req.body.object_kind; 36 | var object_attributes = req.body.object_attributes; 37 | 38 | if (object_kind == 'note') { 39 | 40 | if (object_attributes.noteable_type == 'MergeRequest') { 41 | if (object_attributes.note == '大海') { 42 | console.log(object_attributes); 43 | MergeRequestNote(object_attributes.project_id, 44 | req.body.merge_request.id, 45 | '我们的征途是星辰大海'); 46 | } else if (object_attributes.note.trim() == '看美女') { 47 | GetWomenPhoto(function(url) { 48 | MergeRequestNote(object_attributes.project_id, 49 | req.body.merge_request.id, 50 | '![美女](' + url + ')'); 51 | }); 52 | } else if (object_attributes.note.trim() == '讲笑话') { 53 | GetJoke(function(joke) { 54 | MergeRequestNote(object_attributes.project_id, 55 | req.body.merge_request.id, 56 | joke.title + '\r\n\r\n' + joke.text); 57 | }); 58 | } else if (object_attributes.note.trim() == '合并后部署') { 59 | MergeRequestNote(object_attributes.project_id, 60 | req.body.merge_request.id, 61 | '好的! 当这个分支成功合并后 我会触发部署服务! \r\n ![fax_nick](http://ww1.sinaimg.cn/large/74311666jw1f1qaa18su7j20dw08zgmb.jpg)'); 62 | } 63 | } 64 | } else if (object_kind == 'merge_request') { 65 | 66 | if (object_attributes.action == 'open' || object_attributes.action == 'reopen') { 67 | 68 | MergeRequestNote(object_attributes.target_project_id, 69 | object_attributes.id, 70 | '您好! 尼克狐为您服务! 合并请求通过后需要我做些什么吗? 例如: 合并后部署 讲笑话 看美女 ? \r\n ![DC_W_LVGKY6H7RYX8F__WWI](http://ww1.sinaimg.cn/large/74311666jw1f1qa9kmjmcj206h08cdg2.jpg)'); 71 | } else if (object_attributes.action == 'merge') { 72 | //遍历commit 73 | GetMergeNotes(object_attributes.target_project_id, 74 | object_attributes.id, function(err, notes) { 75 | for (index in notes) { 76 | if (notes[index].body.trim() == '合并后部署') { 77 | GetCiID(object_attributes.target_project_id, function(err, ci) { 78 | InvokeTrigger(ci, object_attributes.target_branch, function(err, ciResult) { 79 | MergeRequestNote(object_attributes.target_project_id, 80 | object_attributes.id, '我已成功触发自动构建 [View build details](' + object_attributes.target.web_url + '/commit/' + JSON.parse(ciResult).commit.sha + '/builds)'); 81 | }); 82 | }); 83 | break; 84 | } 85 | } 86 | }); 87 | } 88 | } 89 | res.send("ok"); 90 | }); 91 | ``` 92 | 93 | 上面的代码实现了监听了合并请求和评论的事件,然后会对一些命令做出响应(这儿特意加了看美女和讲笑话的实现可以自动去随机获取哟~ 编码道路上不在枯燥!!!) 94 | 95 | PS: 我们的项目使用git的build做自动构建并且生成docker镜像push到仓库,然后会回掉k8s服务去做自动更新部署详情请看源码 96 | 97 | 98 | 上图看效果 99 | 100 | ![merge request](http://7xrn7f.com1.z0.glb.clouddn.com/16-3-9/2889275.jpg) 101 | 102 | ![merge depoly](http://ww4.sinaimg.cn/large/74311666jw1f1qajp0423j218e14646a.jpg) 103 | 104 | ![women](http://ww3.sinaimg.cn/large/74311666jw1f1qakbuwrrj2186134aep.jpg) 105 | 106 | ![merge](http://ww4.sinaimg.cn/large/74311666jw1f1qakrfbf2j218m08ojsk.jpg) 107 | 108 | ![build success](http://7xrn7f.com1.z0.glb.clouddn.com/16-3-9/41236685.jpg) 109 | 110 | 111 | ### 参考内容 112 | 113 | [0] https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/ci/quick_start/README.md 114 | 115 | [1] https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/web_hooks/web_hooks.md 116 | 117 | [2] https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/system_hooks/system_hooks.md 118 | 119 | [3] https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/ci/triggers/README.md 120 | 121 | [4] https://github.com/gitlabhq/gitlabhq/tree/master/doc/api 122 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var express = require('express'); 4 | var bodyParser = require('body-parser'); 5 | var k8s = require('k8s'); 6 | var moment = require('moment'); 7 | var fs = require('fs'); 8 | var app = express(); 9 | var request = require('request'); 10 | 11 | var kubectl = k8s.kubectl({ 12 | endpoint: 'http://master:8080', 13 | binary: '/usr/bin/kubectl' 14 | }); 15 | 16 | var GIT_HOST = 'YOU GIT SERRVER'; 17 | var PRIVATE_TOKEN = 'YOU PRIVATE TOKEN'; 18 | var TRIGGER_TOKEN = 'YOU TRIGGER TOKEN'; 19 | 20 | 21 | app.set('port', process.env.PORT || 3002); 22 | app.use(bodyParser.json()); 23 | 24 | 25 | //部署服务 26 | app.use('/depoly', function(req, res) { 27 | var relicationName = req.body.relicationName; 28 | var image = req.body.image; 29 | kubectl.rc.rollingUpdate(relicationName, image, function(err, data) { 30 | if (err == null) { 31 | console.log('部署似乎已经完成! www-dev'); 32 | } else { 33 | console.log('抱歉! 部署 失败!' + err); 34 | } 35 | }); 36 | res.send('ok'); 37 | }); 38 | 39 | app.use('/trigger-ci', function(req, res, next) { 40 | var object_kind = req.body.object_kind; 41 | var object_attributes = req.body.object_attributes; 42 | console.log('kind:' + object_kind); 43 | console.log(req.body); 44 | 45 | if (object_kind == 'note') { 46 | 47 | if (object_attributes.noteable_type == 'MergeRequest') { 48 | if (object_attributes.note == 'test') { 49 | console.log(object_attributes); 50 | MergeRequestNote(object_attributes.project_id, 51 | req.body.merge_request.id, 52 | '我们的征途是星辰大海'); 53 | } else if (object_attributes.note.trim() == '看美女') { 54 | GetWomenPhoto(function(url) { 55 | MergeRequestNote(object_attributes.project_id, 56 | req.body.merge_request.id, 57 | '![美女](' + url + ')'); 58 | }); 59 | } else if (object_attributes.note.trim() == '讲笑话') { 60 | GetJoke(function(joke) { 61 | MergeRequestNote(object_attributes.project_id, 62 | req.body.merge_request.id, 63 | joke.title + '\r\n\r\n' + joke.text); 64 | }); 65 | } else if (object_attributes.note.trim() == '合并后部署') { 66 | MergeRequestNote(object_attributes.project_id, 67 | req.body.merge_request.id, 68 | '好的! 当这个分支成功合并后 我会触发部署服务! \r\n ![fax_nick](http://ww1.sinaimg.cn/large/74311666jw1f1qaa18su7j20dw08zgmb.jpg)'); 69 | } 70 | } 71 | } else if (object_kind == 'merge_request') { 72 | 73 | if (object_attributes.action == 'open' || object_attributes.action == 'reopen') { 74 | 75 | MergeRequestNote(object_attributes.target_project_id, 76 | object_attributes.id, 77 | '您好! 尼克狐为您服务! 合并请求通过后需要我做些什么吗? 例如: 合并后部署 讲笑话 看美女 ? \r\n ![DC_W_LVGKY6H7RYX8F__WWI](http://ww1.sinaimg.cn/large/74311666jw1f1qa9kmjmcj206h08cdg2.jpg)'); 78 | } else if (object_attributes.action == 'merge') { 79 | //遍历commit 80 | GetMergeNotes(object_attributes.target_project_id, 81 | object_attributes.id, function(err, notes) { 82 | for (index in notes) { 83 | if (notes[index].body.trim() == '合并后部署') { 84 | GetCiID(object_attributes.target_project_id, function(err, ci) { 85 | InvokeTrigger(ci, object_attributes.target_branch, function(err, ciResult) { 86 | MergeRequestNote(object_attributes.target_project_id, 87 | object_attributes.id, '我已成功触发自动构建 [View build details](' + object_attributes.target.web_url + '/commit/' + JSON.parse(ciResult).commit.sha + '/builds)'); 88 | }); 89 | }); 90 | break; 91 | } 92 | } 93 | }); 94 | } 95 | } 96 | 97 | res.send("ok"); 98 | }); 99 | 100 | function GetMergeNotes(projectId, mergeRequestId, cb) { 101 | 102 | request({ 103 | url: GIT_HOST + '/api/v3/projects/' + projectId + 104 | '/merge_requests/' + mergeRequestId + '/notes', 105 | method: 'GET', 106 | headers: { 107 | 'PRIVATE-TOKEN': PRIVATE_TOKEN 108 | } 109 | }, function(err, response, body) { 110 | if (!err && response.statusCode == 200) { 111 | var data = JSON.parse(body) 112 | cb(null, data); 113 | } 114 | }); 115 | } 116 | 117 | //评论 118 | function MergeRequestNote(projectId, mergeRequestId, note) { 119 | request({ 120 | url: GIT_HOST + '/api/v3/projects/' + projectId + 121 | '/merge_requests/' + mergeRequestId + '/notes?body=' + note, 122 | method: 'POST', 123 | headers: { 124 | 'PRIVATE-TOKEN': PRIVATE_TOKEN 125 | } 126 | }); 127 | } 128 | 129 | //获取嘿嘿嘿 130 | function GetWomenPhoto(cb) { 131 | var i = Math.floor(Math.random() * 100); 132 | request.get('http://www.tngou.net/tnfs/api/news?id=' + i + '&rows=100', function(err, r, body) { 133 | if (!err && r.statusCode == 200) { 134 | var data = JSON.parse(body) 135 | var i = Math.floor(Math.random() * 100); 136 | cb && cb('http://tnfs.tngou.net/img' + data.tngou[i].img); 137 | } 138 | }); 139 | } 140 | 141 | //获取一个笑话 142 | function GetJoke(cb) { 143 | 144 | var year = moment().format("YYYY"); 145 | var timestamp = moment().format("YYYYMMDDHHmmss"); 146 | 147 | var i = Math.floor(Math.random() * 100); 148 | request.get('http://route.showapi.com/341-1?showapi_appid=16508&showapi_timestamp=' + timestamp + '&showapi_sign=58752887c0fd4e8aa42e2282182da999&time=' + year + '-01-01&page=' + i + '&maxResult=20&', function(err, r, body) { 149 | if (!err && r.statusCode == 200) { 150 | var data = JSON.parse(body); 151 | if (data.showapi_res_code == 0 && data.showapi_res_body.contentlist.length > 0) { 152 | var contentlist = data.showapi_res_body.contentlist; 153 | 154 | cb(contentlist[Math.floor(Math.random() * 20)]); 155 | } 156 | } 157 | }); 158 | } 159 | 160 | //触发一个构建 161 | //触发构建的时候携带了一个variables参数,在编写.gitlab-ci.yml脚本的时候可以判断这个参数从而进行自动部署 162 | //类似这样 163 | // - if [ -n "${DOCKER_BUILD}" ]; then 164 | // - YOU DEPOLY SCRIPT 165 | // - fi 166 | function InvokeTrigger(id, branch, cb) { 167 | request.post({ 168 | url: GIT_HOST + '/ci/api/v1/projects/' + id + '/refs/' + branch + '/trigger', 169 | form: { 170 | 'token': TRIGGER_TOKEN, 171 | 'variables[DOCKER_BUILD]': true 172 | } 173 | }, function(err, response, body) { 174 | cb && cb(err, body) 175 | }); 176 | 177 | } 178 | 179 | //获取ciID 180 | function GetCiID(project_id, cb) { 181 | request({ 182 | url: GIT_HOST + '/ci/api/v1/projects/', 183 | method: 'GET', 184 | headers: { 185 | 'PRIVATE-TOKEN': PRIVATE_TOKEN 186 | } 187 | }, function(err, response, body) { 188 | var ci = null; 189 | if (!err && response.statusCode == 200) { 190 | var data = JSON.parse(body) 191 | for (index in data) { 192 | if (data[index].gitlab_id == project_id) { 193 | ci = data[index].id; 194 | break; 195 | } 196 | } 197 | } 198 | cb(err, ci) 199 | }); 200 | } 201 | 202 | var server = app.listen(app.get('port'), function() { 203 | console.log("Server is start!"); 204 | }); 205 | 206 | process.on('SIGINT', function() { 207 | console.log("exit."); 208 | process.exit(0); 209 | }); 210 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitlab-bot", 3 | "version": "1.0.0", 4 | "description": "gitlab bot", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/beyondblog/gitlab-bot" 12 | }, 13 | "author": "beyondblog", 14 | "license": "MIT", 15 | "dependencies": { 16 | "body-parser": "^1.15.0", 17 | "express": "^4.13.4", 18 | "k8s": "^0.2.0", 19 | "moment": "^2.11.2" 20 | } 21 | } 22 | --------------------------------------------------------------------------------