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
mainvia pull requests, increasing the PATCH version. - Builds on
mainare potential releases. - After a release, tag the next merge to
mainto increase the MINOR version. - Create a
support/x.ybranch for post-release fixes, only if needed. - Merge hotfixes into
support/x.yand then mergesupportback 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.

