Saturday, March 7, 2015

Convention-Driven Automatic Release Versioning from Your Git Repo

Introduction

I have a project where I wanted to automatically generate some information about what version has been deployed so I can ensure that, post-deploy, the correct version has been deployed to production easily.  For my purposes, I chose to use a mechanism that depends on semantic versioning of the Git release branches to avoid having to maintain version numbers inside my project. That is, assuming that my Git release branches follow a naming standard that incorporates the version of the code, I wanted to use the branch name and some other information to identify the version in the deployed application.

I found an article, Unobtrusive MSBuild: Using Git information in your assemblies, that was slightly more complex than I wanted describing how to do this with MSBuild Community tasks. Using information from the article and simplifying it some I was able to develop a fairly straight-forward way of building an AssemblyInformationalVersionAttribute attribute with the Git branch name, commit hash, and the build time.

Using the "unobtrusive approach" described in the article, this is generated in a separate, unversioned file that is re-created on every build(Properties/AutoGeneratedAssemblyInfo.cs). When done for a release it will reflect the branch being used for the release deployment. The file is removed when the project is cleaned.  Note: it's important that this file be excluded from your Git repo as it changes on every build - no sense in checking in a file that is auto-generated anyway.

Process

  1. Update your .gitignore file to exclude files named "GeneratedAssemblyInfo.cs". Commit this change so that the automatically generated files we will be creating will not be added to the repository.
  2. Unload your project and open the project file in the editor.  Right-click on the project name in Visual Studio and select Unload Project. Right-click again on the (unloaded) project and select Edit <project-name>.
  3. Install MSBuildTasks NuGet package into the project. The original author added a separate “dummy” build project to the solution. I found that adding it to an existing project works equally well. This will add a .build folder to your solution with the MSBuild Community tasks and targets.

    Note: you may need to set the PowerShell Execution Policy for this to work. I found that I needed to set the 32-bit PS Execution Policy to RemoteSigned on my 64-bit system as well using:
    start-job { Set-ExecutionPolicy Unrestricted } -RunAs32 | wait-job | Receive-Job 
    

    This needs to be executed from a PowerShell console window running as administrator.
  4. Add a Project Group to your .csproj referencing the community tasks that you just added. This property group defines the path to the community tasks folder, relative to the solution directory, the path of the generated assembly info file, the assembly copyright condition and extends the build/clean item groups so that the targets that we will define later are called at the appropriate times.
    <PropertyGroup>
      <MSBuildCommunityTasksPath>$(SolutionDir)\.build</MSBuildCommunityTasksPath>
      <GeneratedAssemblyInfoFile Condition=" '$(GeneratedAssemblyInfoFile)' == '' ">$(MsBuildProjectDirectory)\Properties\GeneratedAssemblyInfo.cs</GeneratedAssemblyInfoFile>
      <AssemblyCopyright Condition="'$(AssemblyCopyright)' == ''">
      </AssemblyCopyright>
      <BuildDependsOn>
        SetAssemblyVersion
        $(BuildDependsOn)
      </BuildDependsOn>
      <CleanDependsOn>
        $(CleanDependsOn)
        SetAssemblyVersionClean
      </CleanDependsOn>
      <TargetFrameworkProfile />
    </PropertyGroup>
    
  5. Import the MSBuild Community task targets. Note original article describes also having to define a UsingTask for the Git branch command. I found that this already existed in the version of the community targets I was using, 1.4.0.88. Add the following directive with your other Import directives.
    <Import Project="$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets" />
    
  6. Add the SetAssemblyVersion target which actually builds the GeneratedAssemblyInfo.cs file with the AssemblyInformtionalVersion attribute applied to the assembly being built. Also add a BeforeBuild target that depends on the SetAssemblyVersion target you just added to make sure that it is invoked. Note: the build target below is simplified from the original article to include only those aspects of the Git information that are to be used and does not include any manually maintained version information or build computer information as that was not necessary for my requirements.
    <Target Name="BeforeBuild" DependsOnTargets="SetAssemblyVersion">
    </Target>
    <Target Name="SetAssemblyVersion">
    <PropertyGroup>
      <BuildTime>$([System.DateTime]::UtcNow.ToString("yyyy-MM-dd HH:mm:ss"))</BuildTime>
      <AssemblyCopyrightText Condition=" '$(AssemblyCopyright)' != '' ">$(AssemblyCopyright) $([System.DateTime]::UtcNow.Year)</AssemblyCopyrightText>
      <AssemblyCopyrightText Condition=" '$(AssemblyCopyrightText)' == '' ">
      </AssemblyCopyrightText>
    </PropertyGroup>
    <GitVersion LocalPath="$(SolutionDir)">
      <Output TaskParameter="CommitHash" PropertyName="CommitHash" />
    </GitVersion>
    <Message Importance="High" Text="Commit is $(CommitHash)" />
    <GitBranch LocalPath="$(SolutionDir)">
      <Output TaskParameter="Branch" PropertyName="GitBranch" />
    </GitBranch>
    <Message Importance="High" Text="Branch is $(GitBranch)" />
    <AssemblyInfo CodeLanguage="CS" OutputFile="$(GeneratedAssemblyInfoFile)" AssemblyInformationalVersion="$(GitBranch)-$(CommitHash), built $(BuildTime) UTC" AssemblyCopyright="$(AssemblyCopyrightText)" />
    </Target>
    
  7. Add the SetAssemblyVersionClean target to delete the generated assembly info file when the project is cleaned.
    <Target Name="SetAssemblyVersionClean" Condition="Exists($(GeneratedAssemblyInfoFile))">
      <Delete Files="$(GeneratedAssemblyInfoFile)" />
    </Target>
    
  8. Add a compile directive to the ItemGroup containing your other compile directives for the generated file.
    <Compile Include="$(GeneratedAssemblyInfoFile)" />
    
  9. Save your changes to the project file, close, and reload the project in your solution. Build the solution. Observe in the build output that the commit hash and branch information messages specify the current values.

Usage

In my project I have a MaintenanceController that is restricted to administrators. This controller outputs a maintenance page with information about the current version and other system information. The version is obtained from the AssemblyInformationalVersionAttribute we created.
using System.Linq;
using System.Reflection;
using System.Web.Mvc;
using System.Web.UI;

using MyProject.Web.ViewModels;

namespace MyProject.Web.Controllers
{
    [Authorize(Roles = Role.ADMIN)]
    public class MaintenanceController : Controller
    {
        private static readonly string _currentProductionTag;
        private const int ONE_DAY = 60 * 60 * 24;

        static MaintenanceController()
        {
            try
            {
                _currentProductionTag = Assembly.GetExecutingAssembly()
                                               .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false)
                                               .OfType<AssemblyInformationalVersionAttribute>()
                                               .First()
                                               .InformationalVersion;
            }
            catch
            {
                _currentProductionTag = "Unknown";
            }
        }

        [HttpGet]
        [OutputCache(Location = OutputCacheLocation.Server, Duration = ONE_DAY)]
        public ActionResult Index()
        {
            return View(new MaintenanceIndexViewModel()
            {
                CurrentProductionTag = _currentProductionTag,
                // other information
            });
        }

    }
}