├── .gitignore ├── AUTHORS ├── MANIFEST ├── setup.py ├── LICENSE ├── README.rst └── bin └── git-play /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ju-yeong Park 2 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | bin/git-play 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | try: 4 | ldsc = open("README.rst").read() 5 | except: 6 | ldsc = "" 7 | 8 | setup( 9 | name="git-play", 10 | version="0.13", 11 | author="Stewart Park", 12 | author_email="stewartpark92@gmail.com", 13 | scripts=["bin/git-play"], 14 | url="http://github.com/stewartpark/git-play", 15 | license="MIT LICENSE", 16 | description="Git-play is a custom git command for deploying an application server very easily from a remote git repository. It checks the remote git repository every minute and if something has changed, it will restart the application server automatically.", 17 | long_description = ldsc, 18 | install_requires=[ 19 | 'PyYAML==5.4', 20 | 'GitPython==0.3.2.RC1' 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Ju-yeong Park 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.rst: -------------------------------------------------------------------------------- 1 | Git-play 2 | ======== 3 | 4 | Git-play is a custom git command for deploying an application server very easily from a remote git repository. It checks the remote git repository every minute and if something has changed, it will restart the application server automatically. 5 | 6 | 7 | Installation 8 | ------------ 9 | 10 | You can simply install git-play from PyPI by using ``pip`` or ``easy_install``: 11 | 12 | .. code-block:: console 13 | 14 | $ pip install git-play 15 | 16 | 17 | Getting started 18 | --------------- 19 | 20 | Git-play is made for people who hate complicated configurations, thus basically it doesn't require you to do much except for ``.git-play.yml``. 21 | 22 | 23 | Configuring your git-play deployment with ``.git-play.yml`` 24 | ----------------------------------------------------------- 25 | 26 | Git-play uses the ``.git-play.yml`` file in the root of your repository to configure how you want your application to be executed. 27 | ``.git-play.yml`` file has three parts: ``app``, ``setup``, ``teardown``. 28 | 29 | For your convenience, there are several examples of ``.git-play.yml`` file. 30 | 31 | Django 32 | ------ 33 | 34 | .. code-block:: yaml 35 | 36 | app: 37 | workdir: ./mysite 38 | respawn: yes 39 | exec: python manage.py runserver 40 | 41 | setup: 42 | - pip install -r requirements.txt 43 | - cd mysite 44 | - python manage.py syncdb 45 | 46 | teardown: 47 | - echo "The server is going down for maintanance..." 48 | 49 | 50 | Express 51 | ------- 52 | 53 | .. code-block:: yaml 54 | 55 | app: 56 | respawn: yes 57 | env: 58 | PORT: 80 59 | exec: node app.js 60 | 61 | setup: 62 | - npm install 63 | 64 | teardown: 65 | - echo "The server is going down for maintanance..." 66 | 67 | 68 | Spray and pray! 69 | --------------- 70 | 71 | Lastly, all you have to do is simply type the following in your terminal: 72 | 73 | .. code-block:: console 74 | 75 | $ git play http://github.com/foo/bar --remote origin --branch master 76 | Spawned! 77 | 78 | For an existing repository, type the following: 79 | 80 | .. code-block:: console 81 | 82 | $ git play bar -r origin -b master 83 | Spawned! 84 | 85 | .. code-block:: console 86 | 87 | $ ls -F 88 | bar/ 89 | $ cd bar 90 | $ git play 91 | Spawned! 92 | 93 | Contributing 94 | ------------ 95 | Just fork and request pulls. Any help or feedback is appreciated. 96 | -------------------------------------------------------------------------------- /bin/git-play: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | git-play 5 | by Ju-yeong Park 6 | """ 7 | 8 | import argparse 9 | import git 10 | import logging 11 | import os 12 | import re 13 | import signal 14 | import subprocess 15 | import sys 16 | import time 17 | import yaml 18 | 19 | 20 | # Parse arguments. 21 | parser = argparse.ArgumentParser(prog='git-play', description='Watch and deploy a repository') 22 | parser.add_argument('repository', default='.', nargs='?') 23 | parser.add_argument('directory', nargs='?', help='Specify the name of a newly created directory (default: the name of the repository)') 24 | parser.add_argument('--remote', '-r') 25 | parser.add_argument('--branch', '-b') 26 | parser.add_argument('--verbose', '-v', default=False, action='store_true', help='Makes the fetching more verbose/talkative. Mostly useful for debugging.') 27 | parser.add_argument('--log', '-l', default='./git-play.log', help='Specify log file path. (default: ./git-play.log)') 28 | parser.add_argument('--wait', '-i', default=60, help='Wait n seconds between pulling from the remote repository. The default is to wait for 60 seconds between each pulling.') 29 | parser.add_argument('--config', '-c', default='.git-play.yml', help='Specify configuration file name. (default: .git-play.yml)') 30 | args = parser.parse_args(sys.argv[1:]) 31 | 32 | # Default values 33 | _not_specified = not args.remote and not args.branch 34 | _repo = args.repository 35 | _remote = args.remote or 'origin' 36 | _branch = args.branch or 'master' 37 | _workdir = args.directory or \ 38 | os.path.basename(re.sub(r'\/$', '', args.repository.split(':')[-1])) 39 | _verbose = args.verbose 40 | _logfile = args.log 41 | _wait = int(args.wait) 42 | _configfile = args.config 43 | 44 | # Logging setting 45 | logging.basicConfig(filename=_logfile, filemode='a+', 46 | level=logging.DEBUG if _verbose else logging.INFO) 47 | 48 | if _verbose: 49 | logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) 50 | 51 | try: 52 | repo = git.Repo.clone_from(_repo, _workdir) 53 | if _not_specified: 54 | repo.git.checkout('%s/%s' % (_remote, _branch)) 55 | logging.info('Cloned.') 56 | except: 57 | try: 58 | repo = git.Repo(_workdir) 59 | if _not_specified: 60 | repo.git.checkout('%s/%s' % (_remote, _branch)) 61 | except: 62 | logging.fatal('No such repository.') 63 | exit(1) 64 | repo.git.pull(_remote, _branch) 65 | logging.info('Pulled.') 66 | 67 | 68 | # _workdir should be strict. 69 | _workdir = repo.working_dir 70 | 71 | logging.info('Git: %s, %s/%s' % (_repo, _remote, _branch)) 72 | logging.info('Working directory: %s' % (_workdir,)) 73 | 74 | # Utilities 75 | class Config(object): 76 | def __init__(self, path): 77 | self.path = path 78 | self.env = os.environ.copy() 79 | try: 80 | self.config = yaml.load(open('%s/%s' % (path, _configfile))) 81 | if 'app' in self.config: 82 | if 'env' in self.config['app']: 83 | for k in self.config['app']['env']: 84 | self.env[k] = str(self.config['app']['env'][k]) 85 | except: 86 | self.config = {} 87 | logging.error('Failed to read configurations.') 88 | 89 | def setup(self): 90 | if 'setup' in self.config: 91 | logging.info('Setting up...') 92 | os.system(("cd \"%s\";" % (self.path,)) + 93 | (';'.join(self.config['setup']))) 94 | 95 | def teardown(self): 96 | if 'teardown' in self.config: 97 | logging.info('Tearing down...') 98 | os.system(("cd \"%s\";" % (self.path,)) + 99 | (';'.join(self.config['teardown']))) 100 | 101 | def app_spawn(self): 102 | if 'app' in self.config: 103 | if 'workdir' in self.config['app']: 104 | path = os.path.join(self.path, self.config['app']['workdir']) 105 | else: 106 | path = self.path 107 | if 'exec' in self.config['app']: 108 | logging.info('Spawning an application...') 109 | self.server = subprocess.Popen(self.config['app']['exec'], 110 | close_fds=True, 111 | preexec_fn=os.setsid, 112 | cwd=path, 113 | env=self.env, 114 | shell=True) 115 | else: 116 | logging.warning('`app.exec` is empty.') 117 | 118 | def is_respawnable(self): 119 | if 'app' in self.config: 120 | if 'respawn' in self.config['app']: 121 | if self.config['app']['respawn']: 122 | return self.config['app']['respawn'] 123 | return False 124 | 125 | def app_respawn(self): 126 | if self.is_respawnable(): 127 | logging.info('Re-spawning an application...') 128 | if 'app' in self.config and 'workdir' in self.config['app']: 129 | path = os.path.join(self.path, self.config['app']['workdir']) 130 | else: 131 | path = self.path 132 | self.server = subprocess.Popen(self.config['app']['exec'], 133 | close_fds=True, 134 | preexec_fn=os.setsid, 135 | cwd=path, 136 | env=self.env, 137 | shell=True) 138 | 139 | def app_terminate(self): 140 | try: 141 | logging.info('Killing the server...') 142 | # Press Ctrl+C 143 | os.killpg(self.server.pid, signal.SIGINT) 144 | # 5 secs to clean up 145 | for x in xrange(10): 146 | if self.server.poll() is not None: 147 | break 148 | time.sleep(0.5) 149 | else: 150 | # Forcibly kill 151 | os.killpg(self.server.pid, signal.SIGKILL) 152 | except: 153 | logging.fatal("Couldn't kill the server") 154 | 155 | tick = 0 156 | 157 | # Initial start 158 | _conf = Config(_workdir) 159 | _conf.setup() 160 | _conf.app_spawn() 161 | 162 | print 'Spawned.' 163 | try: 164 | while True: 165 | time.sleep(1) 166 | tick = (tick + 1) % _wait 167 | if tick == _wait-1: 168 | # Pull every minute. 169 | logging.debug('Pulling from the remote repository...') 170 | while True: 171 | try: 172 | rst = repo.git.pull(_remote, _branch) 173 | break 174 | except: 175 | logging.error('Failed to pull, retry...') 176 | # See if something is changed. Fix 18/09/2019 handle text response for newer git versions 177 | if rst not in ('Already up to date.', 'Already up-to-date.'): 178 | # If it is, 179 | logging.info('Something has changed. Applying...') 180 | _conf.teardown() 181 | _conf.app_terminate() 182 | _conf = Config(_workdir) 183 | _conf.setup() 184 | _conf.app_spawn() 185 | else: 186 | # See if the process is dead 187 | if _conf.is_respawnable(): 188 | if _conf.server.poll() is not None: 189 | # Respawn. 190 | _conf.app_respawn() 191 | except KeyboardInterrupt: 192 | # Ctrl+C 193 | logging.info('Ctrl+C pressed.') 194 | _conf.teardown() 195 | _conf.app_terminate() 196 | --------------------------------------------------------------------------------