Gamifying Continuous Integration

If you read the internet, you'll soon realise that a large number of software developers don't understand what CI is and why it's important.

It's important to state that continuous integration is not a server.

Like the word DevOps, people seem to think these important principals of software engineering are about tools and products. Perhaps misinformed managers think if they buy the right magic box they can finally deliver their projects on time.

Continuous integration is continuous integration

Read the words.

It's about making sure that when you are working on code, it is the most up to date version of the entire team's code.

If you have multiple versions of code checked out on different branches or machines, then you are not collectively integrated in respect to code. The code you see, is different to the code your colleague sees.

In an ideal world if you could somehow all work on same computer at the same time comfortably, that would be pure CI.

If you've ever used VS-code's Live Share plugin, that's as pure a CI experience you can get in a remote environment. If you've not used it, it allows you to connect to a colleague's VS code and edit the code together as if you're working on the same computer. The changes you make are reflected (or integrated) to everyone else in the session - the feedback is immediate.

A much simpler experience of CI is mobbing. Everyone is working from the same version of the code together.

How do you know if you're not practicing CI well?

One obvious sign is developers wasting lots of time resolving large merge conflicts. Merge conflicts are a direct result of you working on a version of the code that isn't up to date. Your team are not integrating their changes continuously and therefore as they make changes the risk of making invalid changes increases.

The time wasted on merge conflicts has compounding effects as it increases the delays of integrating your own changes to the system.

One might argue you can resolve this by structuring work differently to avoid conflicts, even going as far as to re-architect your system to avoid merge conflicts. An often touted reason to go for microservices is it allows teams to work independently.

Reader, I have worked on a team with 20 odd developers, shipping frequently on a monolith and very rarely did we have merge conflicts. We also didn't do pull requests, we committed to the main branch.

If you don't practice CI you are not working with your team as well as you could

Changes to the system are invisibly stored on your colleague's computers where they are of no use to you.

You could be imagining some great abstraction given the code in front of you but unfortunately the code in front of you is not the source of truth because unknown to you another developer merges a gigantic change after spending a week on a feature.

People become reluctant to refactor parts of the code because someone is working on a branch that is vaguely near the code you want to change so you wait a few days and then forget about it.

How do you CI well?

The simplest way is for everyone to commit to main (or master), frequently. It really is that simple!

To be good at continuous integration, you should continuously integrate your changes to main. That way everyone will be working with the same version of code for the most part.

It's important that you respect your colleagues and run your tests before pushing. If the build is broken the team should stop committing unless it's to directly fix the problem. If people keep committing whilst the build is broken it will become very difficult to diagnose what the problem is and the new commits may compound the issue.

The process, in full

  • git pull -r
  • Make a positive change to the system
  • git commit -am "added blockchain terraform machine-learning module 2.0"
  • git pull -r
  • ./build.sh && git push

This practice is simple to follow, doesn't require elaborate review systems, branch strategies etc, and forces good habits.

  • In order to push frequently, you need to pull changes frequently
  • You can only push frequently if you work on small tasks, iteratively. There are many, many reasons why this is a wonderful, less risky and easier way of working. (see TDD, XP, any decent agile book, etc etc)

It wont entirely stop merge conflicts occurring, but they should be very infrequent and because you're doing small changes they're typically trivial to resolve.

But what about code review?

The industry seems to have conflated reviewing code with a very specific, relatively new process of looking at code when a pull request is submitted; usually too late once all the code is written.

Did you know that you can talk about code, at any time during the day?

Did you also know that the correct abstractions and patterns may not be apparent when the code is "done" and you'd be better off encouraging an environment where people are free to refactor code when they want to, rather than having to go through a laborious process every time they want to change the code. I've worked on and observed projects that practice code review very strictly and yet the code is still not great.

Relying on a process of checking code before it is merged is not going to result in a healthy codebase alone.

With this way of working I strongly encourage pair programming because people rightly assert that as a lone developer it is very easy to do something wrong if you don't gather feedback. Pair programming facilitates a constant feedback loop on what you are writing.

What if someone commits something wrong/broken?

First of all, no process in the world can prevent this. Stuff does go wrong, and you would be better asking yourself:

How can we detect and recover from problems easier?

Secondly, this approach does require a healthy and collaborative team that talk to each other regularly about what they are working on. They should be talking to each other when they start a bit of work about what they are going to do, how they will release it safely, what kind of tests, etc. Couple that with working on small things iteratively the risk of pushing something catastrophic are very low.

If someone does push some code that maybe works (so the tests pass, monitoring is all good, etc) but is actually "bad". Well so what? We can refactor it if we don't like it.

What if I, a tech lead want to check every change before merging

I don't want to work with gatekeepers.

I want to work on a team that trusts one-another.

Perfect is the enemy of good.

So is a system of pull requests bad?

Not at all, they're perfect for open source projects where you want to welcome contributions from other people but dont have implicit trust. In that case you will want to review changes that go into your precious project.

For a team where you trust the developers all it adds is overhead.

Liberation

We should be removing the barriers to changing software, not adding walls and walls of bureaucracy.

In this kind of environment people fix code when they see it needs fixing.

This way of working tightens feedback loops and increases actual meaningful agility.

CI is not just about the code

  • It's important to integrate knowledge of domain, work etc. (so important not to have silos of work with just the BAs/UXD etc)
  • Who is working on what (standups, kick-offs, etc)

Gaming it

I feel very strongly about working this way over a pull request approach when you are working on a team you trust.

Most of my team until recently were very unfamiliar with this way of working so I made a silly dashboard on one of the TVs which showed who committed to the main branch most over a given week. It penalises you if you push a change which breaks the build.

ci-league dashboard

After just a few weeks this slight nudging has improved the team's approach to CI, we've had significantly less merge conflicts, people are refactoring more often and the number of failed builds has also dropped.

The code for it is on github

And if you have docker, you can run it with

$ docker run -p 8000:8000 quii/ci-league

If you want it to look at a private repo you'll need to supply a github personal access token as an environment variable

$ docker run -p 8000:8000 -e GITHUB_TOKEN=supersecret quii/ci-league

It doesn't do any fancy auto-refreshing but most browsers have extensions to auto refresh a tab, we set it to update every 10 minutes here.

Wrapping up

As usual in software, we seem to suffer as an industry of remembering the why of all of these fancy terms.

If you appreciate the why of CI you may not fall in to the trap of blindly applying best practices that work in a different context to the one you are working in.

Pull-requests in an open source project make total sense and work great. In a different context it can add overhead.

You should instead focus on what your environment your team is working in, the constraints you have and use principles like CI to guide what process best fits.