Scott Hanselman

Adding a git commit hash and Azure DevOps Build Number and Build ID to an ASP.NET website

March 06, 2020 Comment on this post [15] Posted in ASP.NET | DotNetCore
Sponsored By

imageA few months back I moved my CI/CD (Continuous Integration/Continuous Development) to Azure DevOps for free. You get 1800 build minutes a month FREE and I'm not even close to using it with three occasionally-updated sites building on it. Earlier this week I wrote about making a cleaner and more intentional azure-pipelines.yml for an ASP.NET Core Web App

I was working/pairing with Damian today because I wanted to get my git commit hashes and build ids embedded into the actual website so I could see exactly what commit is in production.

That's live on hanselminutes.com righ tnow and looks like this

© Copyright 2020, Scott Hanselman. Design by @jzy, Powered by .NET Core 3.1.2 and deployed from commit 6b48de via build 20200310.7

There's a few things here and it's all in my ASP.NET Web App's main layout page called _layout.cshtml. You can look all about ASP.NET Core 101, .NET and C# over at https://dot.net/videos if you'd like. They've lovely videos.

My website footer has git commits

So let's take this footer apart, shall we?

<div class="copyright">&copy; Copyright @DateTime.Now.Year, 
<a href="https://www.hanselman.com">Scott Hanselman</a>.
Design by <a href="http://www.8164.org/">@@jzy</a>,
Powered by @System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription
and deployed from commit <a href="https://github.com/shanselman/hanselminutes-core/commit/@appInfo.GitHash">@appInfo.ShortGitHash</a>
via build <a href="https://dev.azure.com/hanselman/Hanselminutes%20Website/_build/results?buildId=@appInfo.BuildId&view=results">@appInfo.BuildNumber</a>
</div>

First, the obvious floating copyright year. Then a few credits that are hard coded.

Next, a call to @System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription which gives me this string ".NET Core 3.1.2" Note that there was a time for a while where that Property was somewhat goofy, but no longer.

I have two kinds of things I want to store along with my build artifact and output.

  • I want the the Git commit hash of the code that was deployed.
    • Then I want to link it back to my source control. Note that my site is a private repo so you'll get a 404
  • I want the Build Number and the Build ID
    • This way I can link back to my Azure DevOps site

Adding a Git Commit Hash to your .NET assembly

There's lots of Assembly-level attributes you can add to your .NET assembly. One lovely one is AssemblyInformationalVersion and if you pass in SourceRevisionId on the dotnet build command line, it shows up in there automatically. Here's an example:

[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+d6b3d432970c9acbc21ecd22c9f5578892385305")]
[assembly: AssemblyProduct("hanselminutes.core")]
[assembly: AssemblyTitle("hanselminutes.core")]
[assembly: AssemblyVersion("1.0.0.0")]

From this command line:

dotnet build --configuration Release /p:SourceRevisionId=d6b3d432970c9acbc21ecd22c9f5578892385305

But where does that hash come from? Well, Azure Dev Ops includes it in an environment variable so you can make a YAML task like this:

- task: DotNetCoreCLI@2
displayName: 'dotnet build $(buildConfiguration)'
inputs:
command: 'build'
arguments: '-r $(rid) --configuration $(buildConfiguration) /p:SourceRevisionId=$(Build.SourceVersion)'

Sweet. That will put in VERSION+HASH, so we'll pull that out of a utility class Damian made like this (full class will be shown later)

public string GitHash
{
get
{
if (string.IsNullOrEmpty(_gitHash))
{
var version = "1.0.0+LOCALBUILD"; // Dummy version for local dev
var appAssembly = typeof(AppVersionInfo).Assembly;
var infoVerAttr = (AssemblyInformationalVersionAttribute)appAssembly
.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute)).FirstOrDefault();

if (infoVerAttr != null && infoVerAttr.InformationalVersion.Length > 6)
{
// Hash is embedded in the version after a '+' symbol, e.g. 1.0.0+a34a913742f8845d3da5309b7b17242222d41a21
version = infoVerAttr.InformationalVersion;
}
_gitHash = version.Substring(version.IndexOf('+') + 1);

}

return _gitHash;
}
}

Displaying it is then trivial given the helper class we'll see in a minute. Note that hardcoded paths for my private repo. No need to make things complex.

deployed from commit <a href="https://github.com/shanselman/hanselminutes-core/commit/@appInfo.GitHash">@appInfo.ShortGitHash</a>

Getting and Displaying Azure DevOps Build Number and Build ID

This one is a little more complex. We could theoretically tunnel this info into an assembly as well but it's just as easy, if not easier to put it into a text file and make sure it's part of the ContentRootPath (meaning it's just in the root of the website's folder).

To be clear, an option: There are ways to put this info in an Attribute but not without messing around with your csproj using some not-well-documented stuff. I like a clean csproj so I like this. Ideally there'd be another thing like SourceRevisionID to carry this metadata.

You'd need to do something like this, and then pull it out with reflection. Meh.

<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="$(BuildNumber) != ''" >
<_Parameter1>BuildNumber</_Parameter1>
<_Parameter2>$(BuildNumber)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="$(BuildId) != ''" >
<_Parameter1>BuildId</_Parameter1>
<_Parameter2>$(BuildId)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>

Those $(BuildNumber) and $(BuildId) dealies are build variables. Again, this csproj messing around is not for me.

Instead, a simple text file, coming along for the ride.

- script: 'echo -e "$(Build.BuildNumber)\n$(Build.BuildId)" > .buildinfo.json'
displayName: "Emit build number"
workingDirectory: '$(Build.SourcesDirectory)/hanselminutes.core'
failOnStderr: true

I'm cheating a little as I gave it the .json extension, only because JSON files are copying and brought along as "Content." If it didn't have an extension I would need to copy it manually, again, with my csproj:

<ItemGroup>
<Content Include=".buildinfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

So, to be clear, two build variables inside a little text file. Then make a little helper class from Damian. Again, that file is in ContentRootPath and was zipped up and deployed with our web app.

public class AppVersionInfo
{
private static readonly string _buildFileName = ".buildinfo.json";
private string _buildFilePath;
private string _buildNumber;
private string _buildId;
private string _gitHash;
private string _gitShortHash;

public AppVersionInfo(IHostEnvironment hostEnvironment)
{
_buildFilePath = Path.Combine(hostEnvironment.ContentRootPath, _buildFileName);
}

public string BuildNumber
{
get
{
// Build number format should be yyyyMMdd.# (e.g. 20200308.1)
if (string.IsNullOrEmpty(_buildNumber))
{
if (File.Exists(_buildFilePath))
{
var fileContents = File.ReadLines(_buildFilePath).ToList();

// First line is build number, second is build id
if (fileContents.Count > 0)
{
_buildNumber = fileContents[0];
}
if (fileContents.Count > 1)
{
_buildId = fileContents[1];
}
}

if (string.IsNullOrEmpty(_buildNumber))
{
_buildNumber = DateTime.UtcNow.ToString("yyyyMMdd") + ".0";
}

if (string.IsNullOrEmpty(_buildId))
{
_buildId = "123456";
}
}

return _buildNumber;
}
}

public string BuildId
{
get
{
if (string.IsNullOrEmpty(_buildId))
{
var _ = BuildNumber;
}

return _buildId;
}
}

public string GitHash
{
get
{
if (string.IsNullOrEmpty(_gitHash))
{
var version = "1.0.0+LOCALBUILD"; // Dummy version for local dev
var appAssembly = typeof(AppVersionInfo).Assembly;
var infoVerAttr = (AssemblyInformationalVersionAttribute)appAssembly
.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute)).FirstOrDefault();

if (infoVerAttr != null && infoVerAttr.InformationalVersion.Length > 6)
{
// Hash is embedded in the version after a '+' symbol, e.g. 1.0.0+a34a913742f8845d3da5309b7b17242222d41a21
version = infoVerAttr.InformationalVersion;
}
_gitHash = version.Substring(version.IndexOf('+') + 1);

}

return _gitHash;
}
}

public string ShortGitHash
{
get
{
if (string.IsNullOrEmpty(_gitShortHash))
{
_gitShortHash = GitHash.Substring(GitHash.Length - 6, 6);
}
return _gitShortHash;
}
}
}

How do we access this class? Simple! It's a Singleton added in one line in Startup.cs's ConfigureServices():

services.AddSingleton<AppVersionInfo>();

Then injected in one line in our _layout.cshtml!

@inject AppVersionInfo appInfo

Then I can use it and it's easy. I could put an environment tag around it to make it only show up in staging:

<environment include="Staging">
<cache expires-after="@TimeSpan.FromDays(30)">
<div class="copyright">&copy; Copyright @DateTime.Now.Year, <a href="https://www.hanselman.com">Scott Hanselman</a>. Design by <a href="http://www.8164.org/">@@jzy</a>, Powered by @System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription and deployed from commit <a href="https://github.com/shanselman/hanselminutes-core/commit/@appInfo.GitHash">@appInfo.ShortGitHash</a> via build <a href="https://dev.azure.com/hanselman/Hanselminutes%20Website/_build/results?buildId=@appInfo.BuildId&view=results">@appInfo.BuildNumber</a> </div>
</cache>
</environment>

I could also wrap it all in a cache tag like this. Worst case for a few days/weeks at the start of a new year the Year is off.

<cache expires-after="@TimeSpan.FromDays(30)">

<cache>

Thoughts on this technique?


Sponsor: This week's sponsor is...me! This blog and my podcast has been a labor of love for over 18 years. Your sponsorship pays my hosting bills for both AND allows me to buy gadgets to review AND the occasional taco. Join me!

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
March 10, 2020 2:34
We are using Nuke to do all this. It will even autogenerate your YAML files for Azure DevOps for you. :-)
March 10, 2020 3:56
Adding build metadata into the build artifact is one of the cleverest things my team has done. It's gone on to be a routine piece of any new build scripts. It can be a nuisance to set up, but the first time you find yourself "what version are we running?" you'll be glad to you did it
March 10, 2020 3:59
I have a nuget package that will handle putting the git hash in the AssemblyInformationalVersion. https://github.com/markpflug/msbuildgithash No need to customize the command line. Should work regardless of where you build from, VS, cmdline, build server.
March 10, 2020 7:15
I normally just make it a tiny class with a constant or something? That way I avoid the penalty of carrying around and "parsing" the file.
March 10, 2020 7:58
Nice! I'd go a couple of steps further: write an actual JSON file so it can be parsed anywhere that knows JSON, and push the hash in there as well. Having the hash and real JSON file means you can use the .buildinfo.json file as THE build info in other styles of projects too - ship it with a SPA, expose the info in it from an API call, embed it as a resource in your winforms app, something something cordova etc. I'd also add the date and time of the build into the JSON as well. I know you can work it out by looking at DevOps release, but having it in the JSON file just feels right.
March 10, 2020 8:08
Not to be nitpicky, but... well, ok, to be nitpicky, this test may fail years from now when you release 1.10.11 and there is no git hash: infoVerAttr.InformationalVersion.Length > 6 Might as well save future-you 4 hours of debugging and change that to: infoVerAttr.InformationalVersion.IndexOf('+') > 0 I apologize that the unit tester in me shrieked at the pattern "if (some test says we're ok) { execute a different operation; }"
March 10, 2020 8:57
Recently added as .NET Foundation project I love it and use it in all my projects, where I can: Nerdbank.GitVersioning
March 10, 2020 12:51
Great tip. It's small, easy change to do, but saves a lot of time checking last build etc.
March 10, 2020 13:43
I like how c# 8.0 features make code more concise and readable(this one is subjective, of course):
public string GitHash
{
    get
    {
        if (string.IsNullOrEmpty(_gitHash))
        {
            var appAssembly = typeof(AppVersionInfo).Assembly;
            var infoVerAttr = (AssemblyInformationalVersionAttribute)appAssembly
                .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute)).FirstOrDefault();
            
            _gitHash = infoVerAttr?.InformationalVersion is {Length: var length} && length > 6 
                ? infoVerAttr.InformationalVersion.Substring(infoVerAttr.InformationalVersion.IndexOf('+') + 1) 
                : "LOCALBUILD" /* Dummy version for local dev */;
        }
        return _gitHash;
    }
}
March 10, 2020 19:19
I went the other direction for the build number and have a small block of code to update the build number to match what's being built. That avoids having to pass the build ID into the assembly or having to manually match up the build output with the build proper. It's all the same version. Output a message with a shell script like:
Write-Host "##vso[build.updatebuildnumber]1.0.0+abcd1234"
March 11, 2020 5:53
I simple powershell that modify some text file in the ASP.NET Core will be enough As a demo, please look at https://bookrentalapi20191216080922.azurewebsites.net/ It displays (today ;-) ) " Latest commit was made 8 hours ago by Bobby at March 11th 2020, 00:37:21 with the message: adding all the removed commits" How it was done: Running powershell in .NET Core builds - just use pwsh ( with dotnet local tools) See https://github.com/ignatandrei/SimpleBookRental/blob/master/azure-pipelines.yml - bash: | git log --format='%s' -1 git log --pretty=oneline | head -1 gitMessage=$(git log --format='%s' -1) echo "##vso[task.setvariable variable=commitMessage;isOutput=true]$gitMessage" displayName: Store commit message in variable - powershell: .\modifyinfo.ps1 displayName: modify info
March 11, 2020 10:35
FYI, the links to the commit and build result fail for me. If this is supposed to be the case, maybe make them non-links?
March 11, 2020 16:18
Thanks for right unique information about this topic Nice blog and the details about it really interesting.
March 14, 2020 2:33
In this lab, you will learn how to establish a local Git repository, which can easily be synchronized with a centralized Git repository in Azure DevOps. In addition, you will learn about Git branching and merging support. You will use Visual Studio Code, but the same processes apply for using any Git-compatible client with Azure DevOps.
March 16, 2020 22:03
Where's the docs about SourceRevisionId? It's really helpful stuff but nearly impossible to discover by yourself.

(will show your gravatar icon)
5-3=?
Comment (Some html is allowed: a@href@title, b, blockquote@cite, em, i, li, ol, pre, strike, strong, sub, super, u, ul) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

Live Comment Preview

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.