Version numbering is an odd detail - one we can (and should) easily ignore at the start of a project. There comes a point, however, where we find that version numbering is vitally useful. At that point, we usually discover that we should have started versioning things about six months ago … and now we have a mess to clean up.

Why version?

Have you ever had a conversation with a colleague where you said something like this: “My exe has a creation timestamp of 1 November 2016 and it’s 149,266 bytes in size; how does yours compare?”

Failing to think about versioning early enough is a sure recipe for pain. Anything more complex than throwaway toy code evolves over time and knowing what version of the code is in use can be vitally important.

Here’s my guideline for when you need to start using version numbers on a project: as soon as you expect there to be multiple copies.

As long as the code exists only on your development machine, and as long as it is run only from within Visual Studio (or whatever development environment you use), you can blithely ignore versioning with impunity because there will only ever be one version - and no possibility for confusion. But when there are multiple copies - even if they are all on your own machine - you’ll sooner or later have to compare the two and decide which one is newer.

What style of versioning?

If you start researching version numbers, you’ll quickly find this is one of those areas of religious fervor (like tabs vs spaces or vi vs anything else) where otherwise easygoing developers suddenly become intractable.

For this project, I’ve chosen the following requirements:

  • Adhere to Semantic Versioning as closely as possible.
  • Little or no dependence on human intervention.
  • Building the same source should give the same build number.
  • Multiple builds from different source on the same day should have different build numbers.
  • Minimize the number of times we need to commit because the version changed.

Using semver is highly desirable given that I’m publishing a NuGet package for others to consume; the other requirements help to reduce ambiguity and the opportunity for error.

Confession: I once created a build system that used the passage of time to specify the build (or patch) number - I formatted it as MMDD where MM was the number of months since the project started, and DD was the day within the current month. This worked fine right up until the time I needed to deploy two builds into the test environment on the same day … both builds had the same build number and the new MSI wouldn’t deploy the new assemblies because the inbuilt version check said they were already there! I had to manually uninstall things from the test environment in order to get things deployed. Lesson learnt, I promptly made some changes.

Simple Versioning

As a starting point, let’s follow the basics of semver by defining our build number as:

major . minor . patch

Where major and minor will be read from version.txt at the root of our project, and patch will be generated from git by counting the number of commits since we last updated version.txt (this gives us a nicely incrementing integer with useful properties).

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"

    $script:semanticVersion = "$version.$patchVersion"
    Write-Host "Semantic version $semanticVersion"
}

Note the quotes around the revision range when setting $script:patchVersion - these are required to prevent PowerShell from trying to evaluate the .. before invoking git.

Of course, there are a bunch of other ways to define the build number. On one past project we used TeamCity for our continuous integration server and it defined our build numbers for us (with a fallback for local builds). I like the git based approach because it’s not dependent on any external factors - choose a system that works for you.

In the .NET world, there are three different kinds of version information we can embed in a compiled assembly:

  • AssemblyVersion specifies the version of the overall project
  • AssemblyFileVersion specifies the specific version number for a given assembly
  • AssemblyInformationalVersion specifies an informational version string for the assembly

To inject the version number into our C# assemblies, we generate a VersionInfo.cs file alongside each of the already existing AssemblyInfo.cs files that has all three of these attributes:

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(`"$semanticVersion`")]"
        Write-Host "Generated $versionInfo"
    }
}

After the first time these files are generated, they need to be added to each individual project. At the same time, any version attributes that already exist in AssemblyInfo.cs need to be removed. (In some projects, to avoid future confusion, I’ve left a comment in the AssemblyInfo.cs file telling any future developer to look for versioning in VersionInfo.cs.)

There tends to be some contention over whether the VersionInfo.cs files should be checked into source control. Some people, including myself, contend that future developers should be able to build a freshly cloned repo from within Visual Studio without needing to run anything on the command line. Others quite validly point out that generated files have no place in source control because they are, by their nature, ephemeral and easily replaced.

Our last step is to add the new dependencies into our existing task structure:

Task Formal.Build -Depends Release.Build, Generate.Version, 
    Compile.Assembly, Compile.NuGet, Unit.Tests { ... }

Task Compile.Assembly -Depends Requires.BuildType, Requires.MSBuild, 
    Requires.BuildDir, Generate.VersionInfo { ... }

Next time, we’ll expand from this base to full semantic versioning while also solving a few problems with this approach.

Comments

blog comments powered by Disqus