├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── gitlabhook-server.js ├── gitlabhook.conf ├── gitlabhook.js ├── gitlabhook.service └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .tgz 2 | node_modules/ 3 | tmp/ 4 | package/ 5 | NOTIZEN 6 | Makefile.spec 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Rolf Niepraschk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Rolf Niepraschk, 2016-01-05, Rolf.Niepraschk@gmx.de 2 | 3 | MAIN = gitlabhook 4 | VERSION = $(shell awk -F '["]' '/version/ {print $$4}' package.json) 5 | SPEC_FILE = $(MAIN).spec 6 | SOURCE = $(MAIN)-$(VERSION).tgz 7 | 8 | ECHO = @echo -e 9 | 10 | rpm : clean $(SPEC_FILE) 11 | rm -rf package 12 | mkdir package 13 | cp -p gitlabhook-server.js package/ 14 | cp -p gitlabhook.js package/ 15 | cp -p gitlabhook.conf package/ 16 | cp -p README.md package/ 17 | cp -p LICENSE package/ 18 | cp -p package.json package/ 19 | cp -p gitlabhook.service package/ 20 | ( cd package/ && /usr/bin/npm install ) 21 | tar cvzf $(SOURCE) $(SPEC_FILE) package/ 22 | rpmbuild -ta $(SOURCE) 23 | 24 | $(SPEC_FILE) : 25 | $(MAKE) -f Makefile.spec 26 | 27 | clean : 28 | rm -rf $(SOURCE) package/ $(SPEC_FILE) 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-gitlab-hook 2 | 3 | This is an easy to use nodeJS based web hook for GitLab. 4 | 5 | ## To Install: 6 | ``` 7 | npm install gitlabhook 8 | ``` 9 | 10 | ## To Use: 11 | 12 | ```javascript 13 | var gitlabhook = require('gitlabhook'); 14 | var gitlab = gitlabhook({/* options */} [, callback]); 15 | 16 | gitlab.listen(); 17 | ``` 18 | 19 | Configure a WebHook URL to whereever the server is listening. 20 | 21 | ### Available options are: 22 | 23 | * **host**: the host to listen on, defaults to `0.0.0.0` 24 | * **port**: the port to listen on, defaults to `3420` 25 | * **configFile**: the json config file. May located at **configPathes**, defaults to `gitlabhook.conf` 26 | * **configPathes**: the search pathes for **configFile**, defaults to `['/etc/gitlabhook/', '/usr/local/etc/gitlabhook/', '.']` 27 | * **keep**: if true, temporary files are not deleted, defaults to `false`. Mostly only for debugging purposes. 28 | * **logger**: an optional instance of a logger that supports the "info" and "error" methods and one parameter for data (like console), default is to not log (`logger:{info:function(s){}, error:function(s){}}`). Mostly only for debugging purposes. 29 | * **tasks**: relations between repositories and shell commands (e.g. `{repo1:'cmd1', repo2:['cmd2a','cmd2b','cmd2c']}`) 30 | * **cmdshell**: the command-line interpreter to be used, defaults to `/bin/sh` 31 | 32 | The config file will be ignored if a callback function is declared. 33 | 34 | Example config file with task definitions: 35 | 36 | ```javascript 37 | { 38 | "tasks": { 39 | "myRepo": "/usr/local/bin/myDeploy %g", 40 | "*": ["echo 'GitLab Server %s'", 41 | "echo 'Repository: %r'", 42 | "echo 'Event: %k'", 43 | "echo 'User: %u'", 44 | "echo 'Branch: %b'", 45 | "echo 'Git Url: %g'", 46 | "echo 'Last Commit: %i'", 47 | "echo '\tMessage: %m'", 48 | "echo '\tTime: %t'"] 49 | }, 50 | "keep":false, 51 | "logger": false, 52 | "cmdshell":"/bin/bash" 53 | } 54 | ``` 55 | The `*` matches any tasks. 56 | 57 | The place holders are: 58 | 59 | * `%s`: GitLab server's IP address 60 | * `%r`: name of the repository (e.g. `myRepo`) 61 | * `%k`: kind of event (e.g. `tag_push`) 62 | * `%u`: owner of the repository (user name) 63 | * `%b`: branch reference (e.g. `refs/heads/master`) 64 | * `%g`: ssh-based cloning url on the GitLab server (e.g. `git@gitlab.host:rolf.niepraschk/myRepo.git`) 65 | * `%h`: http-based cloning url on the GitLab server (e.g. `http://gitlab.host/rolf.niepraschk/myRepo.git`) 66 | * `%i`: id of the last commit 67 | * `%t`: timestamp of the last commit 68 | * `%m`: message of the last commit 69 | 70 | The file `gitlabhook-server.js` shows an example GitLab Hook server listen at port 3420. 71 | 72 | ## Installation hints for Linux 73 | 74 | The file `gitlabhook.service` is intended to use as a systemd sercvice. The `Makefile` helps to create an rpm archive for a systemd based OS. Call 75 | ``` 76 | make rpm 77 | ``` 78 | 79 | # License 80 | 81 | MIT 82 | -------------------------------------------------------------------------------- /gitlabhook-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var util = require('util'); 4 | 5 | var PORT = 3420; 6 | 7 | var logger = { 8 | info: console.log, 9 | error: console.log 10 | }; 11 | 12 | var glh = { 13 | port: PORT, 14 | host: '0.0.0.0', 15 | logger: logger 16 | }; 17 | 18 | // With an additional callback function the "gitlabhook.conf" will be ignored. 19 | var server = require('gitlabhook')(glh); 20 | server.listen(); 21 | if (server.server) logger.info('webhook server listen (%d)\n', PORT); 22 | 23 | /* 24 | http://localhost:3420 25 | */ 26 | -------------------------------------------------------------------------------- /gitlabhook.conf: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "*": [ 4 | "echo 'GitLab Server: %s' > /tmp/gitlabhook.tmp", 5 | "echo 'Repository: %r' >> /tmp/gitlabhook.tmp", 6 | "echo $(date) >> /tmp/gitlabhook.tmp" 7 | ] 8 | }, 9 | "keep":false 10 | } 11 | -------------------------------------------------------------------------------- /gitlabhook.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Rolf Niepraschk (Rolf.Niepraschk@gmx.de) 4 | Inspired by https://github.com/nlf/node-github-hook 5 | */ 6 | 7 | var Http = require('http'); 8 | var Url = require('url'); 9 | var Fs = require('fs'); 10 | var execFile = require('child_process').execFile; 11 | var Path = require('path'); 12 | var Os = require('os'); 13 | var Tmp = require('temp'); Tmp.track(); 14 | var Util = require('util'); 15 | var inspect = Util.inspect; 16 | var isArray = Util.isArray; 17 | 18 | var GitLabHook = function(_options, _callback) { 19 | if (!(this instanceof GitLabHook)) return new GitLabHook(_options, _callback); 20 | var callback = null, options = null; 21 | if (typeof _options === 'function') { 22 | callback = _options; 23 | } else { 24 | callback = _callback; 25 | options = _options; 26 | } 27 | options = options || {}; 28 | this.configFile = options.configFile || 'gitlabhook.conf'; 29 | this.configPathes = options.configPathes || 30 | ['/etc/gitlabhook/', '/usr/local/etc/gitlabhook/', '.']; 31 | this.port = options.port || 3420; 32 | this.host = options.host || '0.0.0.0'; 33 | this.cmdshell = options.cmdshell || '/bin/sh'; 34 | this.keep = (typeof options.keep === 'undefined') ? false : options.keep; 35 | this.logger = options.logger; 36 | this.callback = callback; 37 | 38 | var active = false, tasks; 39 | 40 | if (typeof callback == 'function') { 41 | active = true; 42 | } else { 43 | cfg = readConfigFile(this.configPathes, this.configFile); 44 | if (cfg) { 45 | this.logger.info('loading config file: ' + this.configFile); 46 | this.logger.info('config file:\n' + Util.inspect(cfg)); 47 | for (var i in cfg) { 48 | if (i == 'tasks' && typeof cfg.tasks == 'object' && 49 | Object.keys(cfg.tasks).length) { 50 | this.tasks = cfg.tasks; 51 | active = true; 52 | } else { 53 | this[i] = cfg[i]; 54 | } 55 | } 56 | } else { 57 | this.logger.error("can't read config file: ", this.configFile); 58 | } 59 | } 60 | 61 | this.logger = this.logger || { info: function(){}, error: function(){} }; 62 | 63 | this.logger.info('self: ' + inspect(this) + '\n'); 64 | 65 | if (active) this.server = Http.createServer(serverHandler.bind(this)); 66 | }; 67 | 68 | GitLabHook.prototype.listen = function(callback) { 69 | var self = this; 70 | if (typeof self.server !== 'undefined') { 71 | self.server.listen(self.port, self.host, function () { 72 | self.logger.info(Util.format( 73 | 'listening for github events on %s:%d', self.host, self.port)); 74 | if (typeof callback === 'function') callback(); 75 | }); 76 | } else { 77 | self.logger.info('server disabled'); 78 | } 79 | }; 80 | 81 | function readConfigFile(pathes, file) { 82 | var fname, ret = false; 83 | for (var i=0;i 0) { 163 | 164 | self.logger.info('cmds: ' + inspect(cmds) + '\n'); 165 | 166 | Tmp.mkdir({dir:Os.tmpDir(), prefix:'gitlabhook.'}, function(err, path) { 167 | if (err) { 168 | self.logger.error(err); 169 | return; 170 | } 171 | self.path = path; 172 | self.logger.info('Tempdir: ' + path); 173 | var i = 0; 174 | execute(path, i); 175 | }); 176 | 177 | } else { 178 | self.logger.info('No related commands for repository "' + repo + '"'); 179 | } 180 | } 181 | 182 | function serverHandler(req, res) { 183 | var self = this; 184 | var url = Url.parse(req.url, true); 185 | var buffer = []; 186 | var bufferLength = 0; 187 | var failed = false; 188 | var remoteAddress = req.ip || req.socket.remoteAddress || 189 | req.socket.socket.remoteAddress; 190 | 191 | req.on('data', function (chunk) { 192 | if (failed) return; 193 | buffer.push(chunk); 194 | bufferLength += chunk.length; 195 | }); 196 | 197 | req.on('end', function (chunk) { 198 | if (failed) return; 199 | var data; 200 | 201 | if (chunk) { 202 | buffer.push(chunk); 203 | bufferLength += chunk.length; 204 | } 205 | 206 | self.logger.info(Util.format('received %d bytes from %s\n\n', bufferLength, 207 | remoteAddress)); 208 | 209 | data = Buffer.concat(buffer, bufferLength).toString(); 210 | data = parse(data); 211 | 212 | // invalid json 213 | if (!data || !data.repository || !data.repository.name) { 214 | self.logger.error(Util.format('received invalid data from %s, returning 400\n\n', 215 | remoteAddress)); 216 | return reply(400, res); 217 | } 218 | 219 | var repo = data.repository.name.replace(/[&|;$`]/gi, ""); 220 | 221 | reply(200, res); 222 | 223 | self.logger.info(Util.format('got event on %s:%s from %s\n\n', repo, data.ref, 224 | remoteAddress)); 225 | self.logger.info(Util.inspect(data, { showHidden: true, depth: 10 }) + '\n\n'); 226 | 227 | 228 | if (typeof self.callback == 'function') { 229 | self.callback(data); 230 | } else { 231 | executeShellCmds(self, remoteAddress, data); 232 | } 233 | 234 | }); 235 | 236 | // 405 if the method is wrong 237 | if (req.method !== 'POST') { 238 | self.logger.error(Util.format('got invalid method from %s, returning 405', 239 | remoteAddress)); 240 | failed = true; 241 | return reply(405, res); 242 | } 243 | 244 | } 245 | 246 | function getCmds(tasks, map, repo) { 247 | var ret = [], x = []; 248 | if (tasks.hasOwnProperty('*')) x.push(tasks['*']); 249 | if (tasks.hasOwnProperty(repo)) x.push(tasks[repo]); 250 | for (var i=0; i= width ? n : new Array(width - n.length + 1).join(z) + n; 262 | } 263 | 264 | module.exports = GitLabHook; 265 | 266 | -------------------------------------------------------------------------------- /gitlabhook.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Hook Server for GitLab 3 | After=network.target 4 | 5 | [Service] 6 | User=wwwrun 7 | Group=www 8 | Type=simple 9 | StandardOutput=journal 10 | StandardError=journal 11 | Restart=always 12 | # Run ExecStartPre/ExecStartPost with root-permissions 13 | PermissionsStartOnly=true 14 | # Remove PIDFile at Stop 15 | PIDFile=/var/run/gitlabhook/gitlabhookd.pid 16 | ExecStartPre=/usr/bin/mkdir -p /var/run/gitlabhook 17 | ExecStartPost=/bin/sh -c '/usr/bin/echo $MAINPID > /var/run/gitlabhook/gitlabhookd.pid' 18 | ExecStart=/usr/bin/node /usr/lib/node_modules/gitlabhook/gitlabhook-server.js 19 | 20 | [Install] 21 | WantedBy=multi-user.target 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitlabhook", 3 | "version": "0.0.16", 4 | "description": "This is an easy to use nodeJS based web hook for GitLab.", 5 | "main": "gitlabhook.js", 6 | "dependencies": { 7 | "temp": ">=0.6.0" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/rolfn/node-gitlab-hook.git" 16 | }, 17 | "keywords": [ 18 | "GitLab", 19 | "webhook" 20 | ], 21 | "author": "Rolf Niepraschk ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/rolfn/node-gitlab-hook/issues" 25 | } 26 | } 27 | --------------------------------------------------------------------------------