Keeping it Simple
In the past months my team and I have spent some time reflecting our development processes in general and our Git workflow in particular.
One of my favorite things to do these days is questioning why something has to be that complex. Over time every single aspect of our work has become increasingly complex and many people come up with solutions to these problems. But looking at these fancy solutions often doesn't make things easier since now you have to understand why something was solved that way, what benefits it brings over doing it in a different way and if the assumption under which this solution was designed even applies to our situation.
I feel it's very refreshing to forget about everything that's out there for a moment and start from scratch looking at the big picture first, reflecting the actual problem and what's the simplest solution to it. Then think about what the limitation are to this solution and if there are ways to work around these in an elegant way or if we might want to come up with a different solution for the edge cases.
Don't confuse this approach with quickly hacking something that "works for now". In fact a lot of thought usually goes into creating a very minimal solution. After questioning and rethinking how to set up our development environments with Chef and Vagrant, how to design our build process with Jenkins and how to deploy code to production, the next thing was to come up with a Git workflow that's simple enough to bring back the fun into committing code :)
There are many workflows out there that are trying to bring some structure into the process of collaboratively working on the same project. The most known is definitely the branching model described in this post by Vincent Driessen.
There's another highly interesting blog post that mentions a couple of different approaches and then there's this post on the Atlassian website that nicely explains and visualizes four different workflow types.
Git is a very powerful tool with some highly interesting concepts. These days it's kind of uncool to NOT use Git, although I'm sure most developers only use a small subset (and I won't blame you, because this is actually what this post is all about). At the same time all the features Git offers seems to inspire people to come up with the possibly most difficult solution to any problem, just because they can. Being a smart developer is all about coming up with solutions that only a very small group of equally smart developers can follow, right? Well, maybe not...
Don't get me wrong: I like Git. I like distributed version control. I like how snappy and fast it is and after working with CVS and SVN I'll never want to go back.
But: Dealing with version control was never a big deal. Before we started using Git a couple of years ago. We used to develop something, commit it and we were done. Occasionally we had do deal with conflicts and that was highly annoying, but you didn't need many weeks to fully grasp a development workflow and you didn't need to constantly look things up on a cheatsheet. Also there weren't too many things you could screw up. (OK, the commit history didn't look as pretty, but so what? At least we didn't spend days rewriting histories just to make them look pretty).
I know at some point while reading this blog post you'll want to yell at me: "You just didn't understand what Git is all about, stupid!", preparing yourself to have to teach me much how simple rebasing, squashing, cherry-picking, stashing and subtree-merging was and about all the great scenarios you could handle with these tools.
I'll still like Git, and I might still like you, but: At this point I might not care too much about what's possible with Git. There are a lot more exciting things to play with than version control.
But, let's forget about all of this for a minute and reflect what's important to us:
Our development teams mostly do project work and there's a couple of modules out there (Magento, TYPO3 and some more stuff...) that we're maintaining.
I think to most important think now is to understand that it highly depends on what the team is developing in order to come up with a Git workflow. That's the point that I'm missing in all resources mentioned above. We're not selling any products. We're (mostly) not creating frameworks. We're not maintaining any public APIs.
Let's forget about our modules, tools and extensions and focus on our project work. The majority of all our commits go into projects and project specific (private) repositories.
So we're mostly talking about projects with a classic application lifecycle: develop, build, test, deploy.
Now, the point is:
- We will not have multiple versions of that project "out there" at the same time (we're not talking about modules here...).
- All development work always goes into the latest version. We never go back and continue developing on an older snapshot.
- We don't backport features into older versions
- We do have a build process in place that will create a snapshot of the project in time.
But of course there's more requirements
- We don't want unfinished features to block other code from going out to production.
- Critical bugs might happen and we need a quick way of fixing them and rolling them out after testing.
Don't focus on the rare edge-cases
I don't think it makes any sense to come up with a workflow that will cover every single edge case and in turn will make your regular work a lot more difficult. Let's just focus on what we do most of the time and find "good enough" solutions for the edge cases.
As a result of these requirements we came up with following workflow:
Instead of having a master branch, a develop branch, hotfix, release and feature branches, I'd like to keep this to a minimum. And the minimum is
- a single mainline branch ("master")
- one short-lived feature branch per feature with a speaking branch name.
The mainline branch is what we feed into our continuous integration pipeline. We'll create a snapshot, package it into a build package, run a series of tests and if everything passes this snapshot turns into a release candidate. The product owner / project manager decides if and when it goes out to production.
We still want to isolate any feature development from the mainline, so that we can focus on the feature were currently working on without constantly having to worry about what else would be happening on the same branch. You'll be faced with potential conflicts only once: When merging your branch back into the mainline.
A feature should only be merged into the mainline branch if it's "done done". Our expectation is that it will still work after it meets the other new features on the integration environment. If the series of tests proofs us wrong we'll go back and fix the bug and/or revert the merge. There's no need to be pessimistic about this in the first place and to come up with a solution that will test every feature branch individually.
The merging should happen with the
--no-ff option so that we'll be able to tell that this had been a feature branch even if nothing else might have been committed to the main branch since the feature branch was created.
No commits to the mainline, only merges!
We also established to rule that there can't be any real commits on the mainline. Every single line of code needs to come in as a merge from a feature branch. This also forces us to make every code modification/addition under the context of a user story or bugfix ticket since this is part of the naming convention we use to name the feature branches.
I'm particular bad at following this rule and might not end up creating a feature branch for a simple single-commit task. While I see it's important to be consistent, in many cases I'm also too lazy and reluctant to do this just to follow a process without seeing any real benefit. This is driving some people on my team crazy. :)
How do we deal with hotfixes? While this is hopefully a rare exception, it's still important enough to consider in our workflow.
The problem here is that we need to create a new build that's equal to the one that's currently on production with the only change that it has the bugfix included. We do not want any other feature that might have been merged in the meantime to accidentally end up on production as a side effect of deploying a hotfix release.
Basically there are two different scenarios here:
- Nothing else has been merged (or committed) into the mainline branch since the build has been created that's now on production and contains the bug.
- Something else has been merged (or committed) into the mainline branch since that build was created.
It's very unlikely that bugs will be found days after that build was deployed. In most cases you'll know within the first hour or two. And the chances that another feature branch was merged into the mainline are low. In this case we can treat our bugfix exactly like a feature branch. We branch off master, fix the bug, and merge it back after testing it. A new build package will be created, unit tests, selenium tests,... and the build can be deployed to production.
In case another feature branch was merged since then we need to get a little more fancy. (Please note, that so far this is the only special case we need to cover in our simplistic workflow.) So now we need to find the commit that was used to create the current build. That shouldn't be too hard since your CI server will probably keep track of this or you might automatically tag your Git repo on successful builds. Now is the time when we'll create a hotfix branch from an earlier point in time (back when nothing else was merged into the mainline). We fix the bug in this hotfix branch and now the trick is to create a build from this hotfix branch instead. Implementing this with Jenkins is very easy. Instead of hardcoding the branch to "master" you can make the branch an input parameter (that defaults to "master"). The build that get's created now follows the exact pipeline and is ready to be deployed to production after all the tests have passed. The original - and modified - mainline is not included in this build. Now, after deploying this build to production you'll merge the hotfix branch back into the master branch and continue from there.
So this is our very simple workflow. After trying several other workflows I'm very happy with how this is working out now. Also, our approach seems to be pretty close to what the GitHub team is doing. I'm enjoying the fact that we don't have any overhead and that our development process went back to being easy to understand and to follow. I still haven't experienced any limitations of getting rid of the extra develop, features and hotfix branches. I love that it's very clear where any new (feature/hotfix) branches originate from and where the go to. I love that there's no "release manager" role that needs to take care of merging a branch into another one or preparing any release branches. And I love that I can worry less about version control now and spend time to do the what I like most: writing code.