Auto-deployment from Subversion

October 30, 2011    automated deployment continuous deployment subversion

For the toy-website NS-Stats.nl I wanted to be able to deploy the software automatically. Having to log-in at the server, switching/updating your working directory, and copying all the files manually does not seem to be much of a hassle, but automating things is something I prefer. Not only does this save you some work, but this also keeps you from making mistakes during deployment. (Of course, if you automated deployment method is faulty, you’ll still get errors.)

The idea for this article came from the article Automated Deployment with Subversion by Imram Nazar. In the article, Subversion commit-hooks are used. But, due to my set-up (I’m using xp-dev to host my repositories) I cannot use hooks, so a different approach is taken. Note: The auto-deployment-script from this article is a proof-of-concept. If you want to use this for a real case, be sure to use a Subversion library, such as pysvn, instead of calling the command-line svn command and check for errors during the process.

The method I propose is based on tags and commit messages. Two requisites have to be matched before a new version is deployed. First, a new tag (assuming you’ll be using the Subversion convention for repository structure (trunk, tags, branches)) has to be used for every version. Second, the commit message for a new tag has to contain the string “DEPLOY". Thus, when you want to deploy a new version to your production server, you’ll create a new tag and include “DEPLOY” in the commit message.

Why not do deployment only based on commit messages? In my opinion, it is always good to explicitly tag a version used during production. If any bugs occur, a bug-report can be ‘bound’ to a specific version. Using deployment based on both a tag and commit message, you always have the version tagged and do not need to deploy every tag.

The steps taken to perform a deployment are as follows. First, the commit-messages are checked to see if a new tag was created and the commit message contains the string “DEPLOY”. To retrieve the commits from a subversion repository you can use the following command:

$  svn log -v --xml http://svn.example.com/repo

This will give the commit messages in an XML format, which can be easily parsed using some Python code. The commit messages are in reverse chronological order: The last commit message comes first. Thus, as soon as the script sees a commit which matches our requisites, this commit is deployed. The following Python code does this:

def get_last_deployment_tag(repo_url):
    # execute svn log repo/trunk
    tags_url = repo_url + '/tags'
    out = subprocess.Popen(['svn', 'log', '-v', '--xml', tags_url], stdout=subprocess.PIPE).communicate()[0]

    # parse output
    tree = xml.etree.ElementTree.fromstring(out)

    for log_entry in tree.findall('logentry'):
        if deploy_string in log_entry.find('msg').text:
            for paths in log_entry.findall('paths'):
                for path in paths.findall('path'):
                    return path.text

    return (None, None)

Next, we need to do some bookkeeping to prevent re-deploying the same version every time. The current tag is written to a file on every deployment. This file is checked upon each possible deployment to see if the current deployed version isn’t the same as the new version. The following Python code reads and writes the current tag name to a file:

def current_tag_name(tag_file):
    f = open(tag_file, 'r')
    tag_name = f.read()
    f.close()
    return tag_name

def write_tag_name(tag_file, tag_name):
    f = open(tag_file, 'w')
    f.write(tag_name)
    f.close()

Finally, the new code needs to be deployed. This is done using the ‘svn export’ command. Calling this from Python is done as follows:

def export(repo_path, target_dir):
    subprocess.call(['svn', 'export', '--force', repo_path, target_dir])

Finally, the code is tied together by the following function:

def main():
    logging.debug('starting deployer at {0}'.format(datetime.datetime.now()))
    logging.debug('fetching svn tags')
    tag_name = get_last_deployment_tag(repo)
    tag_path = repo + tag_name

    if tag_name and tag_name != current_tag_name(tag_id_file):
        logging.info('deploying new version, path: {0}'.format(tag_name))
        export(tag_path + component_dir, export_dir)
        write_tag_name(tag_id_file, tag_name)

    logging.debug('ending fetcher at {0}'.format(datetime.datetime.now()))

Call this code periodically using a Cron job and you’ll have an automated deployment method.

The auto-deployment-script is used for NS-Stats.nl. The current deployed version of NS-Stats.nl is specified in this file.

With minimal effort, an automated deployment method and script has been created. The final script can be found here.