TeamCityLog.psm1

function EscapeTeamCityBuildText {
    <#
    .SYNOPSIS
    Escape text so special characters are interpreted properly in a TeamCity
    build log.

    .DESCRIPTION
    To have certain special characters properly interpreted by the TeamCity
    server, they must be preceded by a vertical bar (|).

    .EXAMPLE
    EscapeTeamCityBuildText "This's some text in need of [escaping]`n"

    This|'s some text in need of |[escaping|]|n

    .LINK
    https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+values

    .NOTES
        ╔═══════════╦═════════════════════════════════╦═══════════╗
        ║ Character ║ Description ║ Escape as ║
        ╠═══════════╬═════════════════════════════════╬═══════════╣
        ║ ' ║ Apostrophe ║ |' ║
        ╠═══════════╬═════════════════════════════════╬═══════════╣
        ║ \n ║ Line Feed ║ |n ║
        ╠═══════════╬═════════════════════════════════╬═══════════╣
        ║ \r ║ Carriage Return ║ |r ║
        ╠═══════════╬═════════════════════════════════╬═══════════╣
        ║ \uNNNN ║ Unicode Symbol with code 0xNNNN ║ |0xNNNN ║
        ╠═══════════╬═════════════════════════════════╬═══════════╣
        ║ | ║ Vertical bar ║ || ║
        ╠═══════════╬═════════════════════════════════╬═══════════╣
        ║ [ ║ Opening bracket ║ |[ ║
        ╠═══════════╬═════════════════════════════════╬═══════════╣
        ║ ] ║ Closing bracket ║ |] ║
        ╚═══════════╩═════════════════════════════════╩═══════════╝
    #>

    param (
        [Parameter(Position = 0, ValueFromPipeline)]
        [AllowEmptyString()]
        [string]
        # The text to be escaped
        $Text
    )
    PROCESS {
        if ([string]::IsNullOrEmpty($Text)) {
            return ''
        }
        $StringBuilder = [System.Text.StringBuilder]::new()
        foreach ($Character in $Text.GetEnumerator()) {
            switch ($Character) {
                '|' { 
                    $StringBuilder.Append('||') | Out-Null
                    continue
                }
                "'" { 
                    $StringBuilder.Append('|''') | Out-Null
                    continue
                }
                "`n" { 
                    $StringBuilder.Append('|n') | Out-Null
                    continue
                }
                "`r" { 
                    $StringBuilder.Append('|r') | Out-Null
                    continue
                }
                '[' { 
                    $StringBuilder.Append('|[') | Out-Null
                    continue
                }
                ']' { 
                    $StringBuilder.Append('|]') | Out-Null
                    continue
                }
                [char]0x0085 { 
                    $StringBuilder.Append('|x') | Out-Null
                    continue
                }
                [char]0x2028 { 
                    $StringBuilder.Append('|l') | Out-Null
                    continue
                }
                [char]0x2029 { 
                    $StringBuilder.Append('|p') | Out-Null
                    continue
                }
                Default {
                    if ($PSItem -gt 127) {
                        $StringBuilder.Append($('|0x{0:X4}' -f $([long]$PSItem))) | Out-Null
                    } else {
                        $StringBuilder.Append($PSItem) | Out-Null
                    }
                }
            }
        }
        return $StringBuilder.ToString()
    }
}

function IsRunningInTeamCity {
    <#
    .SYNOPSIS
    Returns whether the current script is executing in TeamCity

    .DESCRIPTION
    Uses the TEAMCITY_VERSION environment variable to determine if the current
    script is executing on a TeamCity build agent

    .EXAMPLE
    $env:TEAMCITY_VERSION = '9999.99.99'
    IsRunningInTeamCity
    $true
    # Returns true when the environment variable is set

    .EXAMPLE
    IsRunningInTeamCity
    $false
    # Returns false when the environment variable is not set

    .NOTES
    The TEAMCITY_VERSION environment variable is present automatically when a
    build is running on a TeamCity agent.
    #>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
    )
    return -not [string]::IsNullOrEmpty($env:TEAMCITY_VERSION)
}

function IsValidIdentifier {
    <#
    .SYNOPSIS
    Returns whether a string is a valid Java Identifier.

    .DESCRIPTION
    TeamCity Service Message identifiers are required to be valid Java
    identifiers. These are strings which only contain alphanumeric
    characters, '-', and must start with a letter.

    .EXAMPLE
    IsValidIdentifier 'Valid-Identifier001'
    True

    .LINK
    https://www.jetbrains.com/help/teamcity/service-messages.html#Service+messages+formats
    #>

    [CmdletBinding()]
    [OutputType([Boolean])]
    param (
        [Parameter(Position = 0, ValueFromPipeline)]
        [AllowEmptyString()]
        [string]
        # The identifier to check
        $Identifier
    )
    BEGIN {
        $RegexPatternJavaIdentifier = '^[A-Za-z0-9-]+$'
    }
    PROCESS {
        # An empty string is not a valid identifier
        if ([string]::IsNullOrEmpty($Identifier)) {
            return $false
        }

        # Identifiers must start with a letter
        if (-not [char]::IsLetter($Identifier[0])) {
            return $false
        }

        # Identifiers must only contain alphanumeric characters and '-'
        if ($Identifier -notmatch $RegexPatternJavaIdentifier) {
            return $false
        }
        return $true
    }
}
function Close-BuildMessageBlock {
    <#
    .SYNOPSIS
    Blocks are used to group several messages in the build log.

    .DESCRIPTION
    Creating a block allows you to fold the log lines between the block in TeamCity.
    Blocks can be nested.

    When you close a block, all its inner blocks are closed automatically.

    .EXAMPLE
    Close-TeamCityBuildMessageBlock "Querying ServiceNow"
    "##teamcity[blockClosed name='Querying ServiceNow']"

    .NOTES
    https://www.jetbrains.com/help/teamcity/service-messages.html#Blocks+of+Service+Messages
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]
        # The name of the block to close
        $Name
    )
    if (IsRunningInTeamCity) {
        Write-Host "##teamcity[blockClosed name='$(EscapeTeamCityBuildText $Name)']"
    }
}

function New-BuildMessageBlock {
    <#
    .SYNOPSIS
    Blocks are used to group several messages in the build log.

    .DESCRIPTION
    This function wraps the scriptblock in the names message block. Doesn't
    create a new scope.

    .EXAMPLE
    New-TeamCityBuildMessageBlock 'Querying ServiceNow' {
        ...
    }

    ##teamcity[blockOpened name='Querying ServiceNow']
    ...
    ##teamcity[blockClosed name='Querying ServiceNow']

    .NOTES
    https://www.jetbrains.com/help/teamcity/service-messages.html#Blocks+of+Service+Messages

    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]
        # The name of the block
        $Name,
        [Parameter(Mandatory, Position = 1)]
        [scriptblock]
        # The content of the script block (executed in the current scope)
        $ScriptBlock
    )
    if (IsRunningInTeamCity) {
        Write-Host "##teamcity[blockOpened name='$(EscapeTeamCityBuildText $Name)']"
    }
    Invoke-Command -NoNewScope -ScriptBlock $ScriptBlock
    if (IsRunningInTeamCity) {
        Write-Host "##teamcity[blockClosed name='$(EscapeTeamCityBuildText $Name)']"
    }
}

function Open-BuildMessageBlock {
    <#
    .SYNOPSIS
    Blocks are used to group several messages in the build log.

    .DESCRIPTION
    Creating a block allows you to fold the log lines between the block in
    TeamCity. Blocks can be nested.

    When you close a block, all its inner blocks are closed automatically.

    .EXAMPLE
    Open-TeamCityBuildMessageBlock "Querying ServiceNow"
    "##teamcity[blockOpened name='Querying ServiceNow']"

    .NOTES
    https://www.jetbrains.com/help/teamcity/service-messages.html#Blocks+of+Service+Messages
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]
        # The name of the block to open
        $Name
    )
    if (IsRunningInTeamCity) {
        Write-Host "##teamcity[blockOpened name='$(EscapeTeamCityBuildText $Name)']"
    }
}

function Publish-BuildArtifact {
    <#
    .SYNOPSIS
    Publishes a build artifact.

    .DESCRIPTION
    Build artifacts are files produced by the build which are stored on TeamCity
    server and can be downloaded from the TeamCity web UI or used as artifact
    dependencies by other builds. Calling this function allows you to publish
    build artifacts while the build is still running.

    .EXAMPLE
    Publish-TeamCityBuildArtifact -Path 'numbers.csv'
    # Uploads the relative file 'numbers.csv' as an artifact of this build.

    .EXAMPLE
    Publish-TeamCityBuildArtifact -Path 'output'
    # Uploads the contents of the 'Output' directory as artifact of this build.

    .NOTES
    If several publishArtifacts service messages are specified, only
    artifacts defined in the last message will be published. To configure
    publishing of multiple artifact files in one archive, use the Artifact
    paths field of the General Settings page.
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [ValidateScript({
            if (-not (Test-Path $_)){
                throw "The path specified ('$_') does not exist."
            }
            return $true
        })]
        [string]
        # The path to the artifact(s) to publish
        $Path
    )
    if (IsRunningInTeamCity) {
        Write-Host "##teamcity[publishArtifacts '$Path']"
    } else {
        Write-Host "Publish Artifact at '$Path'"
    }
}

function Set-BuildNumber {
    <#
    .SYNOPSIS
    Set a custom build number

    .DESCRIPTION

    .EXAMPLE
    Set-TeamCityBuildNumber -BuildNumber '1.2.3_{build.number}'

    .EXAMPLE
    Set-TeamCityBuildNumber '1.2.3_beta1'

    .NOTES
    In the <new build number> value, you can use the {build.number} substitution
    to use the current build number automatically generated by TeamCity.

    .LINK
    https://www.jetbrains.com/help/teamcity/service-messages.html#Reporting+Build+Number
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]
        # The build number
        $BuildNumber
    )
    if (IsRunningInTeamCity) {
        Write-Host "##teamcity[buildNumber '$(EscapeTeamCityBuildText $BuildNumber)']"
    } else {
        Write-Host "Set Build Number to '$BuildNumber'"
    }
}

function Set-BuildParameter {
    <#
    .SYNOPSIS
    Dynamically update build parameters of the running build

    .DESCRIPTION
    By using a dedicated service message in your build script, you can
    dynamically update build parameters of the build right from a build step
    (the parameters need to be defined in the Parameters section of the build
    configuration).

    .EXAMPLE
    Set-TeamCityBuildParameter -Name 'system.instance' -Value 'UAT'

    .EXAMPLE
    Set-TeamCityBuildParameter 'env.database' 'development'

    .NOTES
    The changed build parameters will be available in the build steps following
    the modifying one. They will also be available as build parameters and can
    be used in the dependent builds via %dep.*% parameter references.

    Since the setParameter mechanism does not publish anything to the server
    until the build is finished, it is not possible to get updated parameters
    during the build via the REST API.

    When specifying a build parameter's name, mind the prefix:

        - 'system' for system properties.

        - 'env' for environment variables.

        - No prefix for configuration parameters.

    .LINK
    https://www.jetbrains.com/help/teamcity/service-messages.html#set-parameter
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]
        # The build parameter name
        $Name,
        [Parameter(Mandatory, Position = 1)]
        [AllowEmptyString()]
        [string]
        # The build parameter value
        $Value
    )
    if (IsRunningInTeamCity) {
        Write-Host "##teamcity[setParameter name='$(EscapeTeamCityBuildText $Name)' value='$(EscapeTeamCityBuildText $Value)']"
    } else {
        Write-Host "Set Build Parameter '$Name' to '$Value'"
    }
}

function Stop-Build {
    <#
    .SYNOPSIS
    Stop the currently executing teamcity build

    .DESCRIPTION
    Use this if you need to cancel a build from a script, for example, if a
    build cannot proceed normally due to the environment, or a build should be
    canceled form a subproces

    .EXAMPLE
    Stop-TeamCityBuild -Comment 'Cancelling'

    .EXAMPLE
    Stop-TeamCityBuild -Comment 'Cancelling' -ReAddToQueue

    .NOTES
    If required, you can re-add the build to the queue after canceling it. By
    default, TeamCity will do 3 attempts to re-add the build into the queue.

    .LINK
    https://www.jetbrains.com/help/teamcity/service-messages.html#Canceling+Build+via+Service+Message
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]
        # The comment to cancel the build with
        $Comment,
        [Parameter()]
        [switch]
        # Re-Add the build to the build queue
        $ReAddToQueue
    )
    if (IsRunningInTeamCity) {
        $ReAdd = if ($ReAddToQueue.IsPresent) {
            " readdToQueue='true'"
        } else {
            ''
        }
        Write-Host "##teamcity[buildStop comment='$(EscapeTeamCityBuildText $Comment)'$ReAdd]"
    }
}

function Write-BuildMessage {
    <#
    .SYNOPSIS
    Reports a message for a TeamCity build log
    .DESCRIPTION

    .EXAMPLE
    Write-TeamCityBuildMessage "Message for the build log"

    ##teamcity[message text='Message for the build log' status='NORMAL']
    .NOTES
    https://www.jetbrains.com/help/teamcity/service-messages.html#Reporting+Messages+for+Build+Log

    'errorDetails' is used only if status is ERROR, in other cases it is ignored.
    This message fails the build in case its status is ERROR and the "Fail build
    if an error message is logged by build runner" box is checked on the Build
    Failure Conditions page of the build configuration.

    .LINK
    https://www.jetbrains.com/help/teamcity/service-messages.html#Service+Messages+Formats
    #>

    [CmdletBinding(DefaultParameterSetName = 'Normal')]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'Normal')]
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'Warning')]
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'Failure')]
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'Error')]
        [ValidateNotNullOrEmpty()]
        [string]
        # The text content of the build message
        $Text,
        [Parameter(ParameterSetName = 'Warning')]
        [switch]
        # Whether this message represents a warning
        $IsWarning,
        [Parameter(ParameterSetName = 'Failure')]
        [switch]
        # Whether this message represents a failure
        $IsFailure,
        [Parameter(ParameterSetName = 'Error', Mandatory)]
        [switch]
        # Whether this message represents an error
        $IsError,
        [Parameter(ParameterSetName = 'Error', Mandatory)]
        [string]
        # More detail on the error this message represents
        $ErrorDetails
    )
    $Status = if ($IsWarning.IsPresent) {
        'WARNING'
    } elseif ($IsFailure.IsPresent) {
        'FAILURE'
    } elseif ($IsError.IsPresent) {
        'ERROR'
    } else {
        'NORMAL'
    }
    $ErrorDetailText = if (-not [string]::IsNullOrEmpty($ErrorDetails)) {
        " errorDetails='$(EscapeTeamCityBuildText $ErrorDetails)'"
    } else {
        ''
    }
    if (IsRunningInTeamCity) {
        Write-Host "##teamcity[message text='$(EscapeTeamCityBuildText $Text)' status='$Status'$ErrorDetailText]"
    } else {
        if ($IsWarning.IsPresent) {
            Write-Warning "$Text"
        } elseif ($IsFailure.IsPresent -or $IsError.IsPresent) {
            Write-Error "$Text"
        } else {
            Write-Host "$Text"
        }
    }
}

function Write-BuildProblem {
    <#
    .SYNOPSIS
    Fails the currently running build with a problem using a service message.

    .DESCRIPTION
    To fail a build directly from the build script, a build problem must be
    reported. Build problems affect the build status text. They appear on the
    Build Results page.

    .EXAMPLE
    Write-TeamCityBuildProblem -Description "Unable to access system x"
    "##teamcity[buildProblem description='Unable to access system x']"

    .EXAMPLE
    Write-TeamCityBuildProblem "Unable to access system x"
    ##teamcity[buildProblem description='Unable to access system x']

    .EXAMPLE
    Write-TeamCityBuildProblem -Description "Unable to access system x" -Identity "PRB0000001"
    ##teamcity[buildProblem description='Unable to access system x' identity='PRB0000001']

    .NOTES
    https://data-flair.training/blogs/identifiers-in-java/
    Description is limited to 4,000 symbols (characters). Truncated after that.

    Identity - must be a valid Java ID up to 60 characters. If omitted, the identity
    is calculated based on the description text.

    .LINK
    https://www.jetbrains.com/help/teamcity/service-messages.html#Reporting+Build+Problems
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]
        # A human-readable plain text description of the build problem.
        $Description,
        [Parameter(Position = 1)]
        [AllowEmptyString()]
        [string]
        # A unique problem ID. Different problems must have different identities.
        $Identity
    )
    if ($Description.Length -gt 4000) {
        $Description = $Description.Substring(0, 4000)
    }

    if (-not [string]::IsNullOrWhiteSpace($Identity) -and
        $Identity.Length -gt 60) {
        $Identity = $Identity.Substring(0, 60)
    }

    if (IsRunningInTeamCity) {
        $Description = EscapeTeamCityBuildText $Description
        if (-not [string]::IsNullOrWhiteSpace($Identity)) {
            $Identity = EscapeTeamCityBuildText $Identity
            Write-Host "##teamcity[buildProblem description='$Description' identity='$Identity']"
        } else {
            Write-Host "##teamcity[buildProblem description='$Description']"
        }
    } else {
        Write-Error "$Description"
    }
}

function Write-BuildProgress {
    <#
    .SYNOPSIS
    Set the progress of the current build.

    .DESCRIPTION
    Use special progress messages to mark long-running parts in a build
    script. These messages will be shown on the projects dashboard for
    the corresponding build and on the Build Results page.

    .EXAMPLE
    Write-TeamCityBuildProgress -Message 'Starting Stage 2'
    "##teamcity[progressMessage 'Starting Stage 2']"

    .EXAMPLE
    Write-TeamCityBuildProgress 'Starting Stage 2'
    "##teamcity[progressMessage 'Starting Stage 2']"

    .EXAMPLE
    Write-TeamCityBuildProgress -Start 'Stage 2'
    "##teamcity[progressStart 'Stage 2']"

    .EXAMPLE
    Write-TeamCityBuildProgress -Finish 'Stage 2'
    "##teamcity[progressFinish 'Stage 2']"

    .LINK
    https://www.jetbrains.com/help/teamcity/service-messages.html#Reporting+Build+Progress
    #>

    [CmdletBinding(DefaultParameterSetName = 'message')]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, ParameterSetName = 'message', Position = 0)]
        [string]
        # The progress message to report.
        $Message,
        [Parameter(Mandatory, ParameterSetName = 'start')]
        [string]
        # Report the start of a progress block.
        $Start,
        [Parameter(Mandatory, ParameterSetName = 'finish')]
        [string]
        # Report the end of a progress block. Should have the same content as a
        # previous start message.
        $Finish
    )
    if (IsRunningInTeamCity) {
        if ($PSBoundParameters.ContainsKey('Message')) {
            Write-Host "##teamcity[progressMessage '$(EscapeTeamCityBuildText $Message)']"
        } elseif ($PSBoundParameters.ContainsKey('Start')) {
            Write-Host "##teamcity[progressStart '$(EscapeTeamCityBuildText $Start)']"
        } elseif ($PSBoundParameters.ContainsKey('Finish')) {
            Write-Host "##teamcity[progressFinish '$(EscapeTeamCityBuildText $Finish)']"
        }
    }
}

function Write-BuildStatistic {
    <#
    .SYNOPSIS
    Reports statistical data back to TeamCity about the build using a service
    message.

    .DESCRIPTION

    .EXAMPLE
    Write-TeamCityBuildStatistic -Key "MyCustomStatistic" -Value 13.5
    "##teamcity[buildStatisticValue key='MyCustomStatistic' value='13.5']"

    .EXAMPLE
    Write-TeamCityBuildStatistic -Key "MyCustomStatistic" -Value "13.5"
    "##teamcity[buildStatisticValue key='MyCustomStatistic' value='13.5']"

    .EXAMPLE
    Write-TeamCityBuildStatistic "MyCustomStatistic" 13.5
    "##teamcity[buildStatisticValue key='MyCustomStatistic' value='13.5']"

    .EXAMPLE
    Write-TeamCityBuildStatistic "MyCustomStatistic" "1234567890123"
    "##teamcity[buildStatisticValue key='MyCustomStatistic' value='1234567890123'"
    
    .NOTES
    Key must not be equal to any of predefined keys.

    Integer values are interpreted as [int64] as the default [int32]
    cannot be store a large enough value to store 13 digits (which
    is) the limit of the value that TeamCity can handle.

    .LINK
    https://www.jetbrains.com/help/teamcity/service-messages.html#Reporting+Build+Statistics
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]
        # BuildType-Unique identifier of the statistic being reported.
        $Key,
        [Parameter(Mandatory, Position = 1)]
        [ValidateNotNull()]
        # A numeric value for the statistic being reported.
        $Value
    )
    $PredefinedKeys = @(
        'ArtifactsSize'
        'VisibleArtifactsSize'
        'buildStageDuration:artifactsPublishing'
        'buildStageDuration:buildStepRunner_*'
        'buildStageDuration:sourcesUpdate'
        'buildStageDuration:dependenciesResolving'
        'BuildDuration'
        'BuildDurationNetTime'
        'CodeCoverageB'
        'CodeCoverageC'
        'CodeCoverageL'
        'CodeCoverageM'
        'CodeCoverageR'
        'CodeCoverageS'
        'CodeCoverageAbsBCovered'
        'CodeCoverageAbsBTotal'
        'CodeCoverageAbsCCovered'
        'CodeCoverageAbsCTotal'
        'CodeCoverageAbsLCovered'
        'CodeCoverageAbsLTotal'
        'CodeCoverageAbsMCovered'
        'CodeCoverageAbsMTotal'
        'CodeCoverageAbsRCovered'
        'CodeCoverageAbsRTotal'
        'CodeCoverageAbsSCovered'
        'CodeCoverageAbsSTotal'
        'DuplicatorStats'
        'TotalTestCount'
        'PassedTestCount'
        'FailedTestCount'
        'IgnoredTestCount'
        'InspectionStatsE'
        'InspectionStatsW'
        'SuccessRate'
        'TimeSpentInQueue'
    )
    foreach ($PredefinedKey in $PredefinedKeys) {
        if ($Key -like $PredefinedKey) {
            throw [System.ArgumentException]::new("Argument 'Key' ('$Key') cannot match a TeamCity Pre-defined key. See this link for more info: https://www.jetbrains.com/help/teamcity/custom-chart.html#Default+Statistics+Values+Provided+by+TeamCity")
        }
    }
    if ($Value -is [string]) {
        if ($Value.indexof('.') -gt -1) {
            # Try interpret as double
            if ($null -eq ($Value -as [double])) {
                throw [System.ArgumentException]::new("Argument 'Value' ($Value) was passed in as a string. It contains a period but cannot be interpretted as a [double].")
            }
            $Value = $Value -as [double]
        } else {
            # Try interpret as int64
            if ($null -eq ($Value -as [int64])) {
                throw [System.ArgumentException]::new("Argument 'Value' ($Value) was passed in as a string. It cannot be interpretted as a [int64].")
            }
            $Value = $Value -as [int64]
        }
    }
    if (($Value -is [int16]) -or ($Value -is [int32]) -or ($Value -is [int64])) {
        $NumDigits = "$Value".Length
        if ($NumDigits -gt 13) {
            throw [System.ArgumentException]::new("Argument 'Value' ('$Value') is an Integer that has more that 13 digits.")
        }
    }
    if (IsRunningInTeamCity) {
        Write-Host "##teamcity[buildStatisticValue key='$(EscapeTeamCityBuildText $Key)' value='$Value']"
    } else {
        Write-Host "Build Statistic: '$Key' -> '$Value'"
    }
}

function Write-BuildStatus {
    <#
    .SYNOPSIS
    Writes to the build output log, setting the status of the build using a
    service message.

    .DESCRIPTION
    Outputs a well formatted and correctly escaped TeamCity build status to
    stdout. By default, the {build.status.text} substitution pattern is
    prepended to the build status text. Use the -NoBuildStatusInText switch to
    prevent this.

    This should not be used to fail the build. Write-TeamCityBuildProblem
    should be used instead.

    .EXAMPLE
    Write-TeamCityBuildStatus 'Processed: 10'
    "##teamcity[buildStatus text='{build.status.text} - Processed: 10']"

    .EXAMPLE
    Write-TeamCityBuildStatus 'Processed: 10' -Success
    "##teamcity[buildStatus status='SUCCESS' text='{build.status.text} - Processed: 10']"

    .EXAMPLE
    Write-TeamCityBuildStatus 'Processed: 10' -NoBuildStatusInText
    "##teamcity[buildStatus text='Processed: 10']"

    .LINK
    https://www.jetbrains.com/help/teamcity/service-messages.html#Reporting+Build+Status
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]
        # Set the new build status text.
        $Text,
        [Parameter()]
        [switch]
        # Change the build status to Success.
        $Success,
        [Parameter()]
        [switch]
        # Removes the build status from the start of the build status text.
        $NoBuildStatusInText
    )
    if (IsRunningInTeamCity) {
        $Text = EscapeTeamCityBuildText $Text
        if (-not $NoBuildStatusInText.IsPresent) {
            $Text = "{build.status.text} - $Text"
        }
        if ($Success.IsPresent) {
            Write-Host "##teamcity[buildStatus status='SUCCESS' text='$Text']"
        } else {
            Write-Host "##teamcity[buildStatus text='$Text']"
        }
    } else {
        Write-Host "Build Status: '$Text'"
    }
}