Get-TimeBasedVersionString.ps1

$ErrorActionPreference = "Stop"

function Get-TimeBasedVersionString([string]$buildType) {
    # Expected Azure DevOps version string format: $(date:yyyy).$(date:Mdd).$(rev:r)
    # If using GitHub, call Get-VersionNumberFromCurrentTime before this to set the version number to this format.

    $expectedFormat = '$(date:yyyy).$(date:Mdd).$(rev:r)'
    # We expect the name of the pipeline (in Build.BuildNumber) to be the above Azure DevOps format string.
    # To this, we suffix the Git commit ID (-abcabcabc).
    # To this, we prefix the branch name if this is not the default branch (my-branch-123-).

    # Note about incrementing build numbers: The major and minor part are date-based, so strictly increasing.
    # The revision, however, is specific to the build pipeline! Different pipelines have different revision counters.
    # As such, you could theoretically create multiple builds from different pipelines with the same version string.
    # This is not ideal, especially for NuGet which will reject duplicates we attempt to publish.
    # The solution? Suffix the version string with the build type.
    # This suffix will also serve as the NuGet "preview version" marker string (1.2.3-foobar).
    # The same logic roughly follows for GitHub version numbers (timestamp based), as conflicts can occur there, as well.

    # So the final full version string will be [branch-name-]1111.2222.33-abcabca[-foobar]
    # Number of digits may vary in some components.

    # Try read commit ID for Azure DevOps.
    $commitId = $env:BUILD_SOURCEVERSION

    if (!$commitId) {
        # Maybe it is GitHub instead?
        $commitId = $env:GITHUB_SHA
    }

    if (!$commitId) {
        Write-Error "BUILD_SOURCEVERSION or GITHUB_SHA environment variable must be defined and contain a Git commit ID."
    }

    $versionNumber = $env:BUILD_BUILDNUMBER

    if (!$versionNumber) {
        Write-Error "BUILD_BUILDNUMBER environment variable must be defined."
    }

    ### Validate the version number.

    $buildNumberParser = '^(?<major>\d{4})\.(?<minor>\d{3,4})\.(?<revision>\d+)$'
    if (-not ($versionNumber -match $buildNumberParser)) {
        Write-Error "BUILD_BUILDNUMBER value '$versionNumber' does not match expected Azure DevOps format string $expectedFormat or the equivalent defined by Get-VersionNumberFromCurrentTime."
    }

    Write-Host "Version number is $versionNumber"

    ### Add Git commit ID.

    $DESIRED_COMMIT_ID_LENGTH = 7

    if ($commitId.Length -gt $DESIRED_COMMIT_ID_LENGTH) {
        $commitId = $commitId.Substring(0, $DESIRED_COMMIT_ID_LENGTH)
    }

    ### Add branch prefix.

    $PRIMARY_BRANCH_NAMES = @("master", "main", "dev")

    if ($env:SYSTEM_PULLREQUEST_SOURCEBRANCH) {
        # This is an Azure PR build.
        $sourceRef = $env:SYSTEM_PULLREQUEST_SOURCEBRANCH

        # branchPrefix will be parsed from sourceRef down below
    }
    elseif ($env:BUILD_SOURCEBRANCHNAME -and $PRIMARY_BRANCH_NAMES -notcontains $env:BUILD_SOURCEBRANCHNAME) {
        # This is an ADO build of a non-primary branch but is not a pull request.
        # Just tick the branch name in front (lowercase).
        $branchPrefix = ($env:BUILD_SOURCEBRANCHNAME).ToLower()
    }
    elseif ($env:GITHUB_REF) {
        # This is a GitHub build (either PR or regular)
        $sourceRef = $env:GITHUB_REF

        # If this is a PR, use the head (foreign) branch ref instead of the triggering ref.
        if ($env:GITHUB_HEAD_REF) {
            $sourceRef = $env:GITHUB_HEAD_REF
        }
        else {
            # GitHub workflows run this logic for all builds. Don't prefix if there's nothing special.
            foreach ($candidate in $PRIMARY_BRANCH_NAMES) {
                if ($sourceRef -eq "refs/heads/$candidate") {
                    $sourceRef = ""
                }
            }
        }

        # branchPrefix will be parsed from sourceRef down below (if sourceRef got set)
    }

    if ($sourceRef) {
        # We obtained the reference to the source branch but don't have the prefix figured out yet.
        # So we cut the reference string (refs/heads/abc123) after the last / and life is easy again.

        if ($sourceRef.Contains("/")) {
            $branchPrefix = $sourceRef.Substring($sourceRef.LastIndexOf("/") + 1)
        }
        else {
            # Sometimes it is just the branch name.
            $branchPrefix = $sourceRef
        }
    }

    if ($branchPrefix) {
        $branchPrefix = $branchPrefix + "-"
    }

    ### Make the final version string.

    if ($buildType) {
        $buildTypeSuffix = "-$buildType"
    }

    $versionString = "$branchPrefix$versionNumber-$commitId$buildTypeSuffix"
    Write-Host "Version string is $versionString"

    Write-Host "##vso[task.setvariable variable=VERSION_STRING;]$versionString"
    Write-Host "::set-output name=VERSION_STRING::$versionString"
    return $versionString
}