Update as of 29 February 2020
The article below was written 2 years ago. During the last 2 years, a lot has changed regarding the tooling I have been using. I’m currently using Azure DevOps on a daily basis and it has completely replaced the need for using BitBucket, Jenkins, Octopus Deploy, own private npm or NuGet feeds as Azure DevOps has it all in-the-box. Apart from all those features, it also allows us to keep track of our Agile process using boards, user-stories, backlogs, release management and test plans. Even if you’re not a Microsoft fan-boy or you’re building Java, nodejs or other non-.NET applications, consider using Azure DevOps as it greatly simplifies the process.
During that same period, I’ve also switched to another GUI-based git client that is worth mentioning. It’s name if Fork, still free of charge as of today, but soon it will be $49.99 as mentioned on their website.
Even while switching tools, we’re still using the same process as described in this post. Just keep in mind, whenever you read Jenkins, Octopos Deploy, BitBucket, etc, you can as well think Azure DevOps :)
Having a good, automated workflow for your project can be hard. In this article I will write about the way I’ve been working in the past to get a smooth workflow from writing code to the actual deployment.
- Just because it works for me doesn’t mean it’ll work for you, but it might get you started.
- The tools mentioned in this article are the ones I’m familiar with, almost all of them have great alternatives.
A lot has been written about the gitflow workflow in commercial and non-commercial posts, so let’s just point to a few interesting articles:
- A successful Git branching model by Vincent Driessen
- Feature Branch Workflow by Atlassian
- Gitflow Workflow by Atlassian
Many UI tools are available that simplify the process of working with git and gitflow. Sourcetree from Atlassian is a great, free UI tool for Mac and Windows that supports git and gitflow. GitKraken another example that also runs on Linux but is not free of charge. Fan boys can stick to the CLI of course!
Branch naming convention
In gitflow, you’ll always have a develop and a master branch. The develop branch is where we merge in or squash in finished feature branches. The master branch is where we merge in tested release branches or hotfix branches (bugfixes/patches).
For release and hotfix branch names, some guides will use hotfix- and release- as prefix, while others will use hotfix/ and release/ as prefix. This depends on user or team preference, but know that Sourcetree groups branches in some kind of tree-structure when they use the / as separator in their name, which can be convenient.
Continuous Integration (CI)
Each commit that gets pushed to a repository should get picked up automatically by some kind of Continuous Integration build system. Jenkins is probably one of the most commonly used on-premise build systems used for continuous integration.
The CI system is responsible for trying to integrate proposed code changes with a destination branch. F.e. when creating a pull-request for a feature branch, the CI system will:
- try to merge the feature branch into the destination branch (develop in this case)
- build the merged result while enforcing coding style
- run unit-tests while capturing code-coverage
- run integration-tests
- create a package
- deploy the package (if continuous deployment is being used for the given branch)
Running integration-tests can be a lengthy process as it often involves creating databases from scratch and emulating user actions on a live running instance. In this case, we can decide not to run integration-tests for pull-request and only run them for release and hotfix branches.
There’s also the additional complexity of different database schemes for different branches, therefore a good rule is to let the tests itself drop and recreate the database themselves at the start.
When working in a team on a serious project, nobody should ever directly commit to the develop or master branch. Any change to one of those branches should be requested by the means of a pull-request. This allows us to detect breaking changes early in the process before actually breaking the code-base for everyone. Creating a pull-request is putting your code up for review by other coworkers but also to trigger the Continuous Integration system to check if our changes won’t break anything (see Continuous Integration topic above).
When using BitBucket f.e., you can disallow merging of a pull-request if it doesn’t pass Continuous Integration. This can be configured in the settings of a repository.
In a team working on the same code-base, code-quality and knowledge sharing can be improved by introducing code reviews. Even when the team only consists of two people, code reviewing can be benefiting.
Also here, when using BitBucket, you can disallow merging of a pull-request before a certain number of coworkers have approved your change.
The time of your coworker is precious, so don’t bother them with code style issues, formatting issues and changes that are unrelated to the feature or issue you’re trying to resolve. Commit messages and linked issues should clearly describe why the change is needed.
For the same reason, always keep the changes as small as possible:
- Don’t reformat a complete file/directory when fixing a bug or adding a feature. Create a separate pull-request for the reformatting alone or write down an issue and do it later
- If code needs to be refactored before you can add the new feature, consider if you can do the refactor first, in a separate pull-request
As one last note on code reviews: adding one or more unit-tests should be mandatory and also increases the confidence coworkers will have in your change.
Squash vs merge
Once the pull-request has been approved by the Continuous Integration system and one or more coworkers, you can apply your changes to the destination branch.
With git (and thus BitBucket), there are two ways to do this. You can:
- Merge the pull-request
- Squash the pull-request
Merging the pull-request leaves the separate commits that were needed for a certain pull-request visible in the repository and you’ll clearly see a merge to the destination branch. In most cases, these separate commits don’t have any added value and will only pollute the source control history. More than often, the message of those separate commits are quickly chosen and meaningless to coworkers (f.e. Remove space, Fix unit-test, Fix test again, Almost giving up, Aaargh $@#&!, etc…). What really matters are the final changes that were needed to add a certain feature or to fix a certain bug.
This brings us to the other method of applying your pull-request changes to the destination repository: squashing. Squashing means: create a new commit that includes all changes and apply it on top of the destination branch. When the message of that commit is wisely chosen, this results in a flat and clean commit history for the destination branch. The advantage is that all changes for a fix or feature are at one place and can be easily reverted if that would be needed.
Don’t forget: always remove the source branch after the changes have been incorporated into the destination branch.
Whether you’re building an npm package, NuGet package or anything else that uses Semantic Versioning, you’ll want to pick a certain versioning scheme that plays well with the different branches in gitflow (master, develop, feature, hotfix, release).
For the master branch, the version can be extracted from the tag, as every merge from a release or hotfix branch will add a version tag. F.e. tag `2.1.5` on the master branch will result in package version `v2.1.5`.
For the release branch, the name of the branch can be used with an incremental build number appended. F.e. a release branch named `release/2.2.0` will result in package version `v2.2.0-beta0001` or when a certain commit is tagged `rc1`, the version will become `v2.2.0-rc1`.
For the develop branch, we could derive the version from the master branch and increment the minor number and append an incremental build number or short git commit hash. F.e. when the master branch is tagged `2.1.5` the version will become `v2.2.0-unstable2`. If you want to be able to build a package for a historical commit from the develop branch, this approach might not work and it might be better to resort to a file containing the next version which gets committed into the develop branch.
Versioning can quickly become complex to incorporate into your build script. Luckily there’s an open-source project that aims to simplify this process and has all kind of integration's with different build systems. Check out the GitVersion project on github and its documentation.
Continuous Delivery (CD)
Deploying and delivering a new version to the customer can be hard, especially when this is a manual process. A saying that applies to Continuous Integration also applies to Continuous Delivery:
If it hurts, do it more often — Martin Fowler
In fact, when combined with Continuous Integration, Continuous Delivery should happen all the time. Imagine you have a Staging and a Production environment for your customer, you could automatically deploy every new release from the master branch to the Staging environment, ask the customer to validate the Staging environment after which you promote the new release to production with a single click.
When using containerized images like Docker you could even spin up the new production environment on a temporary URL, have it validated, switch load-balancing to the new environment and take the old production environment offline. Many things are possible and each might have their own set of complexities (database migrations being a big one f.e.).
In the same way, feature branches and release branches could have their dedicated environment that gets automatically redeployed on every new commit.
A tool that can be helpful for managing deploys on different servers at different locations for different customers/tenants is definitely Octopus Deploy. When the application is only used by a single customer and runs on a single server, you could probably configure your CI infrastructure (like Jenkins) to run some deployment scripts.