Skip to content

Instantly share code, notes, and snippets.

@Jaykul
Last active August 29, 2024 23:35
Show Gist options
  • Save Jaykul/5687dc25b081e61e41de2a5635b506aa to your computer and use it in GitHub Desktop.
Save Jaykul/5687dc25b081e61e41de2a5635b506aa to your computer and use it in GitHub Desktop.
Versioning

Versioning Software

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.

A note on semantic versioning

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 Development

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 default branch (usually called main or previously master or trunk) is special, and represents the official project history. It is always in a state that could be deployed, and indeed, the tip of your default branch inherently represents the current release. Only releasable iterations are merged to this branch, 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 the default branch only when the work is complete.

NOTE: compare this to Martin Fowler's description of Continuous Integration, where you merge back and forth between the default branch and open feature branches frequently, and use feature flags to hide incomplete work.

Releases are represented by tags on the repository --usually on the default branch.

When release processes are long, or when there are arbitrary release cycles, or a long-term servicing release, it is acceptable to create a release branch. Short term patching branches should only be created on-demand, for issues found during the release process -- and should have the full version of the intended patch as their branch name, like "release/1.2.5". They must be tagged upon release, and then merged back into the default branch. Some teams will call these "hotfix" releases -- but they should follow the same pattern.

Long term servicing branches are not appropriate for continuous delivery apps like web apps and consumer/mobile apps. They are created with a name like "release/1.2" so that multiple releases can be made from them. Each release should still be tagged upon release, and merged back into the default branch.

How SemVer works with feature branching

  • Each commit to the default branch 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.

Using GitVersion for versioning

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.

  1. If the commit being built is tagged with a version string, the version will always match the tag
  2. In mainline mode, the version is incremented for every commit on the default branch since the last tag
  3. We tag that default branch when we successfully complete a build, to ensure builds from other feature branches increment appropriately
  4. 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
  5. On release or 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

Developing software in feature branches

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 the default branch 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/main
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 the default branch 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

As a final note, there are many formal workflows based on this type of branching, such as OneFlow and CommonFlow and GitHub Flow -- I can't endorse any particular one, and each place I've helped to implement these has ended up customizing the workflow for themselves.

A note on builds versus releases or deployments

In the enterprise, we often separate the concept of build (and deliver) from release (and deploy). Continuous delivery becomes about building and delivering a tested and deployable package (for instance: pushing a docker image to a registry, or publishing a nuget package to a feed). In most cases, only test environments will continuously deploy those packages, and there's a separate release step where that package is deployed to the production environment. Releases might be manual, and might depend on a separate approval process such as a change advisory board, or be scheduled for a specific date and time.

In this situation, Teams should create continuous integration and delivery pipelines which build and publish packages for each pull request and merge to the default branch, and then create a separate release pipeline which can deploy any given package to any specified environment. That deployment pipeline should be automatically triggered by the delivery pipeline to deploy to a test environment, and the same process should be used to release the package to the production environment when the time comes. It's critical that release pipelines be able to deploy any given version to any environment, and be able to re-deploy packages which have already been deployed.

# Each merged branch against main will increment the version unless otherwise specified in a commit message
# TrunkBased is the only workflow where each commit to a feature changes the pre-release tag
workflow: TrunkBased/preview1
# mode: ContinuousDeployment
# mode: ContinuousDelivery
# mode: ManualDeployment
# No dashes in date
commit-date-format: "yyyyMMddTHHmmss"
# Use BuildId from Azure DevOps (with fallback)
assembly-versioning-format: '{Major}.{Minor}.{Patch}.{env:BuildCount ?? 0}'
assembly-informational-format: '{Major}.{Minor}.{Patch}{PreReleaseTagWithDash}+Build.{env:BuildCount ?? 0}.Date.{CommitDate}.Branch.{env:SafeBranchName ?? unknown}.Sha.{Sha}'
# Format version bump messages as git trailers
major-version-bump-message: 'semver:\s?(breaking|major)'
minor-version-bump-message: 'semver:\s?(feature|minor)'
patch-version-bump-message: 'semver:\s?(fix|patch)'
no-bump-message: 'semver:\s?(none|skip)'
commit-message-incrementing: Enabled
# semantic-version-format: Loose
branches:
main:
# tracks-release-branches: true
increment: Patch
prevent-increment:
# If false, rebuilds of the same code will increment the version!
when-current-commit-tagged: true
release:
label: rc
# A hotfix is just a release with bad habits
regex: ^(?:releases?|hotfix(es)?)/\d+\.\d+(\.\d+)?$
feature:
# any branch name that starts with feature
# (with any number of / separated segments)
# we use the last segment as the BranchName label...
regex: ^features?[/-](.+[/-])*(?<BranchName>[^/-]+)$
# label: alpha.{BranchName}.
# Since we *know* it's a feature, then we can increment the minor version
increment: Minor
source-branches: ["main", "feature", "release"]
pull-request:
label: alpha.
unknown:
# we usually don't distinguish feature from fix in our branch names
# So EVERYTHING ELSE just increments the patch version
regex: ^.*[-/](?<BranchName>[^/-]+)$
increment: Patch
# label: alpha.{BranchName}.
source-branches: ["main", "release", "feature"]
[CmdletBinding()]
param(
# Path to a folder which exists, where we can create a "VersionTest" folder
$TestRoot = "~/Projects"
)
if ($DebugPreference -ne "SilentlyContinue") {
$DebugPreference = "Continue"
}
if (!(Test-Path $TestRoot)) {
throw "The -TestRoot paramter must be the path to a working directory"
}
if (!(Get-Command GitVersion -ErrorAction SilentlyContinue)) {
if (Get-Command dotnet) {
if (!(dotnet tool list gitversion.tool --global || dotnet tool install gitversion.tool --global)) {
throw "Could not find GitVersion"
}
function GitVersion { dotnet gitversion @args }
}
if (!(Get-Command GitVersion)) {
throw "Could not find GitVersion"
}
}
Get-Item Variable:VersionCounter_* | Remove-Item
function New-Build {
# Fake the Azure build counter:
$Result = GitVersion -NoCache | ConvertFrom-Json
$Counter = "VersionCounter_$($Result.MajorMinorPatch)"
if (Test-Path Variable:$Counter) {
Set-Variable -Scope Global $Counter (1 + (Get-Variable -Scope Global $Counter -ValueOnly))
} else {
Set-Variable -Scope Global $Counter 0
}
$Env:BuildCount = Get-Variable -Scope Global $Counter -ValueOnly
$Env:SafeBranchName = $Result.BranchName -replace '[\\/]','-'
Write-Debug "BuildCount ${Env:BuildCount} for $Counter"
}
function Test-Version {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[Alias("Version")]
$AssemblyVersion,
[Alias("SemVer")]
[AllowNull()]
$SemanticVersion
)
New-Build
try {
GitVersion -NoCache | ConvertFrom-Json -ov Result | ForEach-Object {
Write-Host "Informational SemVer: " $_.InformationalVersion -Fore DarkCyan
}
# JSON parse errors should stop everything
} catch {
Write-Warning "Error parsing GitVersion output:"
GitVersion -NoCache | Out-Host
throw $_
}
if(($AssemblyVersion -split '\.').Count -eq 3) {
$AssemblyVersion = "$AssemblyVersion.0"
}
if ($Result.AssemblySemVer -ne $AssemblyVersion) {
Write-Warning "Expected $AssemblyVersion but got $($Result.AssemblySemVer)"
if($DebugPreference -ne "SilentlyContinue") {
$Result | Out-String | Write-Debug
Wait-Debugger
}
} else {
Write-Host "AssemblySemVer: $($Result.AssemblySemVer)" -Fore Green
}
if ($SemanticVersion) {
if ($Result.SemVer -ne $SemanticVersion) {
Write-Warning "Expected $SemanticVersion but got $($Result.SemVer)"
if($DebugPreference -ne "SilentlyContinue") {
$Result | Out-String | Write-Debug
Wait-Debugger
}
} else {
Write-Host "SemanticVersion: $($Result.SemVer)" -Fore Green
}
}
}
function New-Commit {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Version,
$SemVer,
$Message = "Want $Version",
[string]$branch
)
Write-Host "New Commit: $($Message -replace '\n','\\n')" -ForegroundColor "Cyan"
if ($branch) {
git checkout $branch
}
$Version >> touch.log
git add *
git commit -m $Message
Test-Version $Version $SemVer
}
function New-Feature {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Branch,
$Team = "devops",
$WorkItem = "ABC-1234",
$From = "main"
)
if ($Branch -match "^\w+/") {
$Team = ($Branch -split "/")[0]
}
if ($Branch -match "/[A-Za-z]+-\d+/") {
$WorkItem = $Branch -replace ".*/([A-Za-z]+-\d+)/.*", '$1'
}
$branch = ($Team, $WorkItem, ($Branch -split "/")[-1]) -join "/"
git switch -c $branch $From | Write-Host -ForegroundColor Cyan
$branch
}
function New-Hotfix {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Version,
$From = "main"
)
$Tag = (([Version]$Version).ToString(3))
$branch = "hotfix", $Tag -join "/"
git switch -c $branch $From | Out-Host
$branch
}
function New-Tag {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Version,
$SemVer
)
$Tag = (([Version]$Version).ToString(3))
Write-Host "Tagging main: $Tag"
git tag $Tag
Test-Version $Version $SemVer
}
$global:PR = 999
function New-PullRequest {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Version,
[Parameter(Mandatory)]
$SemVer,
[Parameter(Mandatory)]
[Alias("Branch")]
[string]$FromBranch,
$ToBranch = "main"
)
$global:PR++
Write-Host "New Pull Request ${PR}: $FromBranch -> $ToBranch" -ForegroundColor Cyan
git switch -c "pull/$PR/merge" $ToBranch | Out-Host
git merge --no-ff $FromBranch | Out-Host
Test-Version $Version $SemVer
}
function New-Merge {
[CmdletBinding(DefaultParameterSetName = "BugFix")]
param(
[Parameter(Mandatory, Position = 0)]
[string]$Version,
[Parameter(Mandatory, Position = 1)]
$SemVer,
[Parameter(Mandatory, Position = 2)]
[string]$branch,
[Parameter(Mandatory, ParameterSetName = "BugFix")]
[Alias("Bug", "BugFix", "Patch")]
[string]$Fix,
[Parameter(Mandatory, ParameterSetName = "Feature")]
[Alias("Minor","Adds")]
[string]$Feature,
[Parameter(Mandatory, ParameterSetName = "Breaking")]
[Alias("Major")]
[string]$Breaking,
[string]$commitMessage = $("Merge PR $PR - Want $Version (merge $branch)"),
[switch]$Tag,
[string]$ToBranch = "main"
)
Write-Host "Merge PR $PR from $branch into main" -Foreground Cyan
if ($Breaking) {
$commitMessage += "`n`nsemver:breaking $Breaking"
} elseif ($Feature) {
$commitMessage += "`n`nsemver:feature #$Feature"
} else {
$commitMessage += "`n`nsemver:fix $Fix"
}
git switch $ToBranch
git merge --no-ff -m $commitMessage $branch
# Handle the probable git merge conflict
if ((get-content touch.log) -match "^=+$") {
set-content touch.log ((get-content .\touch.log) -notmatch "[<>=]+")
git add touch.log
git commit -m $commitMessage
}
if ($Tag) {
git tag (([Version]$Version).ToString(3))
}
Test-Version $Version $SemVer
}
function New-Rebase {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
$branch,
$Version,
$SemVer
)
git switch $branch
git rebase main
# Handle the probable git merge conflict
if ((get-content touch.log) -match "^==+$") {
set-content touch.log ((get-content .\touch.log) -notmatch "[<>=]+")
git add touch.log
git rebase --continue
}
if ($Version -or $SemVer) {
Test-Version $Version $SemVer
}
}
## reset
Push-Location $TestRoot
if (Test-Path $TestRoot\VersionTest) {
if(!$PSCmdlet.ShouldProcess("All content of '$(Convert-Path $TestRoot\VersionTest)' will be deleted! Is that OK?", "The test folder already exists.")) {
return
}
Get-Item VersionTest -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ea 0
}
Write-Host "Initializing a git repo in $TestRoot\VersionTest" -Foreground Cyan
mkdir VersionTest -ea 0
Set-Location VersionTest
## Set up your project:
## Set up git
git init
## First commit, adding GitVersion configured to MAINLINE and PATCH increment
Copy-Item $PSScriptRoot\GitVersion.yml
git add *
git commit -m "Initial commit"
## The baseline version in GitVersion 6 is 0.0.1 until you change it
Test-Version 0.0.1
## Lets change the initial version using a tag
$NewVersion = "18.5.1"
Write-Host "Manually set the version $NewVersion by tagging" -Foreground Cyan
New-Tag $NewVersion $NewVersion
## Now the "NEXT" release should be 18.5.2 (incrementing the patch number)
## Imagine DEV1 starts a bug fix
$branchOne = New-Feature "Dev1/ABC-1234/fix"
## Each merge to main will increment (at least) the patch: Major.Minor.PATCH
## While in branches, we expect builds to increment: Major.Minor.Patch.$BuildCount
## And we expect the SemVer PreRelease tag to increment as well: Major.Minor.Patch-PreRelease.$CommitCount+Build.$BuildCount
Copy-Item $PSScriptRoot\About` Versioning.md
New-Commit "18.5.2" "18.5.2-fix.1"
## As we add additional commits, we want to see that in the version...
## The SemVer should be: 18.5.2-fix.2 -- the AssemblyVersion will be .1 (because our "BuildCount" starts at zero)
New-Commit "18.5.2.1" "18.5.2-fix.2"
New-Commit "18.5.2.2" "18.5.2-fix.3"
## Now imagine DEV2 starts a feature
$branchTwo = New-Feature "Dev2/ABC-1242/feat"
# We wish this was 18.6.0-feat, because it's a feature
# But we did not tell gitversion it's a feature, so increments the patch
# Our BuildCount (in the assembly version) does not care about the name of the pre-release
# It has to monotonically increase (and doesn't have labels)
New-Commit "18.5.2.3" "18.5.2-feat.1"
## Back on $branchOne, Dev1 is still working on that bug fix
## Notice that the two branches share an incrementing BuildCount for the assembly ...
## The semver is just counting commits to the branch
New-Commit "18.5.2.4" "18.5.2-fix.4" -branch $branchOne
# Dev2 can use a commit message to tell gitversion this was a feature branch
New-Commit "18.6.0" "18.6.0-feat.2" -branch $branchTwo -Message "Want 18.6.0`nsemver:feature #ABC-1242"
# Note that repeating ourselves will make no difference:
New-Commit "18.6.0.1" "18.6.0-feat.3" -Message "Want 18.6.0.1`nsemver:feature #ABC-1242"
## Now imagine DEV3 starts a feature, but he knows what's what
## While this semver is 18.5.2-feat0002+Build.5
$branchThree = New-Feature "feature/ABC-1245/add"
# Because the branch name starts with "feature" it will increment the minor version
New-Commit "18.6.0.2" "18.6.0-add.1" -branch $branchThree
## When you open a PR, you get an -alpha tag and the PR id
## In a PR in a CI system, the last part might be zero, depending on whether it can see the git log
New-PullRequest "18.6.0.3" "18.6.0-alpha.1000.4" -branch $branchTwo
## But otherwise, the version number is still whatever it was
New-PullRequest "18.5.2.5" "18.5.2-alpha.1001.5" -branch $branchOne
## Whichever merges to main first, will release the version they've been using in pre-release
## When we merge a feature branch, we tag it in the commit message with `semver:feature #ABC-1242`
## Which results in incrementing the version number like we wanted, to: 18.6.0
New-Merge "18.6.0.4" "18.6.0" -branch $branchTwo -Feature $($branchTwo -replace ".*/(\d+)/.*", '$1') -Tag
## Back on $branchOne, Dev1 is still working on that bug fix
## But because main has been incremented **and tagged** the version number changes:
New-Commit "18.6.1.0" "18.6.1-fix.5" -branch $branchOne
return
## ################################################################################# #
## To ensure that existing branches pick up that new version, we need to tag main: #
## If we don't do this, it ONLY affects the branch builds #
## They will keep building, e.g. 18.5.2.6 instead of starting on 18.6.1 #
## But when they are merged to main, the merge will be correctly versioned #
## ################################################################################# #
## Note: the tag is "18.6.0" but if this triggers another build, the assembly is 18.6.0.1
New-Tag "18.6.0.1"
## main is now 18.6.0, and feature branches are tracking it
## So new builds on the old feature branch are now up to date:
New-Commit "18.6.1" -branch $branchOne
New-Commit "18.6.1.1"
## We can rebase onto main, but it has no effect on versioning
New-Rebase "18.6.1.2" -branch $branchOne
New-Commit "18.6.1.3" -branch $branchOne
## When we merge the bugfix to main, obviously, this should be ...
New-Merge "18.6.1.4" -branch $branchOne -Fix $($branchOne -replace ".*/(\d+)/.*", '$1')
## Make a hotfix
$NewVersion = "18.6.2"
$hotfixOne = New-Hotfix $NewVersion
New-Commit $NewVersion
New-Commit "18.6.2.1"
# Tag it to release it (and build without the -rc tag)
Write-Host "Tag release to get a final build" -Foreground Cyan
New-Tag "18.6.2.2"
# When we merge it back to main, it should increment again
# Because we _always_ increment when we commit to main
New-Merge "18.6.3" -branch $hotfixOne -Fix '1234127'
# Now we need another hotfix
$hotFix = "18.6.4"
$hotfixTwo = New-Hotfix $hotFix -From "refs/tags/18.6.2"
# While DEV2 needs to start another feature
$branchThree = New-Feature "Dev2/ABC-1266/three"
New-Commit "18.6.5" -branch $branchThree
# What happens when we commit to our hotfix?
New-Commit $hotFix -branch $hotfixTwo
# While DEV2 continues work on her feature
New-Commit "18.6.5.1" -branch $branchThree
# DEV2 finishes her feature while DEV1 is still working on the hotfix
# And that increments the release:
New-Merge "18.7.0" -branch $branchThree -Feature $($branchThree -replace ".*/(\d+)/.*", '$1')
# DEV1 finishes the hotfix, and tags it to remove the pre-release
Write-Host "Tag release to get a final build" -Foreground Cyan
New-Tag "18.6.4.1"
# Notice that we released 18.7.0 and then the 18.6.4 hotfix ...
# So when we merge, it's 18.7.1 ...
New-Merge "18.7.1" -branch $hotfixTwo -Fix '1234128'
## Major Breaking changes, or a new year
$branchFour = New-Feature "feature/ABC-1281/four"
New-Commit "18.7.2"
New-Merge "19.0.0" -branch $branchFour -Breaking $($branchFour -replace ".*/(\d+)/.*", '$1')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment