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'" } } |