Bryan Helmig

Co-founder of Zapier, speaker, musician and builder of things.

Don’t feel like setting up Jenkins you lazy bum? Fine. Try this on for size: use a Github service hook to ping a Django view which runs a bash script out of process. Sound like a bad idea? Probably, but bad is a relative thing you see… here’s how:

  • Gonna need the at command. Do that now: apt-get install at
  • Gonna need two bash scripts: delay_deploy.sh and deploy.sh
  • Gonna need a view to receive the hook

First, let’s do the deploy.sh script:

#!/bin/bash
 
cd /path/to/repo
git pull origin master
service apache2 restart
# or anything else you might need

Second, let’s do the delay_deploy.sh script:

#!/bin/bash
 
at -f '/path/to/deploy.sh' now

If you have a standard Django/Apache/mod_wsgi setup, we will have a problem running this script from a view because Apache is running under a non-root user (as it should be). Usually it is under the user www-data, but you can double check with ps aux | grep apache. The same holds for Nginx, lighttpd, etc…

Third, let’s permit www-data (or whoever) to sudo delay_deploy.sh (which will running a static, out of process command via at). This requires editing the /etc/sudoers file, so nano /etc/sudoers and add this line:

www-data ALL= NOPASSWD: /path/to/delay_deploy.sh

Now, www-data can run delay_deploy.sh as root, but only delay_deploy.sh and nothing else. This is much safer that allowing www-data to run /usr/bin/at or something more generic. Also, don’t forget to run chmod +x on both scripts.

The reason you can’t just hit the deploy.sh script is pretty simple: the server will wait on the command to finish, but the command will kill the server. Not good. So our two script method fixes this: the delay script triggers an out of process deploy script. Also, its good to keep sudo power very, very specific.

Fourth, let’s work on that view. I’ll leave it up to the reader to decide where to put it and how to secure it (hint: maybe check for a regularly changed secret GET param key and restrict to Github’s IP address):

from django.views.decorators.csrf import csrf_exempt
 
@csrf_exempt
def do_deploy(request):
    """
    Smile. Life's just gotten easier.
    """
    import simplejson
    import subprocess
 
    from django.http import HttpResponse, Http404
 
    if request.method != 'POST':
        raise Http404
 
    if not 'payload' in request.POST.keys():
        raise Http404
 
    # might raise 404 if secret GET key isn't good 
    # or requesting IP isn't whitelisted 
 
    payload = simplejson.loads(request.POST['payload'])
 
    out_json = {'status': 'failed'}
 
    # only trigger if master branch receives a commit
    if payload['ref'] == 'refs/heads/master':
        # DO NOT use any user input when calling scripts, as
        # this is naughty enough already
 
        subprocess.call('sudo /path/to/delay_deploy.sh', shell=True)
        out_json['status'] = 'success'
 
    return HttpResponse(simplejson.dumps(out_json), content_type='application/json')

Fifth, and finally, set up your Post Receive Hook or custom Service Hook with Github to hit the URL for the above view. If you need to debug, check out hurl.it for some handy dandy POST testing action.

Another warning: letting HTTP requests trigger sudo’d events is very, very dangerous. While as far as I know, this particular method is mostly safe, it most certainly isn’t best practices. Light me up in the comments.


Posted December 20, 2011 @ 4:32 pm under Work.