Branch Based Versioning
An approach to software versioning that does away with manual versioning, unnecessary check-ins or dependency on environmental variables. It is mainly of use inside agile project teams set up to continuously deliver value (for example releasing on a weekly basis). It contrasts this approach with snapshot publishing in the Snapshot Anti-Pattern section.
Preface
The approach described below assumes:
- a software VCS, such as Git, that supports branching and tagging
- a build tool, such as sbt, that can be configured to run tests; interact with the VCS and publish the compiled artefacts to an artefact repository for use by other projects
- semantic versioning although the same approach could be used with other versioning schemes
- versioning for any one project will be done sequentially on a single CI agent.
This technique is a type of dynamic versioning.
Features
- No need to change build files or commit in order to version and publish
- By default artefacts are published as dependable non-snapshot versions.
- Branch naming determines whether the artefact is published as work-in-progress snapshot or final release version
- Major version increments still manually controlled
- Branch naming determines if the semantic version has its minor or patch number incremented.
How it Works
To use branch based versioning there has to be an agreement about the naming convention used for different versions which will determine the versioning behaviour for that branch. It is assumed that committing to a branch will result in a branch build being triggered on the CI server.
master branch: This could be named ‘master’ if using git branching or ‘develop’ if using ‘gitflow’ branching. It will be referred to as master from now on. Initially this branch will be manually tagged with the first pre-release version say 0.0.0. Branches should only be merged onto this branch if they are complete and a production candidate. On branch build the CI system will:
- compile and run tests, terminating on failure
- find the previous latest version tag on the branch, lets call this x.y.z
- tag the commit with the tag x.(y+1).0 (i.e. the first version will be 0.1.0)
- publish the artefact with the tagged version.
features branches: These could be branches prefixed with ‘feature-’ and will be taken and rebased off the head of the master branch. On branch build the CI system will:
- compile and run tests, terminating on failure
Nothing is published by default on feature branches, the Snapshot Anti-Pattern explains why.
hotfix branches: These could be branches prefixed ‘hotfix-’. Where a problem is discovered with a historical version x.y.0 and a decision has been made to patch that version as opposed to upgrade to the latest version a branch will be created from the version tagged x.y.0 and match the hotfix naming convention (e.g. ‘hotfix-crash-on-empty-friend-list’). On branch build the CI system will:
- compile and run tests, terminating on failure
- find the previous latest version tag on the branch, lets call this x.y.z
- tag the commit with the tag x.y.(z+1)
- publish the artefact with the tagged version.
Release 1.0.0 and Subsequent Major Versions
The major version, usually incremented ‘1’ when the project has reached a point of maturity where it is suitable for production use and subsequently when changes in the external API are incompatible with what has gone before. This needs to be done manually by applying a tag with the major version incremented.
Snapshot Anti-Pattern
From the Maven Getting Started Guide:
“The SNAPSHOT value refers to the ‘latest’ code along a development branch, and provides no guarantee the code is stable or unchanging”.
That seems clear but inconsistent with the desire that changes to the master branch are production candidates. Which brings us to the anti-pattern:
- The latest version on the master branch is a snapshot version. CI publishes this to the artefact repository on every merge overwriting the previous snapshot version.
- Projects dependent on this project reference the snapshot version as they want the latest features and the last final release version is way out of date.
- On feature completion the branch is merged back to master still with the same snapshot version.
- Dependent projects who want the latest snapshot version fail when their local cache of the artefact is not refreshed as desired.
- Dependent projects who want stability fail when a new version is suddenly refreshed which exhibits different behaviour.
- QA fails when the assembled application that contains snapshot versions passes all QA stages (functional, non-functional, performance, volume). This is because they having a working application but it’s constituent parts are neither tagged in VCS nor readily reproducible from source. Finalising versions at this stage and re-building produces a new application, that may contain additional features and bugs, which needs to be QA’d from the start again, and may fail.
Sharing Work In Progress
One valid use case for using snapshots is where a feature needs to be verified by another referencing project before it can be merged. In this case a manual publishing step can be executed (locally or on CI) on the feature branch which does the following:
- compile and run tests, terminating on failure
- find the previous latest version tag on the branch, lets call this x.y.z
- publish the artefact with the version x.(y+1).0-featureName-SNAPSHOT
Including the feature name in the artefact means that two features sharing work in progress will not conflict. Once complete the merged project will tagged and a final version produced that should be referenced by the referencing project prior to merge.
Conclusion
The shortcomings of following the Snapshot Anti-Pattern have been discussed and with branch based versioning proposed as a superior technique.
Appendix — Implementation Using Git and sbt
The sbt build tool can be configured to do branch based versioning on git as the version can be a result of a method evaluated at startup and plugins provide good Git integration.
Get Git details. Git sbt plugin used for this in practice:
# Exits with 1 if their a uncommitted differences
> git diff-index --quiet --exit-code HEAD# Outputs string of format: lastTag-numCommitsSinceTag-hash
> git describe --long --tags# Outputs current branch name
git rev-parse --abbrev-ref HEAD
Define BranchType, SemanticVersion and PublishAction:
trait BranchType
case object Master extends BranchType
case object Feature extends BranchType
case object HotFix extends BranchType
case object Unknown extends BranchTypecase class SemVer(major: Int, minor: Int, patch: Int) {
def incMinor: SemVer = this.copy(minor = minor + 1, patch = 0)
def incPatch: SemVer = this.copy(patch = patch + 1)
def toString: String = s"$major.$minor.$patch"
}trait PublishAction
case object Unchanged extends PublishAction
case class TagAndPublish(version: String) extends PublishAction
case class PublishOnly(version: String) extends PublishAction
Once we derive BranchType from branch naming conventions (not shown), parse lastTag to give SemVer (not shown) we can work out PublishAction:
def publishAction(
hasUncommittedChanges: Boolean,
numCommitsSinceTag: Int,
semVer: SemVer,
branchType: BranchType,
branchName: String
): PublishAction = {
val snapAction = PublishOnly(s"$semVer-$branchName-SNAPSHOT")
branchType match {
case _ if hasUncommittedChanges => snapAction
case _ if numCommitsSinceTag == 0 => Unchanged
case Master => TagAndPublish(s"${semVer.incMinor}")
case HotFix => TagAndPublish(s"${semVer.incPatch}")
case _ => snapAction
}
}
Based on the publish action we can drive our tagging and publishing (not shown).
Tools. Before building your own branch based versioning I suggest you look at the excellent tools that help in doing it or do something similar.