Set-DotNetBuildAndVersionStrings.ps1

# Generates a version string in one of the following formats:
# a) 11.22.33-123456-abcdef0 - when using Azure DevOps pipelines
# b) 11.22.33-123456789012-abcdef0 - when doing any other kind of build process
# The middle part is the build ID in Azure DevOps (always incrementing value)
# In other environments there is often no equivalent, so it is just a timestamp (yymmddhhmmss)
# The middle part is useful for ordering builds by time.
# Returns the version string as the PowerShell output value

function Set-DotNetBuildAndVersionStrings {
    [CmdletBinding()]
    param(
        # This file must contain the AssemblyFileVersion (preferred) or AssemblyVersion attribute.
        [Parameter(Mandatory)]
        [string]$assemblyInfoPath,

        # Azure DevOps build ID. If not present, a timestamp will be generated instead.
        [Parameter()]
        [int]$buildId,

        # Git commit ID.
        [Parameter(Mandatory)]
        [string]$commitId,

        # Name of the primary branch. Builds in any other branch get the branch name as a version string prefix.
        [Parameter()]
        [string]$primaryBranchName = "master"
    )

    if (!(Test-Path $assemblyInfoPath)) {
        Write-Error "AssemblyInfo file not found at $assemblyInfoPath."
    }

    if ($commitId.Length -lt 7) {
        Write-Error "The Git commit ID is too short to be a valid commit ID."
    }

    # Convert to absolute paths because .NET does not understand PowerShell relative paths.
    $assemblyInfoPath = Resolve-Path $assemblyInfoPath

    # All versions built using this process must be release versions. There is no concept of a debug version.
    # Try to ensure this is so by looking for the BuildConfiguration environment variable that is present in automated builds.
    if ($env:BuildConfiguration -and $env:BuildConfiguration -ne "Release") {
        Write-Error "Only release-mode builds are compatible with build automation."
    }

    $assemblyInfo = [System.IO.File]::ReadAllText($assemblyInfoPath)

    # We prefer AssemblyFileVersion because for libraries in oldschool .NET Framework, there was some funny business
    # where you had to keep AssemblyVersion out of date for proper library upgrade functionality. Not relevant on Core.
    $primaryRegex = New-Object System.Text.RegularExpressions.Regex('AssemblyFileVersion(?:Attribute)?\("(.*)"\)')
    $fallbackRegex = New-Object System.Text.RegularExpressions.Regex('AssemblyVersion(?:Attribute)?\("(.*)"\)')

    $versionMatch = $primaryRegex.Matches($assemblyInfo)

    if (!$versionMatch.Success) {
        $versionMatch = $fallbackRegex.Matches($assemblyInfo)

        if (!$versionMatch.Success) {
            Write-Error "Unable to find AssemblyFileVersion or AssemblyVersion attribute."
        }
    }

    $version = $versionMatch.Groups[1].Value

    Write-Host "AssemblyInfo version is $version"

    # Shorten the commit ID. 7 characters seem to be the standard.
    $commitId = $commitId.Substring(0, 7)

    if ($buildId) {
        if ($buildId -gt 999999) {
            Write-Error "Build ID too large! Values over 999999 are not supported."
        }

        # Zero-pad the build ID to 6 digits.
        $temporalIdentifier = $buildId.ToString("000000")
    } else {
        $temporalIdentifier = [DateTimeOffset]::UtcNow.ToString("yyMMddHHmmss")
    }

    $version = "$version-$temporalIdentifier-$commitId"
    Write-Host "Version string is $version"

    # VSTS does not immediately update it, so update it manually to pass along to the next script.
    $env:BUILD_BUILDNUMBER = $version
    $version = Set-VersionStringBranchPrefix -primaryBranchName $primaryBranchName -skipBuildNumberUpdate

    Write-Output $version

    # Publish to Azure Pipelines.
    # NB! In Azure YAML pipelines, a followup pipeline (e.g. a release) does NOT pick up the updated build number!
    # Microsoft says this is by design.
    Write-Host "##vso[build.updatebuildnumber]$version"

    Write-Host "Version string set!"
}