I hope we can agree that source control is a necessity for every software project. The most popular option for Cocoa and Swift development is Git. I've been using it for many years and I continue to appreciate its power and simplicity every day. It's easy to pick up thanks to its gentle learning curve. If you're not comfortable using Git from the command line, then take your pick from the many native clients for iOS and macOS.
Putting a project under source control is only the first step, though. Most projects have a minimum of two branches,
develop. The last commit of
master reflects the current state of the last release whereas the last commit of
develop reflects the current state of the next release.
develop may be sufficient for a team of one, but it isn't a strategy I recommend. Several years ago, I came across the branching strategy of Vincent Driessen. His solution fundamentally changed how I manage releases and organize development. This episode focuses on the branching strategy I use in every project I work on.
A Better Branching Strategy
develop can work for small projects that don't change often. This setup is simple and lightweight, but it has its drawbacks. Consider the following example. You decide to add a new feature to your application. The feature is quite complex and you estimate it will take you several weeks to implement. You check out
develop and start working on the feature.
A few weeks into the development of the feature and after hundreds of lines of code, you run into a major obstacle. You decide to shelf the feature for the time being. What do you do? Do you revert the changes you made up until that point? Or do you comment out the code you wrote? Having only
develop keeps everything simple, but it's limited. You need a better solution.
Features and Releases
The branching strategy I use isn't complex. It simply requires you and your team to stick to a few rules. Vincent Driessen defines two types of branches, the main branches and supporting branches.
The main branches are
develop. They have an infinite lifetime. Supporting branches have a limited lifetime. They are created with a specific goal and destroyed when they are no longer needed.
Let's revisit the previous example. How can we implement a feature without running into problems? The solution is surprisingly simple. We create a feature branch. A feature branch starts its life on
develop and, once the feature is ready to be released, the changes are merged into
develop. It's safe to destroy the feature branch after merging it into
Adopting this simple strategy has several benefits. Feature development is isolated and doesn't affect
develop, making parallel development possible and straightforward. Scrapping the feature a few weeks into development isn't a problem and doesn't affect
develop or the work of other team members. By adding a little bit of complexity, you gain the flexibility you need.
You can also use this branching strategy for bug fixes and it works well in a team if you create a pull request for every feature or bug fix that goes into the next release. Code reviews are focused and it's easy to make changes or collaborate with team members without touching
develop. Remember that you only merge a feature or bug fix branch into
develop when the feature or bug fix is complete and tested.
It's up to you to decide on a naming convention for feature and bug fix branches. I prefix feature branches with
feature/ and bug fix branches with
bug/. There's a reason for using a forward slash instead of a dash. Tower and most other Git clients treat this format as a path. They create a folder named
bug in which feature and bug fix branches are located. This is convenient if you're frequently creating supporting branches.
Feature and bug fix branches are a nice improvement over having only
develop to work with. The power of this branching strategy becomes clear once you start to use supporting branches for release management. We add another bit of complexity for more flexibility. Remember that
develop reflects the current state of development. When a feature or bug fix is completed, it is merged into
develop and scheduled for the next release. We use a similar approach for managing releases. We create a release branch from
develop when its current state is considered ready for release.
Creating a release branch follows the pattern of feature and bug fix branches. A release branch starts its life on
develop. The name of the release branch is the version of the release prefixed with
release/. The release branch serves three goals. First, the release is prepared. This includes updating the version number. Second, the release is tested and bug fixes are applied to the release branch. Third, by creating a release branch
develop isn't blocked. It can still receive changes.
It's important to emphasize that no major changes should be introduced into the release branch. Only bug fixes that block the release should be included.
The moment you or your team are confident about the stability of the upcoming release, a release candidate is created from the release branch and submitted to App Store Connect or deployed to production, depending on the type of software you're developing.
After releasing or deploying to production, the release branch is merged into
develop. Merging the release branch back into
develop is sometimes overlooked. It's essential that any changes that were introduced into the release branch, such as bug fixes, make their way back into
develop. After merging the release branch into
master you tag
master with the version of the release.
That's it. It may feel complex at first, but I can assure you that it's a fantastic strategy once you feel comfortable using it.
Creating a Release Branch
When should you create a release branch? You have two options. The first option is probably the most common and more traditional option. What's included in the next release is defined beforehand. When every feature and bug fix has been merged into
develop, a release branch is created and a release candidate is scheduled for release. The second option is quite different. Instead of defining a release beforehand, the team adopts a release schedule or release cadence.
Consider the following example. Your team decides on a code freeze every Wednesday. The code freeze means in practice the creation of a release branch. After a few days of testing and applying bug fixes, the release goes live. By adopting this option, a new release is pushed to production on a regular basis, for example, every week or every month.
I'm a fan of adopting a release schedule because it makes the release process predictable and more transparent. A release isn't a special event. It also keeps the team sharp and bug fixes make their way to production more quickly. There's less need for hotfixes.
There's another kind of supporting branch that needs mentioning, hotfix branches. As the name implies, a hotfix branch is used to fix a critical problem in production. Consider the following example. You create a release branch from
develop. From the moment the release branch is created, it's fine to add new commits to
develop because the release branch isn't affected by what's happening on
develop. A few days later, you release the build to production and the release branch is merged into
A team member has completed its feature and merges it into
develop. A few hours later, another team member spots a critical problem in production. The issue needs to be patched as soon as possible. What do you do? You apply a hotfix. How do you create a hotfix? Which branch should the hotfix branch originate from?
It may make sense to follow the same branching strategy I outlined for a regular release. That isn't a good idea, though. A hotfix should only include the changes that are required to fix the critical problem. Why is that? A hotfix needs to be applied as soon as possible and you usually don't have the time to test a hotfix as thoroughly as a regular release.
You can't use
develop as the originating branch of the hotfix branch because it's possible that
develop has changes that shouldn't be included in the hotfix. The answer is simple. You create a hotfix branch from
master, apply the solution, and release the hotfix. After releasing the hotfix, the hotfix branch is merged into
develop, like a regular release. If there's an active release branch present the moment the hotfix branch is created, then the hotfix branch is merged into
master and the release branch. The changes of the hotfix automatically make their way into
develop when the active release branch is released.
Hotfixes should only be applied in emergencies. If the problem in production isn't critical, then it may be better to include the bug fix in the next release.
This branching strategy really shines in combination with a continuous integration solution, such as Jenkins, Bitrise, or CircleCI. I won't cover the setup in this episode because it depends on the solution you're using. The general idea is straightforward.
Every time a commit is pushed to
develop, the build server creates a build for testing. When a release or hotfix branch is created or a commit is pushed to a release or hotfix branch, the build server creates a production build that is automatically uploaded to App Store Connect and optionally sent out to testers.
The key advantage of using a build server is automation. It's important that no manual steps are required to create a release or test build. In other words, the build that is sent to testers should be identical to the release candidate that is uploaded to App Store Connect.
Having a reliable build server in place is wonderful and it's a must have for every team, large or small.
Earlier in this episode, I mentioned that
master is tagged every time a release is pushed to production. There are no strict rules to tag
master, but most developers tag
master with the version of the release. That's a good start, but I'd like to take it one step further.
As a developer, it's essential to know which commit was used to create a build. This is true for release builds as well as test builds. Tagging can help you with this. You can save time and reduce frustration by tagging commits with the build number of the build. This is easy to implement if you have a build server in place. Incrementing the build number and tagging the commit should be automated to avoid mistakes.
If release and test builds are created manually, then it's difficult or impossible to figure out which commit was used to create the build. As I mentioned earlier, automation is important. Invest some time in automation to save time and eliminate mistakes.
I'd like to end this episode by talking about best practices. A common mistake developers make is blindly adopting best practices. If you ask them why they use a branching strategy, then they don't have a good answer. That is why they tend to struggle with the implementation, make mistakes, and end up frustrated.
This isn't surprising. A best practice, pattern, or strategy only starts to make sense once you see its benefits. Don't adopt the Model-View-ViewModel pattern because you were told it's better than the Model-View-Controller pattern or because other developers are raving about it. Be critical and only adopt a best practice, pattern, or strategy if you're convinced of its benefits.
By applying the branching strategy I outlined in this episode, tagging commits, and using a basic solution for tracking bugs, you start to feel in control of the projects you work on. You no longer have the feeling that bug reports are falling through the cracks and that features keep getting delayed. Even for a team of one, these simple solutions can prove invaluable. Or should I say especially for a team of one.