We need our software builds to label the software with a version number.
We want each build to produce a label that is unique enough that we can track a binary back to it's build and the commit it was based on.
We want to follow "Semantic Versioning" because our industry as a whole has decided it's useful.
We want the version number to be predictable, so that (as developers) we know what version of the software we're working on.
We use a centralized git workflow commonly known as feature branch or topic branch.
The official Semantic Versioning spec (as listed on SemVer.org requires a three part version number:
Given a version number MAJOR.MINOR.PATCH, increment the:
MAJOR version when you make incompatible API changes, MINOR version when you add functionality in a backwards compatible manner, and PATCH version when you make backwards compatible bug fixes. Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
A "version number" is therefore a series of three numbers, like "18.10.0" -- it does not include any prefix which might be in the release tag, nor a fourth digit which might be used by assembly versions, nor any pre-release or metadata information.
A "semantic version" is a version number with an optional labels for pre-release and build metadata:
- A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version.
- Build metadata MAY be denoted by appending a plus sign and a series of dot separated identifiers immediately following the patch or pre-release version.
We follow the identifier syntax of semantic versioning as prescribed by (SemVer 2), which was revised to ensure that semantic versions will be canonical. Thus:
Identifiers are dot separated. Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes.
NOTE: When dealing with systems that only support SemVer 1 -- notably NuGet 2.0 as used by PowerShell and Chocolatey -- we convert our pre-release identifiers only, by zero-padding the counters and removing the dots.
Feature branching is a simple form of mainline development where a "feature" or "topic" is any kind of change to a project: whether it's a bug fix, new functionality, or even just more documentation. In agile terms, a topic represents a "releasable iteration" or a work item in our backlog.
In mainline development, the main
(sometimes called master
) branch is special, and represents the official project history. It is always in a state that could be deployed, and indeed, the tip of master
inherently represents the current release. Only releasable iterations are merged to master as this triggers the deployment pipeline, including integration testing.
In Feature Branching, a developer creates a branch to start an iteration, and merges it to master only when the work is complete.
NOTE: this is in contrast to Continuous Integration, where you merge back and forth between master and open topic branches frequently.
- Each commit to master increments the version number
- Each commit to any other branch increments a pre-release counter
- Any build can generate a unique, predictable build number by counting commits
We can put additional semantic metadata into the informational version number, like the branch name, the commit sha, the build id, or even the date.
In the case of .NET assembly file versions, the revision number (the 4th digit) may carry the pre-release counter or the build id, since System.Version doesn't support semantic version -prerelease
tags. We'll still put the full semantic version into the "Informational Version" on the assembly.
We use GitVersion to calculate the version based on tagged commits and the feature branch workflow.
It is fully configurable, and each repository can tweak it via a GitVersion.yml
file.
By default GitVersion increments the patch
, so if you don't do anything, you'll get basically the same results you do today.
GitVersion supports adding a line to your commit or merge message to specify the type of change. You can change the pattern of the message that's required in your GitVersion.yml but by default you would use:
- `+semver:breaking` or `+semver:major` to increment the major version when you make incompatible API changes
- `+semver:feature` or `+semver:minor` to increment the minor version when you add functionality in a backwards compatible manner
- `+semver:fix` or `+semver:patch` to increment the patch version when you make backwards compatible bug fixes
Since it calculates the version based on tags and commit messages, the version number is fully controllable, predictable, and semantic. You should be able to get the same version number building locally as on the build server. Note that when merging, if your branch contains multiple +semver
indicators in the commit history, the highest one takes effect. A simple option to prevent confusion is to configure GitVersion to look only at merge messages, and then require the version line in your PR descriptions -- your CI system usually adds the description to the merge commit message by default.
- If the commit being built is tagged with a version string, the version will always match the tag
- In mainline mode, the version is incremented for every commit on master since the last tag
- We tag master when we successfully complete a build on master, to ensure builds from other feature branches increment appropriately
- On feature branches, we are implicitly working on the next version, and we use the branch name as a pre-release label, and increment the counter for each commit
- On hotfix branches, we use "rc" as the pre-release label, and tag manually when we release from a hotfix branch
GitVersion calculates the build number and includes semantic metadata which we can use in the "informational version" on assemblies and the release notes in PowerShell module manifests. A full informational build number should include at least the sha
and the date
, but depending on configuration, your build might be numbered like this:
19.6.0-beta.5+Build.8125.Branch.joelbennett-13452-add-abort-button.Date.2019-05-22.Sha.df9d5e2f6246344c5763b1ea4801edddcfe8e0c7
NOTE: This is just documentation of an agreed-upon work process at one of my previous teams...
Changes, whether for stories, bugs, or documentation, must be made on feature branches.
Developers create a new branch from the tip of master
each time they begin to work on a new work item. The branches should be as small, focused, and short-lived as is practical. We give them descriptive names representing the focus of the branch. This may include a work item number, but that number is insufficient on it's own. To accommodate multiple feature teams working within shared source control projects, we recommend that each team or developer use their name as a prefix on their branch names, and use forward slashes to separate the prefix. A good branch name might be, for example: joelbennett/13452/add-abort-button
...
git switch -c $branchName
As they work, developers should commit their work frequently, and push their branch to the central repository for backup and collaboration. Developers must also regularly update their feature branch from their source branch by rebasing on top of any changes that are added to it. Each time they update, they must use the --force-with-lease
git option to push the rebased feature branch back to the remote server (see --force considered harmful).
git commit -am $commitMessage
git rebase origin/master
git push origin HEAD --force-with-lease
When developers need to discuss a feature branch with others (as they must, before merging it), they may create a pull-request at any time, and can simply mention in the description if the code is not ready to be merged (put "WIP" on the front, or leave the pull-request in draft
mode).
# azure repos (see https://github.com/Azure/azure-devops-cli-extension)
az repos pr create
# github (see https://hub.github.com/)
hub pull-request
Developers should consider the state of their feature branch's commit history before creating any pull request: if it's messy or confusing, it may be cleaned up with a git rebase --interactive
.
Changes can be merged to master only after approval of the pull-request. Normally, each repository will have an automated CI build which runs the full test suite on pull requests, and produces a release candidate build or deployment package which can be used for further testing.
Developers can fix any issues or address code-review comments by simply adding commits to the feature branch to update the pull request.
Project teams should only merge pull requests when:
- The team is happy with the changes
- The branch is up-to-date with it's source branch
- The pull-request passes the full test suite
- The changes are ready to be released
- The feature is intended for the next release