diff options
-rw-r--r-- | .gitignore | 62 | ||||
-rw-r--r-- | Dockerfile | 2 | ||||
-rw-r--r-- | GitAutoDeploy.conf.json.example | 16 | ||||
-rwxr-xr-x | GitAutoDeploy.py | 817 | ||||
-rw-r--r-- | README.md | 73 | ||||
-rw-r--r-- | clear_lock.sh | 2 | ||||
-rw-r--r-- | lock.sh | 27 | ||||
-rw-r--r-- | unlock.sh | 2 |
8 files changed, 615 insertions, 386 deletions
@@ -1 +1,63 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + GitAutoDeploy.conf.json +.idea +.DS_Store +._.DS_Store @@ -1,7 +1,7 @@ FROM google/python-runtime RUN apt-get -y install openssh-client -RUN mkdir $HOME/.ssh && chmod 700 $HOME/.ssh +RUN mkdir $HOME/.ssh && chmod 600 $HOME/.ssh COPY deploy_rsa /root/.ssh/id_rsa ENTRYPOINT ["/env/bin/python", "-u", "GitAutoDeploy.py", "--ssh-keyscan"] diff --git a/GitAutoDeploy.conf.json.example b/GitAutoDeploy.conf.json.example index b3b2c4e..5e038f1 100644 --- a/GitAutoDeploy.conf.json.example +++ b/GitAutoDeploy.conf.json.example @@ -1,20 +1,20 @@ { - "pidfilepath": "/var/run/gitautodeploy/gitautodeploy.pid", - "host": "localhost", + "pidfilepath": "~/.gitautodeploy.pid", + "host": "0.0.0.0", "port": 8001, "global_deploy": [ - "echo Starting deploy!", - "echo Ending deploy!" + "echo Deploy started!", + "echo Deploy completed!" ], "repositories": [{ - "url": "https://github.com/logsol/Test-Repo", + "url": "https://github.com/olipo186/Github-Gitlab-Auto-Deploy.git", "branch": "master", - "path": "/home/logsol/projects/Test-Repo", + "path": "~/repositories/Github-Gitlab-Auto-Deploy", "deploy": "echo deploying" }, { - "url": "https://github.com/logsol/Katharsis-Framework", - "path": "/home/logsol/projects/Katharsis-Framework" + "url": "https://github.com/github/gitignore", + "path": "~/repositories/gitignore" }] } diff --git a/GitAutoDeploy.py b/GitAutoDeploy.py index 563dbb5..5420774 100755 --- a/GitAutoDeploy.py +++ b/GitAutoDeploy.py @@ -1,325 +1,504 @@ #!/usr/bin/env python -import json, urlparse, sys, os, signal, socket, re -from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -from subprocess import call -from threading import Timer - -class GitAutoDeploy(BaseHTTPRequestHandler): - - CONFIG_FILEPATH = './GitAutoDeploy.conf.json' - config = None - debug = True - quiet = False - daemon = False - - @classmethod - def getConfig(myClass): - if(myClass.config == None): - try: - configString = open(myClass.CONFIG_FILEPATH).read() - except: - print "Could not load %s file" % myClass.CONFIG_FILEPATH - sys.exit(2) - - try: - myClass.config = json.loads(configString) - except: - print "%s file is not valid JSON" % myClass.CONFIG_FILEPATH - sys.exit(2) - - for repository in myClass.config['repositories']: - if(not os.path.isdir(repository['path'])): - print "Directory %s not found" % repository['path'] - call(['git clone --recursive '+repository['url']+' '+repository['path']], shell=True) - if(not os.path.isdir(repository['path'])): - print "Unable to clone repository %s" % repository['url'] - sys.exit(2) - else: - print "Repository %s successfully cloned" % repository['url'] - if(not os.path.isdir(repository['path'] + '/.git')): - print "Directory %s is not a Git repository" % repository['path'] - sys.exit(2) - myClass.clearLock(repository['path']) - - return myClass.config - - def do_POST(self): - urls = self.parseRequest() - self.respond() - Timer(1.0, self.do_process, [urls]).start() - - def do_process(self, urls): - for url in urls: - repos = self.getMatchingPaths(url) - for repo in repos: - if self.lock(repo['path']): - try: - n = 4 - while 0 < n and 0 != self.pull(repo['path'], repo['branch']): - --n - if 0 < n: - self.deploy(repo['path']) - except: - call(['echo "Error during \'pull\' or \'deploy\' operation on path: ' + repo['path'] + '"'], shell=True) - finally: - self.unlock(repo['path']) - - def parseRequest(self): - contenttype = self.headers.getheader('content-type') - length = int(self.headers.getheader('content-length')) - body = self.rfile.read(length) - - items = [] - - try: - if contenttype == "application/json" or contenttype == "application/x-www-form-urlencoded": - post = urlparse.parse_qs(body) - - # If payload is missing, we assume gitlab syntax. - if contenttype == "application/json" and "payload" not in post: - mode = "github" - # If x-www-form-urlencoded, we assume bitbucket syntax. - elif contenttype == "application/x-www-form-urlencoded": - mode = "bitbucket" - # Oh Gitlab, dear Gitlab... - else: - mode = "gitlab" - - - if mode == "github": - response = json.loads(body) - items.append(response['repository']['url']) - - elif mode == "bitbucket": - for itemString in post['payload']: - item = json.loads(itemString) - items.append("ssh://hg@bitbucket.org" + item['repository']['absolute_url'][0:-1]) - - # Otherwise, we assume github/bitbucket syntax. - elif mode == "gitlab": - for itemString in post['payload']: - item = json.loads(itemString) - items.append(item['repository']['url']) - - # WTF?! - else: - pass - except Exception: - pass - - return items - - def getMatchingPaths(self, repoUrl): - res = [] - config = self.getConfig() - for repository in config['repositories']: - if(repository['url'] == repoUrl): - res.append({ - 'path': repository['path'], - 'branch': ('branch' in repository) and repository['branch'] or 'master' - }) - return res - - def respond(self): - self.send_response(200) - self.send_header('Content-type', 'text/plain') - self.end_headers() - - def lock(self, path): - return 0 == call(['sh lock.sh "' + path + '"'], shell=True) - - def unlock(self, path): - call(['sh unlock.sh "' + path + '"'], shell=True) - - @classmethod - def clearLock(myClass, path): - call(['sh clear_lock.sh "' + path + '"'], shell=True) - - def pull(self, path, branch): - if(not self.quiet): - print "\nPost push request received" - print 'Updating ' + path - res = call(['sleep 5; cd "' + path + '" && unset GIT_DIR && git fetch origin && git update-index --refresh && git reset --hard origin/' + branch + ' && git submodule init && git submodule update'], shell=True) - call(['echo "Pull result: ' + str(res) + '"'], shell=True) - return res - - def deploy(self, path): - config = self.getConfig() - for repository in config['repositories']: - if(repository['path'] == path): - cmds = [] - if 'deploy' in repository: - cmds.append(repository['deploy']) - - gd = config['global_deploy'] - if len(gd[0]) is not 0: - cmds.insert(0, gd[0]) - if len(gd[1]) is not 0: - cmds.append(gd[1]) - - if(not self.quiet): - print 'Executing deploy command(s)' - for cmd in cmds: - call(['cd "' + path + '" && ' + cmd], shell=True) - - break - - -class GitAutoDeployMain: - - server = None - - def run(self): - for arg in sys.argv: - if(arg == '-d' or arg == '--daemon-mode'): - GitAutoDeploy.daemon = True - GitAutoDeploy.quiet = True - if(arg == '-q' or arg == '--quiet'): - GitAutoDeploy.quiet = True - if(arg == '--ssh-keyscan'): - print 'Scanning repository hosts for ssh keys...' - self.ssh_key_scan() - if(arg == '--force'): - print '[KILLER MODE] Warning: The --force option will try to kill any process ' \ - 'using %s port. USE AT YOUR OWN RISK' %GitAutoDeploy.getConfig()['port'] - self.kill_them_all() - - if(GitAutoDeploy.daemon): - pid = os.fork() - if(pid > 0): - sys.exit(0) - os.setsid() - - self.create_pidfile() - - if(not GitAutoDeploy.quiet): - print 'Github & Gitlab Autodeploy Service v 0.1 started' - else: - print 'Github & Gitlab Autodeploy Service v 0.1 started in daemon mode' - - try: - self.server = HTTPServer((GitAutoDeploy.getConfig()['host'], GitAutoDeploy.getConfig()['port']), GitAutoDeploy) - sa = self.server.socket.getsockname() - print "Listeing on", sa[0], "port", sa[1] - self.server.serve_forever() - except socket.error, e: - if(not GitAutoDeploy.quiet and not GitAutoDeploy.daemon): - print "Error on socket: %s" % e - self.debug_diagnosis() - sys.exit(1) - - def ssh_key_scan(self): - for repository in GitAutoDeploy.getConfig()['repositories']: - url = repository['url'] - print "Scanning repository: %s" % url - m = re.match('.*@(.*?):', url) - if(m != None): - port = repository['port'] - port = '' if port == None else ('-p' + port) - call(['ssh-keyscan -t ecdsa,rsa ' + port + ' ' + m.group(1) + ' >> $HOME/.ssh/known_hosts'], shell=True) - else: - print 'Could not find regexp match in path: %s' % url - - def kill_them_all(self): - pid = self.get_pid_on_port(GitAutoDeploy.getConfig()['port']) - if pid == False: - print '[KILLER MODE] I don\'t know the number of pid that is using my configured port\n ' \ - '[KILLER MODE] Maybe no one? Please, use --force option carefully' - return False - - os.kill(pid, signal.SIGKILL) - return True - - def create_pidfile(self): - with open(GitAutoDeploy.getConfig()['pidfilepath'], 'w') as f: - f.write(str(os.getpid())) - - def read_pidfile(self): - with open(GitAutoDeploy.getConfig()['pidfilepath'],'r') as f: - return f.readlines() - - def remove_pidfile(self): - os.remove(GitAutoDeploy.getConfig()['pidfilepath']) - - def debug_diagnosis(self): - if GitAutoDeploy.debug == False: - return - - port = GitAutoDeploy.getConfig()['port'] - pid = self.get_pid_on_port(port) - if pid == False: - print 'I don\'t know the number of pid that is using my configured port' - return - - print 'Process with pid number %s is using port %s' % (pid, port) - with open("/proc/%s/cmdline" % pid) as f: - cmdline = f.readlines() - print 'cmdline ->', cmdline[0].replace('\x00', ' ') - - def get_pid_on_port(self,port): - with open("/proc/net/tcp",'r') as f: - filecontent = f.readlines()[1:] - - pids = [int(x) for x in os.listdir('/proc') if x.isdigit()] - conf_port = str(GitAutoDeploy.getConfig()['port']) - mpid = False - - for line in filecontent: - if mpid != False: - break - - _, laddr, _, _, _, _, _, _, _, inode = line.split()[:10] - decport = str(int(laddr.split(':')[1], 16)) - - if decport != conf_port: - continue - - for pid in pids: - try: - path = "/proc/%s/fd" % pid - if os.access(path, os.R_OK) is False: - continue - - for fd in os.listdir(path): - cinode = os.readlink("/proc/%s/fd/%s" % (pid, fd)) - minode = cinode.split(":") - - if len(minode) == 2 and minode[1][1:-1] == inode: - mpid = pid - except Exception as e: - pass - return mpid - - - def stop(self): - if(self.server is not None): - self.server.socket.close() - - def exit(self): - if(not GitAutoDeploy.quiet): - print '\nGoodbye' - self.remove_pidfile() - sys.exit(0) - - def signal_handler(self, signum, frame): - self.stop() - if(signum == 1): - self.run() - return - elif(signum == 2): - print '\nKeyboard Interrupt!!!' - elif(signum == 6): - print 'Requested close by SIGABRT (process abort signal). Code 6.' - - self.exit() + +class Lock(): + """Simple implementation of a mutex lock using the file systems. Works on *nix systems.""" + + path = None + _has_lock = False + + def __init__(self, path): + self.path = path + + def obtain(self): + import os + try: + os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + self._has_lock = True + print "Successfully obtained lock: %s" % self.path + except OSError: + return False + else: + return True + + def release(self): + import os + if not self._has_lock: + raise Exception("Unable to release lock that is owned by another process") + try: + os.remove(self.path) + print "Successfully released lock: %s" % self.path + finally: + self._has_lock = False + + def has_lock(self): + return self._has_lock + + def clear(self): + import os + try: + os.remove(self.path) + except OSError: + pass + finally: + print "Successfully cleared lock: %s" % self.path + self._has_lock = False + + +class GitWrapper(): + """Wraps the git client. Currently uses git through shell command invocations.""" + + def __init__(self): + pass + + @staticmethod + def pull(repo_config): + """Pulls the latest version of the repo from the git server""" + from subprocess import call + + branch = ('branch' in repo_config) and repo_config['branch'] or 'master' + + print "\nPost push request received" + print 'Updating ' + repo_config['path'] + + res = call(['sleep 5; cd "' + + repo_config['path'] + + '" && unset GIT_DIR && git fetch origin && git update-index --refresh && git reset --hard origin/' + + branch + ' && git submodule init && git submodule update'], shell=True) + call(['echo "Pull result: ' + str(res) + '"'], shell=True) + return res + + @staticmethod + def clone(url, path): + from subprocess import call + call(['git clone --recursive %s %s' % (url, path)], shell=True) + + + @staticmethod + def deploy(repo_config): + """Executes any supplied post-pull deploy command""" + from subprocess import call + + path = repo_config['path'] + + cmds = [] + if 'deploy' in repo_config: + cmds.append(repo_config['deploy']) + + gd = GitAutoDeploy().get_config()['global_deploy'] + if len(gd[0]) is not 0: + cmds.insert(0, gd[0]) + if len(gd[1]) is not 0: + cmds.append(gd[1]) + + print 'Executing deploy command(s)' + + for cmd in cmds: + call(['cd "' + path + '" && ' + cmd], shell=True) + + +from BaseHTTPServer import BaseHTTPRequestHandler + + +class WebhookRequestHandler(BaseHTTPRequestHandler): + """Extends the BaseHTTPRequestHandler class and handles the incoming HTTP requests.""" + + def do_POST(self): + """Invoked on incoming POST requests""" + from threading import Timer + + # Extract repository URL(s) from incoming request body + repo_urls = self.get_repo_urls_from_request() + + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.end_headers() + + # Wait one second before we do git pull (why?) + Timer(1.0, GitAutoDeploy.process_repo_urls, [repo_urls]).start() + + def get_repo_urls_from_request(self): + """Parses the incoming request and extracts all possible URLs to the repository in question. Since repos can + have both ssh://, git:// and https:// URIs, and we don't know which of them is specified in the config, we need + to collect and compare them all.""" + import json + + #content_type = self.headers.getheader('content-type') + length = int(self.headers.getheader('content-length')) + body = self.rfile.read(length) + + data = json.loads(body) + + repo_urls = [] + + gitlab_event = self.headers.getheader('X-Gitlab-Event') + github_event = self.headers.getheader('X-GitHub-Event') + user_agent = self.headers.getheader('User-Agent') + + if not 'repository' in data: + print "ERROR - Unable to recognize data format" + return repo_urls + + # Assume GitLab if the X-Gitlab-Event HTTP header is set + if gitlab_event: + + print "Received '%s' event from GitLab" % gitlab_event + + # One repository may posses multiple URLs for different protocols + for k in ['url', 'git_http_url', 'git_ssh_url']: + if k in data['repository']: + repo_urls.append(data['repository'][k]) + + # Assume GitHub if the X-GitHub-Event HTTP header is set + elif github_event: + + print "Received '%s' event from GitHub" % github_event + + # One repository may posses multiple URLs for different protocols + for k in ['url', 'git_url', 'clone_url', 'ssh_url']: + if k in data['repository']: + repo_urls.append(data['repository'][k]) + + # Assume BitBucket if the User-Agent HTTP header is set to 'Bitbucket-Webhooks/2.0' (or something similar) + elif user_agent.lower().find('bitbucket') != -1: + + print "Received event from BitBucket" + + # One repository may posses multiple URLs for different protocols + for k in ['url', 'git_url', 'clone_url', 'ssh_url']: + if k in data['repository']: + repo_urls.append(data['repository'][k]) + + if 'full_name' in data['repository']: + repo_urls.append('git@bitbucket.org:%s.git' % data['repository']['full_name']) + repo_urls.append('https://oliverpoignant@bitbucket.org/%s.git' % data['repository']['full_name']) + + else: + print "ERROR - Unable to recognize request origin. Don't know how to handle the request." + + return repo_urls + + +class GitAutoDeploy(object): + + config_path = './GitAutoDeploy.conf.json' + debug = True + daemon = False + + _instance = None + _server = None + _config = None + + def __new__(cls, *args, **kwargs): + """Overload constructor to enable Singleton access""" + if not cls._instance: + cls._instance = super(GitAutoDeploy, cls).__new__( + cls, *args, **kwargs) + return cls._instance + + @staticmethod + def debug_diagnosis(port): + if GitAutoDeploy.debug is False: + return + + pid = GitAutoDeploy.get_pid_on_port(port) + if pid is False: + print 'I don\'t know the number of pid that is using my configured port' + return + + print 'Process with pid number %s is using port %s' % (pid, port) + with open("/proc/%s/cmdline" % pid) as f: + cmdline = f.readlines() + print 'cmdline ->', cmdline[0].replace('\x00', ' ') + + @staticmethod + def get_pid_on_port(port): + import os + + with open("/proc/net/tcp",'r') as f: + file_content = f.readlines()[1:] + + pids = [int(x) for x in os.listdir('/proc') if x.isdigit()] + conf_port = str(port) + mpid = False + + for line in file_content: + if mpid is not False: + break + + _, laddr, _, _, _, _, _, _, _, inode = line.split()[:10] + decport = str(int(laddr.split(':')[1], 16)) + + if decport != conf_port: + continue + + for pid in pids: + try: + path = "/proc/%s/fd" % pid + if os.access(path, os.R_OK) is False: + continue + + for fd in os.listdir(path): + cinode = os.readlink("/proc/%s/fd/%s" % (pid, fd)) + minode = cinode.split(":") + + if len(minode) == 2 and minode[1][1:-1] == inode: + mpid = pid + + except Exception as e: + pass + + return mpid + + @staticmethod + def process_repo_urls(urls): + import os + import time + + repo_config = GitAutoDeploy().get_matching_repo_config(urls) + + if not repo_config: + print 'Unable to find any of the repository URLs in the config: %s' % ', '.join(urls) + return + + running_lock = Lock(os.path.join(repo_config['path'], 'status_running')) + waiting_lock = Lock(os.path.join(repo_config['path'], 'status_waiting')) + try: + + # Attempt to obtain the status_running lock + while not running_lock.obtain(): + + # If we're unable, try once to obtain the status_waiting lock + if not waiting_lock.has_lock() and not waiting_lock.obtain(): + print "Unable to obtain the status_running lock nor the status_waiting lock. Another process is "\ + + "already waiting, so we'll ignore the request." + + # If we're unable to obtain the waiting lock, ignore the request + return + + # Keep on attempting to obtain the status_running lock until we succeed + time.sleep(5) + + n = 4 + while 0 < n and 0 != GitWrapper.pull(repo_config): + n -= 1 + if 0 < n: + GitWrapper.deploy(repo_config) + + except Exception as e: + print 'Error during \'pull\' or \'deploy\' operation on path: %s' % repo_config['path'] + print e + + finally: + + # Release the lock if it's ours + if running_lock.has_lock(): + running_lock.release() + + # Release the lock if it's ours + if waiting_lock.has_lock(): + waiting_lock.release() + + def get_config(self): + import json + import sys + import os + + if self._config: + return self._config + + try: + config_string = open(self.config_path).read() + + except Exception as e: + print "Could not load %s file\n" % self.config_path + raise e + + try: + self._config = json.loads(config_string) + + except Exception as e: + print "%s file is not valid JSON\n" % self.config_path + raise e + + # Translate any ~ in the path into /home/<user> + if 'pidfilepath' in self._config: + self._config['pidfilepath'] = os.path.expanduser(self._config['pidfilepath']) + + for repo_config in self._config['repositories']: + + # Translate any ~ in the path into /home/<user> + if 'path' in repo_config: + repo_config['path'] = os.path.expanduser(repo_config['path']) + + if not os.path.isdir(repo_config['path']): + + print "Directory %s not found" % repo_config['path'] + GitWrapper.clone(url=repo_config['url'], path=repo_config['path']) + + if not os.path.isdir(repo_config['path']): + print "Unable to clone repository %s" % repo_config['url'] + sys.exit(2) + + else: + print "Repository %s successfully cloned" % repo_config['url'] + + if not os.path.isdir(repo_config['path'] + '/.git'): + print "Directory %s is not a Git repository" % repo_config['path'] + sys.exit(2) + + # Clear any existing lock files, with no regard to possible ongoing processes + Lock(os.path.join(repo_config['path'], 'status_running')).clear() + Lock(os.path.join(repo_config['path'], 'status_waiting')).clear() + + return self._config + + def get_matching_repo_config(self, urls): + """Iterates over the various repo URLs provided as argument (git://, ssh:// and https:// for the repo) and + compare them to any repo URL specified in the config""" + + config = self.get_config() + + for url in urls: + for repo_config in config['repositories']: + if repo_config['url'] == url: + return repo_config + + def ssh_key_scan(self): + import re + from subprocess import call + + for repository in self.get_config()['repositories']: + + url = repository['url'] + print "Scanning repository: %s" % url + m = re.match('.*@(.*?):', url) + + if m is not None: + port = repository['port'] + port = '' if port is None else ('-p' + port) + call(['ssh-keyscan -t ecdsa,rsa ' + port + ' ' + m.group(1) + ' >> $HOME/.ssh/known_hosts'], shell=True) + + else: + print 'Could not find regexp match in path: %s' % url + + def kill_conflicting_processes(self): + import os + + pid = GitAutoDeploy.get_pid_on_port(self.get_config()['port']) + + if pid is False: + print '[KILLER MODE] I don\'t know the number of pid that is using my configured port\n ' \ + '[KILLER MODE] Maybe no one? Please, use --force option carefully' + return False + + os.kill(pid, signal.SIGKILL) + return True + + def create_pid_file(self): + import os + with open(self.get_config()['pidfilepath'], 'w') as f: + f.write(str(os.getpid())) + + def read_pid_file(self): + with open(self.get_config()['pidfilepath'], 'r') as f: + return f.readlines() + + def remove_pid_file(self): + import os + os.remove(self.get_config()['pidfilepath']) + + def exit(self): + import sys + print '\nGoodbye' + self.remove_pid_file() + sys.exit(0) + + def run(self): + from sys import argv + import sys + from BaseHTTPServer import HTTPServer + import socket + + if '-d' in argv or '--daemon-mode' in argv: + self.daemon = True + + if '-q' in argv or '--quiet' in argv: + sys.stdout = open(os.devnull, 'w') + + if '--ssh-keygen' in argv: + print 'Scanning repository hosts for ssh keys...' + self.ssh_key_scan() + + if '--force' in argv: + print 'Attempting to kill any other process currently occupying port %s' % self.get_config()['port'] + self.kill_conflicting_processes() + + if '--config' in argv: + import os + pos = argv.index('--config') + if len(argv) > pos + 1: + self.config_path = os.path.realpath(argv[argv.index('--config') + 1]) + print 'Using custom configuration file \'%s\'' % self.config_path + + if GitAutoDeploy.daemon: + pid = os.fork() + if pid > 0: + sys.exit(0) + os.setsid() + + self.create_pid_file() + + if self.daemon: + print 'GitHub & GitLab auto deploy service v 0.1 started in daemon mode' + + # Disable output in daemon mode + sys.stdout = open(os.devnull, 'w') + + else: + print 'GitHub & GitLab auto deploy service v 0.1 started' + + try: + self._server = HTTPServer((self.get_config()['host'], self.get_config()['port']), WebhookRequestHandler) + sa = self._server.socket.getsockname() + print "Listening on", sa[0], "port", sa[1] + self._server.serve_forever() + + except socket.error, e: + + if not GitAutoDeploy.daemon: + print "Error on socket: %s" % e + GitAutoDeploy.debug_diagnosis(self.get_config()['port']) + + sys.exit(1) + + def stop(self): + if self._server is not None: + self._server.socket.close() + + def signal_handler(self, signum, frame): + self.stop() + + if signum == 1: + self.run() + return + + elif signum == 2: + print '\nRequested close by keyboard interrupt signal' + + elif signum == 6: + print 'Requested close by SIGABRT (process abort signal). Code 6.' + + self.exit() if __name__ == '__main__': - gadm = GitAutoDeployMain() + import signal + + app = GitAutoDeploy() - signal.signal(signal.SIGHUP, gadm.signal_handler) - signal.signal(signal.SIGINT, gadm.signal_handler) - signal.signal(signal.SIGABRT, gadm.signal_handler) - signal.signal(signal.SIGPIPE, signal.SIG_IGN) + signal.signal(signal.SIGHUP, app.signal_handler) + signal.signal(signal.SIGINT, app.signal_handler) + signal.signal(signal.SIGABRT, app.signal_handler) + signal.signal(signal.SIGPIPE, signal.SIG_IGN) - gadm.run() + app.run()
\ No newline at end of file @@ -1,23 +1,43 @@ - - # What is it? +GitAutoDeploy.py consists of a small HTTP server that listens for Web hook requests sent from GitHub, GitLab or Bitbucket servers. The script allows you to continuously and automatically deploy you projects during development each time you push new commits to GitHub, GitLab or Bitbucket.</p> + + + +# How does it work? + +When commits are pushed to your Git repository, the Git server will notify ```GitAutoDeploy.py``` by sending a HTTP POST request with a JSON body to a pre configured URL (your-host:8001). The JSON body contains detailed information about the repository and what event that triggered the request. GitAutoDeploy.py parses and validates the request, and if all goes well it issues a ```git pull```. + +Additionally, ```GitAutoDeploy.py``` can be configured to execute a shell command upon each successful "git pull", which can be used to trigger custom build actions.</p> + +# Getting started +## Dependencies +* Git (tested on version 2.5.0) +* Python (tested on version 2.7) + +## Configuration -This is a small HTTP server written in python. -It allows you to have a version of your project installed, that will be updated automatically on each Github or Gitlab push. +* Copy ```GitAutoDeploy.conf.json.example``` to ```GitAutoDeploy.conf.json``` +* Modify ```GitAutoDeploy.conf.json``` to match your project setup +* Make sure that the ```pidfilepath``` path is writable for the user running the script, as well as any other path configured for your repositories. -To set it up, do the following: -* Install python -* Copy the ```GitAutoDeploy.conf.json.example``` to ```GitAutoDeploy.conf.json```. This file will be gitignored and can be environment specific. -* Enter the matching for your project(s) in the ```GitAutoDeploy.conf.json``` file -* Start the server by typing ```python GitAutoDeploy.py``` -* To run it as a daemon add ```--daemon-mode``` -* On the Github or Gitlab page go to a repository, then "Admin", "Service Hooks", -"Post-Receive URLs" and add the url of your machine + port (e.g. ```http://example.com:8001```). +## Running the application +```python GitAutoDeploy.py``` -You can even test the whole thing here, by clicking on the "Test Hook" button, whohoo! +## Command line options -# Configure GitAutoDeploy to get executed at start up +--daemon-mode (-d) Run in background (daemon mode) +--quiet (-q) Suppress all output +--ssh-keygen +--force +--config <path> Specify custom configuration file + +## Start automatically on boot + +### Crontab +The easiest way to configure your system to automatically start ```GitAutoDeploy.py``` after a reboot is through crontab. Open crontab in edit mode using ```crontab -e``` and add the following: + +```@reboot /usr/bin/python /path/to/GitAutoDeploy.py --daemon-mode --quiet``` ### Debian and Sys-V like init system. @@ -27,19 +47,18 @@ You can even test the whole thing here, by clicking on the "Test Hook" button, w * This init script assumes that you have ```GitAutoDeploy.py``` in ```/opt/Gitlab_Auto_Deploy/GitAutoDeploy.py```. If this is not the case, edit ```gitautodeploy``` init script and modify ```DAEMON``` and ```PWD```. * Now you need to add the correct symbolic link to your specific runlevel dir to get the script executed on each start up. On Debian_Sys-V just do ```update.rc.d gitautodeploy defaults``` -### Systemd - -* TODO - -# How this works - -When someone pushes changes into Github or Gitlab, it sends a json file to the service hook url. -It contains information about the repository that was updated. +## Configure GitHub -All it really does is match the repository urls to your local repository paths in the config file, -move there and run "git pull". +* Go to your repository -> Settings -> Webhooks and Services -> Add webhook</li> +* In "Payload URL", enter your hostname and port (your-host:8001) +* Hit "Add webhook" +## Configure GitLab +* Go to your repository -> Settings -> Web hooks +* In "URL", enter your hostname and port (your-host:8001) +* Hit "Add Web Hook" -Additionally it runs a deploy bash command that you can add to the config file optionally, and it also -allows you to add two global deploy commands, one that would run at the beginning and one that would run at the end of the deploy. -Make sure that you start the server as the user that is allowed to pull from the github or gitlab repository. +## Configure Bitbucket +* Go to your repository -> Settings -> Webhooks -> Add webhook +* In "URL", enter your hostname and port (your-host:8001) +* Hit "Save" diff --git a/clear_lock.sh b/clear_lock.sh deleted file mode 100644 index 601a51e..0000000 --- a/clear_lock.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -cd $1 && rm status_waiting status_running; cd - diff --git a/lock.sh b/lock.sh deleted file mode 100644 index a67e019..0000000 --- a/lock.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh -cd $1 - -set -o noclobber -{ > status_waiting; } -if [ "$?" != "0" ] -then - echo "Some other thread is alreay waiting. Exit." - - set +o noclobber - cd - - exit 1 -else - - { > status_running; } - while [ "$?" != "0" ] - do - echo "Some other thread is already building. Waiting 5 sec!" - sleep 5 - { > status_running; } - done - rm status_waiting - - set +o noclobber - cd - - exit 0 -fi diff --git a/unlock.sh b/unlock.sh deleted file mode 100644 index 0ff55c4..0000000 --- a/unlock.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -cd $1 && rm status_running; cd - |