Exposing version information from your Django project

2016-02-23 by Senko Rašić

Adding a custom header identifying the git/hg revision of your project can make it easy to identify the version of the code running. Here's how to do it.

Motivation

If the project back-end and front-end are developed separately (not in the same project, or done by a different team) it's easy to get confused about which version of the code is deployed on the back-end. Likewise, in QA and later in production, easily identifying against which version of the project the bugs are reported is a major plus.

There are various ways to achieve this — one popular way is to show the version somewhere on a page (eg. a footer) or on a specific page.

Another unombtrusive way of achieving this is adding a custom HTTP header to every response. This approach has the added benefit of working for any responses your back-end returns, not just HTML — for example, if creating a JSON-based RESTful service using Django Rest Framework.

Simple approach

In the Django land, this is easy to accomplish by adding a middleware component which will modify every response to include the header with the version information:

# myapp/middleware.py

class RevisionMiddleware(object):
    def process_response(self, request, response):
        response['X-Source-Revision'] = check_output(
            ['git', 'rev-parse', '--short', 'HEAD']).strip()
        return response

# project/settings.py
MIDDLEWARE_CLASSES += ('myapp.middleware.RevisionMiddleware',)

If using some other version control system, such as Mercurial, the code would be similer except the command-line tool being called differentially.

Making it better

The simple approach is a good start, but we'd likely want to make improvements.

One improvement would be to cache the git command output. Unless the code i being hot-reloaded, the version will never change once the application starts. This means that we get the information upfront and just return the cached copy for each request.

Another improvement would be error handling. For example, when the VCS info is not available (eg. not being run from a git repo).

Context processor

Why stop there? Once we have the git revision information, we can make it available for templates, in case we do want to output the version in the page footer or similar location.

Provided we refactored our code to move the git version loading out of the middleware (which we'll want to do anyways for caching purposes), the request context processor would look something like:

# myapp/context_processors.py
from .loader import get_revision

def git_commit_id(request):
    return {'SOURCE_REVISION': get_revision()}

# project/settings.py (Django 1.8+)
TEMPLATES=[
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'OPTIONS': {
            'context_processors': [
                'myapp.context_processors.source_revision'
            ]
        }
    }
]

This will make the GIT_COMMIT_ID variable available to all templates if using RequestContext (which usually you do). Once there, we can also use it for another interesting purpose - making sure we bust the cache for static assets once our code changes.

Static asset cache invalidation

This scenario is probably familiar to you — you release a new version of the code and some users' browsers just stubbornly load the old version from their cache. In theory, if you set the cache-related headers correctly, this shouldn't happen, but it's easy to make a mistake and sometimes it can be inconvenient setting these headers.

Once easy way out is to add a dummy variable part to the asset URL that will force the browser to fetch the new resource. Since our git commit ID (or hg tip rev) changes each time we deploy a new version, it's a great candidate for the job[0].

We would then use it something like:

<script type="text/javascript"
    src="{% static 'js/myscript.js' %}?rev={{ SOURCE_REVISION }}">

Django Source Revision

After polishing it up a bit, writing tests, allowing for different version controls systems, and extracting it into a reusable Django app, we'd get a handy reusable app for using git commit IDs in our projects.

This is exactly what we did with Django Source Revision, with an addition of a drop-in staticfiles replacement which automatically addds the ?rev={{ SOURCE_REVISION }} whenever you use {% static %} tag, so you don't have to.

You can see it in action on this site (fire up your browser's web inspector tool and look at the headers of the HTTP response for this page). So far it's been really useful for us so we're releasing it as open source and hope it'll be as useful to you.

And if you find a bug or want to suggest an improvement, let us know!


[0] Not the best candidate, though, since changing the commit ID doesn't mean the particular static file has changed. In projects with a lot of static assets and frequent releases (meaning the static files are likely to not have changed), it would force re-download of assets not being changed. In this case, approach that tags each file with its shasum (or id of the latest commit that modified it) would be a better approach, but it'd require keeping track of the latest versions of every static file in the project, which is more involved than our simple approach taken here.

Author
Senko Rašić
We’re small, experienced and passionate team of web developers, doing custom app development and web consulting.