Many versioning and branching strategies are available. I prefer a simple approach: mainline development using GitVersion for automatic Semantic Versioning.
Branching strategy
Different development scenarios require different branching and versioning strategies. For instance:
- Packaged apps with multiple concurrent versions.
- In-house web applications with distinct integration, test, and production environments.
- Open-source libraries with community development contributions.
I mostly work on commercial web applications with multiple deployment environments but only one production version. I also contribute to open-source projects. My strategy is straightforward: a single main
branch with temporary feature branches that merge back into main
. Feature Flags are used when necessary for testing without affecting production.
Additional branches are created only as needed, such as for urgent production fixes. For example, if new work is already merged into main
, I will create a support/x.y
branch from the last release commit for the hotfix.
I build and package the application once, deploying it to multiple environments with only runtime configuration changes.
A simple branching strategy minimizes complexity and accelerates deployment, though it may not suitable for application that need to support multiple versions.
Mainline strategy
The mainline approach keeps the main
branch in a releasable state, automatically increasing the version PATCH number with each commit. After a release, the next commit to main
is tagged with a new MINOR version number.
Process:
- Develop features on feature branches.
- Merge them into
main
via pull requests, increasing the PATCH version. - Builds on
main
are potential releases. - After a release, tag the next merge to
main
to increase the MINOR version. - Create a
support/x.y
branch for post-release fixes, only if needed. - Merge hotfixes into
support/x.y
and then mergesupport
back intomain
.
Note: GitVersion also supports release/x.y
branches, however they are configured by default for release candidate builds (e.g. 1.5.3-rc.1
); for Mainline mode go straight to support/x.y
branches.
Version numbering
I follow Semantic Versioning, with MAJOR.MINOR.PATCH
format, which has strict semantics for libraries and APIs.
For applications, where there is no public API, I increase the MINOR version with each release and use the PATCH version for builds and hotfixes.
Increasing version numbers
In Mainline mode, the PATCH version increases with each commit to main
. Unlike the default mode (which only increase build metadata), this simplifies integration with packaging systems like NuGet and containers.
I use tags to increase MINOR version numbers after each release, although Gitversion has several other ways to increase the version if you prefer. Using tags speeds up subsequent version calculations.
Using GitVersion
GitVersion automatically generates version numbers based on Git history. For instance, after tagging "v1.5", the next merge to main
will be calculated as 1.5.1
.
GitVersion supports various branching models but I use the simpler Mainline mode, focusing on a single main
branch with feature and hotfix branches as needed. I avoid a separate develop
branch unless it's necessary, such as for community contributions to an open-source project.
The typical GitVersion.yml
configuration I use is:
assembly-versioning-scheme: MajorMinor
assembly-informational-format: '{SemVer}+{ShortSha}'
mode: Mainline
branches:
main:
regex: ^(origin\/)?main$
support:
regex: ^(origin\/)?support\/.+$
ignore:
sha: []
GitVersion's main advantage is that version numbers are independent of the build system, calculated solely from Git history. This is useful for small projects where builds are run locally.
Limitations of GitVersion
GitVersion needs the Git history to calculate version numbers, which can require configuration changes in build systems (that often default to a shallow checkout) and can increase build time and disk space.
Branching diagram
The branching diagram was generated using Mermaid:
%%{init: { 'theme': 'base', 'gitGraph': {'rotateCommitLabel': false}} }%%
gitGraph
commit id: "0.1.0"
commit id: "1.5.0" tag: "v1.5"
branch feature/n1-foo
commit id: "pre-release 1.5.1-feature-n1-foo"
checkout main
branch feature/n2-bar
commit id: "pre-release 1.5.1-feature-n2-bar"
checkout main
merge feature/n1-foo id: "1.5.1"
branch feature/n3-waz
commit id: "pre-release 1.5.2-feature-n3-waz"
checkout main
merge feature/n2-bar id: "1.5.2"
branch support/1.5 order: 2
checkout main
merge feature/n3-waz id: "1.6.0" tag: "v1.6"
branch feature/n4-hum
commit id: "pre-release 1.6.1-feature-n4-hum"
checkout support/1.5
branch hotfix/n5-fix order: 2
commit id: "pre-release 1.5.3-hotfix-n5-fix"
checkout support/1.5
merge hotfix/n5-fix id: "1.5.3"
checkout main
merge support/1.5 id: "1.6.1" type: reverse
Other branching strategies
GitVersion supports other branching strategies like GitFlow and GitHubFlow, often involving more complexity and separate develop
and main
branches. These are more suitable for libraries, APIs, or applications needing concurrent version support.
For complex applications, multiple long-lived support/x
branches might be necessary. Alternatively, having separate branches for each environment (e.g., test
and production
) can clarify what is in each environment but complicate management.
For a detailed discussion on various branching patterns, refer to this article by Martin Fowler.
Conclusions and next steps
Keep your branching strategy simple until complexity is necessary. Start with a single main
branch in Mainline
mode, using GitVersion for automatic Semantic Versioning, increasing the PATCH number for each build.
For most projects, especially new applications, this approach suffices. Tag subsequent main
merges with new MINOR versions for new builds. Create additional branches only when needed.
Fixes can often go directly into main
for the next deployment. Create support/x
branches only for urgent production fixes when new work is already merged into main
.
Keeping it simple ensures smooth project management.