Perhaps the most significant problem with the simplistic versioning system we put into place last time is that we can get the same version number generated on different branches. Fortunately, semantic versioning gives us room to fix this by adding a descriptive suffix to the build number.

There’s no free lunch however - while I want to use SemVer 2.0 for this project, not everything supports that style of versioning.

While the current NuGet clients and most of the third party NuGet servers support v2, NuGet.org itself is still restricted to v1 (for good reasons of backward compatibility), with a maximum length of just 20 characters.

To complicate matters still further, Windows itself has a somewhat different notion of versioning, one that’s considerably older. Version numbers for windows software are in four parts (major.minor.build.revision) except for when only three parts are relevant (such as when an MSI installation is running).

In short, we’re going to need to generate three different (but closely related) editions of our version.

Builds on our master branch are going to be our formal releases, and don’t need any specific semantic annotation, so we’ll stick with the approach created previously:

major . minor . patch

We can use this style of versioning pretty universally.

The develop branch is typically used to create beta builds used for testing. In some projects, these are the builds that get pushed into specific testing environments. Even though I’m not using develop on this tiny project, it’s still worth including.

// Windows Style
major . minor . patch

// Semver 1.0
major . minor . patch - "beta"

// Semver 2.0
major . minor . patch - "beta" . commit

The value used for commit will be the truncated git commit hash for the current head of the develop branch.

Versioning for other branches (such as those used for feature development or hot-fixes) is much less important because the chances of distributing one of those builds are quite low. But, we still want them to be distinctly versioned so we don’t get two builds confused. We’ll explicitly call them out as alpha releases so that people know they’re not considered production ready and include the actual branch name for reference.

// Windows Style
major . minor . patch

// Semver 1.0
major . minor . patch - "alpha"

// Semver 2.0
major . minor . patch - "alpha" . branch . commit

We’ll need to do a minor transformation to make sure our branch label complies with the semver standard. Since labels are restricted to alphanumeric plus dash, if our branch is feature/new-api, we’ll need to translate that into feature.new-api.

Implementation

Now that we’ve decided what we’re doing, here’s the implementation:

Task Generate.Version {

    $script:version = get-content $baseDir\version.txt -ErrorAction SilentlyContinue
    if ($version -eq $null) {
        throw "Unable to load .\version.txt"
    }
    Write-Host "Version          $script:version"

    $versionLastUpdated = git rev-list -1 HEAD $baseDir\version.txt
    $script:patchVersion = git rev-list "$versionLastUpdated..HEAD" --count
    Write-Host "Patch            $patchVersion"

    $branch = git name-rev --name-only HEAD
    Write-Host "Current Branch   $branch"

    $commit = git rev-parse --short head
    Write-Host "Current Commit   $commit"


    if ($branch -eq "master") {
        $script:semver10 = "$version.$patchVersion"
        $script:semver20 = "$version.$patchVersion"
    }
    elseif ($branch -eq "develop") {
        $script:semver10 = "$version.$patchVersion-beta"
        $script:semver20 = "$version.$patchVersion-beta.$commit"
    }
    else {
        $semverBranch = $branch -replace "[^A-Za-z0-9-]+", "."
        $script:semver10 = "$version.$patchVersion-alpha"
        $script:semver20 = "$version.$patchVersion-alpha.$semverBranch.$commit"
    }

    Write-Host "Semver 1.0: $semver10"
    Write-Host "Semver 2.0: $semver20"
}

We log our progress step by step through the task, just in case something goes wrong part way - we want to give as much context as possible for any error.

Much of the work is done for us by the git console app itself, making it much easier to write the PowerShell required.

Perhaps the most complex part is the use of a regular expression to replace any sequence of unwanted characters with a single period (.) when defining $semverBranch. If the branch name includes any folders (say, if it was feature/new-api) then each folder in the branch name becomes a separate fragment of the version.

For example, when testing this implementation, I got this output:

----------------------------------------------------------------------
Generate.Version
----------------------------------------------------------------------
Version          3.0
Patch            4
Current Branch   feature/new-api
Current Commit   11c02e0
Semver 1.0:      3.0.4-alpha
Semver 2.0:      3.0.4-alpha.feature.new-api.11c02e0
----------------------------------------------------------------------

Updating our Generate.VersionInfo task from last week is relatively simple; we use the $semver20 version string as the informational label for our assemblies.

Task Generate.VersionInfo -Depends Generate.Version {

    foreach($assemblyInfo in (get-childitem $srcDir\AssemblyInfo.cs -recurse)) {
        $versionInfo = Join-Path $assemblyInfo.Directory "VersionInfo.cs"
        set-content $versionInfo "// Generated file - do not modify",
            "using System.Reflection;",
            "[assembly: AssemblyVersion(`"$version`")]",
            "[assembly: AssemblyFileVersion(`"$version.$patchVersion`")]",
            "[assembly: AssemblyInformationalVersion(`"$semver20`")]"
        Write-Host "Generated $versionInfo"
    }
}

Now that we’ve got versioning out of the way, what’s next? Creating a NuGet package.

Prior post in this series:
Versioning
Next post in this series:
NuGet packaging

Comments

blog comments powered by Disqus