Simple CI Pipeline

One of the most productive things I’ve done as a senior developer was to automate the a CI pipeline. This enforced various standards we had already defined such as:

  • Using the black code formatter
  • Unit and integration tests passed

Lets jump in quickly with a look at how this can be done…

Pre-commit

I’m assuming you’re already using git, if not, you definitely should be!

Create (or add these to) a requirements-dev.txt file in the root of your project with the following contents:

pre-commit
black
pytest

Create a .pre-commit-config.yaml file in the root of your project with the following contents:

repos:
-   repo: https://github.com/psf/black
    rev: 19.3b0
    hooks:
    -   id: black

To get started we run:

pip install -r requirements-dev.txt
pre-commit install

From now on any time we create a commit (git commit ...) the pre-commit hook will intercept and black will run against any changed files. If the code would be left unchanged by black the commit will go through, otherwise it will be blocked and you will have to inspect and add (git add ...) any files changed before the commit will go through.

We can see how our current codebase fairs against our new black formatting checks with:

pre-commit run --all-files

Any changes made can quickly be reverted with:

git reset --hard HEAD

Travis

Travis is the tool for actually automating the running of our tests in a Continuous Integration (CI) pipeline. It integrates nicely with github to display the results of the tests.

The tool has some great documentation for how to get started with it, so I won’t reinvent the wheel, but I just want to detail my setup, particularly how to use the pre-commit within the environment.

Create a .travis.yml file in the root of your directory with the following contents:

language: python
python:
  - "3.6"      # my python version
# command to install dependencies
install:
  - pip install -r requirements.txt
  - pip install -r requirements-dev.txt
  - pre-commit install
# command to run tests
script:
  - pytest
  - pre-commit run --all-files

This is enough to get started, however we can improve the runtime of this slightly with some caching as follows:

language: python
python:
  - "3.6"      # my python version
cache: pip #TODO: add pre-commit caching
# command to install dependencies
install:
  - pip install -r requirements.txt
  - pip install -r requirements-dev.txt
  - pre-commit install
# command to run tests
script:
  - pytest
  - pre-commit run --all-files

Why bother?

Previously, these where things were we trusted that our small, co-located, and relatively senior dev team were all doing. We all contributed to our code standards and had a well documented/understood set of expectations of code before it was raised for review in a pull request. Plus we could easily discuss any issues quickly as we all sat together. This worked well enough for us for over a year.

As the team I worked with started to grow it became harder to maintain this trust. Expectations (however well documented) were less adhered to for a variety of reasons. This lead to wasting large amounts of my time as a senior developer doing code reviews. I spend significant time pointing out code which didn’t follow our code standards. Not only was this annoying for me to spend time repeatedly doing, but it was also frustrating for people whose code was being reviewed, because these comments distracted the focus from useful constructive criticism of the way they had implemented something. Additionally, I didn’t have trust if any unittests had been broken by the change.

One big benefit of this was that it enabled junior developers to be much more independent. In my team we had a possibly somewhat unusual issue, some junior devs were in timezones quite different from the majority of senior developers who would actually be the ones reviewing their code. This meant that there could be long waits for feedback, which slowed the team down and reduced our productivity. These checks meant that the junior developers could work more asynchronously with shorter feedback loops for the simple things from the automate checks and ensured focused relevant discussions about the code could be had during the limited overlapping hours.

Easy Additional Improvements

Once this framework was implemented it was super easy to add in some nice to have features such as:

  • Thing which make for clean diffs in git:
    • End of file newlines (end-of-file-fixer)
    • End of line whitespace (trailing-whitespace)
  • Things which make for better code:
    • Static code analysis (flake8)

with:

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.3.0
    hooks:
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
    -   id: flake8
-   repo: https://github.com/psf/black
    rev: 19.3b0
    hooks:
    -   id: black

A variety of useful helpers are available here: https://github.com/pre-commit/pre-commit-hooks