MetaNull.InvokeScript.psm1
# Module Constants # Set-Variable MYMODULE_CONSTANT -option Constant -value $true Function Invoke-VisualStudioOnlineString { <# .SYNOPSIS Process Visual Studio Online strings. .DESCRIPTION Process Visual Studio Online strings. .PARAMETER InputString The object to output to the VSO pipeline. .PARAMETER ScriptOutput The output of the step. #> [CmdletBinding(DefaultParameterSetName='Default')] [OutputType([string])] param( [Parameter(Mandatory, ValueFromPipeline)] [AllowEmptyString()] [string]$InputString, [Parameter(Mandatory = $false)] [ref]$ScriptOutput ) Process { # Initialize the State variable, if not already initialized if($null -eq $ScriptOutput.Value) { $ScriptOutput.Value = [pscustomobject]@{ Result = [pscustomobject]@{ Message = 'Not started' Result = 'Failed' } Variable = @() Secret = @() Path = @() Upload = @() Log = @() Error = @() Retried = 0 } } # Detect if the received object is a VSO command or VSO format string $VsoResult = $InputString | ConvertFrom-VisualStudioOnlineString if(-not ($VsoResult)) { # Input is just a string, no procesing required # Replace any secrets in the output $ScriptOutput.Value.Secret | Foreach-Object { $InputString = $InputString -replace [Regex]::Escape($_), '***' } # Output the message as is $InputString | Write-Output return } if($VsoResult.Format) { # Input is a VSO format string, no procesing required, but output the message according to the format # Replace any secrets in the output $ScriptOutput.Value.Secret | Foreach-Object { $VsoResult.Message = $VsoResult.Message -replace [Regex]::Escape($_), '***' } # Output the message according to the format switch ($VsoResult.Format) { 'group' { Write-Host "[+] $($VsoResult.Message)" -ForegroundColor Magenta return } 'endgroup' { Write-Host "[-] $($VsoResult.Message)" -ForegroundColor Magenta return } 'section' { Write-Host "$($VsoResult.Message)" -ForegroundColor Cyan return } 'warning' { Write-Host "WARNING: $($VsoResult.Message)" -ForegroundColor Yellow return } 'error' { Write-Host "ERROR: $($VsoResult.Message)" -ForegroundColor Red return } 'debug' { Write-Host "DEBUG: $($VsoResult.Message)" -ForegroundColor Gray return } 'command' { Write-Host "$($VsoResult.Message)" -ForegroundColor Blue return } default { # Unknown format/not implemented Write-Warning "Format [$($VsoResult.Format)] is not implemented" # Do not return! Output is processed further } } } if($VsoResult.Command) { # Input is a VSO command, process it switch($VsoResult.Command) { 'task.complete' { Write-Debug "Task complete: $($VsoResult.Properties.Result) - $($VsoResult.Message)" $ScriptOutput.Value.Result.Result = $VsoResult.Properties.Result $ScriptOutput.Value.Result.Message = $VsoResult.Message return } 'task.setvariable' { Write-Debug "Task set variable: $($VsoResult.Properties.Variable) = $($VsoResult.Properties.Value)" $ScriptOutput.Value.Variable += ,[pscustomobject]$VsoResult.Properties return } 'task.setsecret' { Write-Debug "Task set secret: $($VsoResult.Properties.Value)" $ScriptOutput.Value.Secret += ,$VsoResult.Properties.Value return } 'task.prependpath' { Write-Debug "Task prepend path: $($VsoResult.Properties.Value)" $ScriptOutput.Value.Path += ,$VsoResult.Properties.Value return } 'task.uploadfile' { Write-Debug "Task upload file: $($VsoResult.Properties.Value)" $ScriptOutput.Value.Upload += ,$VsoResult.Properties.Value return } 'task.logissue' { Write-Debug "Task log issue: $($VsoResult.Properties.Type) - $($VsoResult.Message)" $ScriptOutput.Value.Log += ,[pscustomobject]$VsoResult.Properties return } 'task.setprogress' { Write-Debug "Task set progress: $($VsoResult.Properties.Value) - $($VsoResult.Message)" $PercentString = "$($VsoResult.Properties.Percent)".PadLeft(3,' ') Write-Host "$($VsoResult.Message) - $PercentString %" -ForegroundColor Green return } default { # Not implemented Write-Debug "Command [$($VsoResult.Command)] is not implemented" # Do not return! Output is processed further } } } # Replace any secrets in the output $ScriptOutput.Value.Secret | Foreach-Object { $InputString = $InputString -replace [Regex]::Escape($_), '***' } # Unknown input, output as is Write-Warning "Error processing input: $InputString" $InputString | Write-Output } } Function ConvertFrom-VisualStudioOnlineString { [CmdletBinding(DefaultParameterSetName='Default')] [OutputType([hashtable])] param( [Parameter(Mandatory, ValueFromPipeline)] [AllowEmptyString()] [AllowNull()] [string] $String ) Begin { $VisualStudioOnlineExpressions = @( @{ Name = 'Command' Expression = '^##vso\[(?<command>[\S]+)(?<properties>[^\]]*)\](?<line>.*)$' } @{ Name = 'Format' Expression = '^##\[(?<format>group|endgroup|section|warning|error|debug|command)\](?<line>.*)$' } ) } Process { # Check if the line is null or empty if ([string]::IsNullOrEmpty($String)) { return } # Evaluate each regular expression against the received String foreach($Expression in $VisualStudioOnlineExpressions) { $RxExpression = [regex]::new($Expression.Expression) $RxResult = $RxExpression.Match($String) if ($RxResult.Success) { $Vso = @{ Type = $Expression.Name Expression = $Expression.Expression Matches = $RxResult } break } } if(-not $Vso.Type) { return } # Handle known commands, or return if($Vso.Type -eq 'Format') { return @{ Format = $Vso.Matches.Groups['format'].value Message = $Vso.Matches.Groups['line'].value } } if($Vso.Type -eq 'Command') { # "> Command: $String" | Write-Debug $Properties = @{} $Vso.Matches.Groups['properties'].Value.Trim() -split '\s*;\s*' | Where-Object { -not ([string]::IsNullOrEmpty($_)) } | ForEach-Object { $key, $value = $_.Trim() -split '\s*=\s*', 2 # "{$key = $value}" | Write-Debug $Properties += @{"$key" = $value } } switch ($Vso.Matches.Groups['command']) { 'task.complete' { # Requires properties to be in 'result' if ($Properties.Keys | Where-Object { $_ -notin @('result') }) { return } # Requires property 'result' if (-not ($Properties.ContainsKey('result'))) { return } # Requires property 'result' to be 'Succeeded', 'SucceededWithIssues', or 'Failed' if (-not ($Properties['result'] -in @('Succeeded', 'SucceededWithIssues', 'Failed'))) { return } # Return the command switch ($Properties['result']) { 'Succeeded' { $Result = 'Succeeded' } 'SucceededWithIssues' { $Result = 'SucceededWithIssues' } 'Failed' { $Result = 'Failed' } default { return } } return @{ Command = 'task.complete' Message = $Vso.Matches.Groups['line'].Value Properties = @{ Result = $Result } } } 'task.setvariable' { # Requires properties to be in 'variable', 'isSecret', 'isOutput', and 'isReadOnly' if ($Properties.Keys | Where-Object { $_ -notin @('variable', 'isSecret', 'isOutput', 'isReadOnly') }) { Write-Warning "Invalid properties" return } # Requires property 'variable' if (-not ($Properties.ContainsKey('variable'))) { Write-Warning "Missing name" return } # Requires property 'variable' to be not empty if ([string]::IsNullOrEmpty($Properties['variable'])) { Write-Warning "Null name" return } # Requires property 'variable' to be a valid variable name try { & { Invoke-Expression "`$$($Properties['variable']) = `$null" } } catch { Write-Warning "Invalid name" return } return @{ Command = 'task.setvariable' Message = $null Properties = @{ Name = $Properties['variable'] Value = $Vso.Matches.Groups['line'].Value IsSecret = $Properties.ContainsKey('isSecret') IsOutput = $Properties.ContainsKey('isOutput') IsReadOnly = $Properties.ContainsKey('isReadOnly') } } } 'task.setsecret' { # Requires no properties if ($Properties.Keys.Count -ne 0) { return } # Requires message if (([string]::IsNullOrEmpty($Vso.Matches.Groups['line'].Value))) { return } return @{ Command = 'task.setsecret' Message = $null Properties = @{ Value = $Vso.Matches.Groups['line'].Value } } } 'task.prependpath' { # Requires no properties if ($Properties.Keys.Count -ne 0) { return } # Requires message if (([string]::IsNullOrEmpty($Vso.Matches.Groups['line'].Value))) { return } return @{ Command = 'task.prependpath' Message = $null Properties = @{ Value = $Vso.Matches.Groups['line'].Value } } } 'task.uploadfile' { # Requires no properties if ($Properties.Keys.Count -ne 0) { return } # Requires message if (([string]::IsNullOrEmpty($Vso.Matches.Groups['line'].Value))) { return } return @{ Command = 'task.uploadfile' Message = $null Properties = @{ Value = $Vso.Matches.Groups['line'].Value } } return $Return } 'task.setprogress' { # Requires properties to be in 'value' if ($Properties.Keys | Where-Object { $_ -notin @('value') }) { return } # Requires property 'value' if (-not ($Properties.ContainsKey('value'))) { return } # Requires property 'value' to be an integer $percent = $null if (-not ([int]::TryParse($Properties['value'], [ref]$percent))) { return } # Requires property 'value' to be between 0 and 100 if ($percent -lt 0 -or $percent -gt 100) { return } return @{ Command = 'task.setprogress' Message = $Vso.Matches.Groups['line'].Value Properties = @{ Value = $percent } } } 'task.logissue' { # Requires properties to be in 'value' if ($Properties.Keys | Where-Object { $_ -notin @('type','sourcepath','linenumber','colnumber','code') }) { return } # Requires property 'type' if (-not ($Properties.ContainsKey('type'))) { return } # Requires property 'type' to be 'warning' or 'error' if (-not ($Properties['type'] -in @('warning', 'error'))) { return } else { switch($Properties['type']) { 'warning' { $percent = 'warning' } 'error' { $percent = 'error' } } } # Requires property 'linenumber' to an integer (if present) $tryparse = $null if ($Properties['linenumber'] -and -not ([int]::TryParse($Properties['linenumber'], [ref]$tryparse))) { return } elseif($Properties['linenumber']) { $LogLineNumber = $Properties['linenumber'] } else { $LogLineNumber = $null } # Requires property 'colnumber' to an integer (if present) $tryparse = $null if ($Properties['colnumber'] -and -not ([int]::TryParse($Properties['colnumber'], [ref]$tryparse))) { return } elseif($Properties['colnumber']) { $LogColNumber = $Properties['colnumber'] } else { $LogColNumber = $null } # Requires property 'code' to an integer (if present) $tryparse = $null if ($Properties['code'] -and -not ([int]::TryParse($Properties['code'], [ref]$tryparse))) { return } elseif($Properties['code']) { $LogCode = $Properties['code'] } else { $LogCode = $null } return @{ Command = 'task.logissue' Message = $Vso.Matches.Groups['line'].Value Properties = @{ Type = $Properties['type'] SourcePath = $Properties['sourcepath'] LineNumber = $LogLineNumber ColNumber = $LogColNumber Code = $LogCode } } } 'build.addbuildtag' { # Requires no properties if ($Properties.Keys.Count -ne 0) { return } # Requires message if (([string]::IsNullOrEmpty($Vso.Matches.Groups['line'].Value))) { return } return @{ Command = 'build.addbuildtag' Message = $null Properties = @{ Value = $Vso.Matches.Groups['line'].Value } } return $Return } } } return } } Function Invoke-Script { <# .SYNOPSIS Invoke a script, defined as an array of strings .DESCRIPTION Invoke a script, defined as an array of strings (Purpose: Run a step from a pipeline) .PARAMETER Commands The Commands to run in the step @("'Hello World' | Write-Output"), for example .PARAMETER ScriptInput The input to the script (optional arguments received from the pipeline definition) (default: @{}) @{args = @(); path = '.'}, for example .PARAMETER ScriptEnvironment The environment ScriptVariable to set for the step (default: @{}) They are added to the script's local environment @{WHOAMI = $env:USERNAME}, for example .PARAMETER ScriptVariable The ScriptVariable to set for the step (default: @{}) They are used to expand variables in the Commands. Format of the variable is $(VariableName) @{WHOAMI = 'Pascal Havelange'}, for example .PARAMETER DisplayName The display name of the step (default: 'MetaNull.Invoke-Script') .PARAMETER Enabled Is the step Enabled (default: $true) .PARAMETER Condition The Condition to run the step (default: '$true') .PARAMETER ContinueOnError Continue on error (default: $false) If set to true, in case of error, the result will indicate that the step has 'completed with issues' instead of 'failed' .PARAMETER TimeoutInSeconds The timeout in seconds after which commands will be aborted (range: 1 to 86400 (1 day); default: 300 (15 minutes)) .PARAMETER MaxRetryOnFailure The number of retries on step failure (default: 0) .PARAMETER ScriptOutput The output of the script will be stored in this variable and the function returns the commands' output .EXAMPLE $ScriptOutput = $null $ScriptOutput = Invoke-Script -commands '"Hello World"|Write-Output' .EXAMPLE $ScriptOutput = $null Invoke-Script -commands '"Hello World"|Write-Output' -ScriptOutput ([ref]$ScriptOutput) #> [CmdletBinding(DefaultParameterSetName='Default')] [OutputType([PSCustomObject], [bool])] param( [Parameter(Mandatory)] [string[]]$Commands, [Parameter(Mandatory = $false)] [string]$ScriptWorkingDirectory = '.', [Parameter(Mandatory=$false)] [hashtable]$ScriptInput = @{}, [Parameter(Mandatory=$false)] [hashtable]$ScriptEnvironment = @{}, [Parameter(Mandatory=$false)] [hashtable]$ScriptVariable = @{}, [Parameter(Mandatory=$false)] [string]$DisplayName = 'MetaNull.Invoke-Script', [Parameter(Mandatory=$false)] [switch]$Enabled, [Parameter(Mandatory=$false)] [string]$Condition = '$true', [Parameter(Mandatory=$false)] [switch]$ContinueOnError, [Parameter(Mandatory=$false)] [ValidateRange(1,86400)] [int]$TimeoutInSeconds = 300, [Parameter(Mandatory=$false)] [int]$MaxRetryOnFailure = 0, [Parameter(Mandatory)] [ref]$ScriptOutput ) Process { $BackupErrorActionPreference = $ErrorActionPreference try { $ErrorActionPreference = "Stop" # Create an empty Result object '' | Invoke-VisualStudioOnlineString -ScriptOutput $ScriptOutput # Check if the step should run (step is Enabled) if ($Enabled.IsPresent -and -not $Enabled) { Write-Debug "Script '$DisplayName' was skipped because it was disabled." Set-Result -ScriptOutput $ScriptOutput -Message 'Disabled' throw $true # Interrupts the flow, $true is interpreted as a success } # Add received input variables to the ScriptOutput foreach ($key in $ScriptVariable.Keys) { Add-Variable -ScriptOutput $ScriptOutput -Name $key -Value $ScriptVariable[$key] } # Add received input environment variables to the process' environment Add-Environment -ScriptOutput $ScriptOutput -Name 'METANULL_CURRENT_DIRECTORY' -Value ([System.Environment]::CurrentDirectory) Add-Environment -ScriptOutput $ScriptOutput -Name 'METANULL_CURRENT_LOCATION' -Value ((Get-Location).Path) Add-Environment -ScriptOutput $ScriptOutput -Name 'METANULL_SCRIPT_ROOT' -Value ($PSScriptRoot) Add-Environment -ScriptOutput $ScriptOutput -Name 'METANULL_WORKING_DIRECTORY' -Value ($ScriptWorkingDirectory | Expand-Variables -ScriptOutput $ScriptOutput) foreach ($key in $ScriptEnvironment.Keys) { Add-Environment -ScriptOutput $ScriptOutput -Name $key -Value $ScriptEnvironment[$key] } # Check if the step should run (Condition is true) $sb_condition = [scriptblock]::Create(($Condition | Expand-Variables -ScriptOutput $ScriptOutput)) if (-not (& $sb_condition)) { Write-Debug "Script '$DisplayName' was skipped because the Condition was false." Set-Result -ScriptOutput $ScriptOutput -Message 'Skipped' throw $true # Interrupts the flow, $true is interpreted as a success } # Create the scriptblocks # - Init: set-location $sb_init = [scriptblock]::Create( @( '$ErrorActionPreference = "Stop"' '$DebugPreference = "SilentlyContinue"' '$VerbosePreference = "SilentlyContinue"' 'Set-Location $env:METANULL_WORKING_DIRECTORY' ) -join "`n" ) # - Step: run the Commands $sb_step = [scriptblock]::Create(($Commands | Expand-Variables -ScriptOutput $ScriptOutput) -join "`n") # Set a timer, after which the job will be interrupted, if not yet complete $timer = [System.Diagnostics.Stopwatch]::StartNew() # Run the step, optionally retrying on failure do { Write-Debug "Running script $DisplayName as a Job" #"INIT: $($sb_init.ToString())" | Write-Debug #"STEP: $($sb_step.ToString())" | Write-Debug $job = Start-Job -ScriptBlock $sb_step -ArgumentList @($ScriptInput.args) -InitializationScript $sb_init try { # Wait for job to complete while ($job.State -eq 'Running') { Start-Sleep -Milliseconds 250 # Collect and process job's (partial) output try { $Partial = Receive-Job -Job $job -Wait:$false # $Partial |% {"Partial: $($_)" | Write-Debug} $Partial | Invoke-VisualStudioOnlineString -ScriptOutput $ScriptOutput | Write-Output } catch { Add-Error -ScriptOutput $ScriptOutput -ErrorRecord $_ } # Interrupt the job if it takes too long if($timer.Elapsed.TotalSeconds -gt $TimeoutInSeconds) { Set-Result -ScriptOutput $ScriptOutput -Failed -Message 'Job timed out.' Stop-Job -Job $job | Out-Null } } # Collect and process job's (remaining) output if($job.HasMoreData) { try { $Partial = Receive-Job -Job $job -Wait # $Partial |% {"Partial: $($_)" | Write-Debug} $Partial | Invoke-VisualStudioOnlineString -ScriptOutput $ScriptOutput | Write-Output } catch { Add-Error -ScriptOutput $ScriptOutput -ErrorRecord $_ } } # Process job result if($job.State -eq 'Completed') { # Interrupt the retry loop, the job si complete Set-Result -ScriptOutput $ScriptOutput -Message $job.State break } elseif($job.State -eq 'Failed' -and $MaxRetryOnFailure -gt 0) { # Keep on with the retry loop, as the job has failed, and we didn't reach yet the maximum number of allowed retries Write-Debug "Job failed, retrying up to $MaxRetryOnFailure time(s)" $ScriptOutput.Value.Retried ++ continue } else { # Interrupt the retry loop: Unexpected job.state and/or out of allowed retries => we shouldn't permit retrying if($ContinueOnError.IsPresent -and $ContinueOnError) { Set-Result -ScriptOutput $ScriptOutput -SucceededWithIssues -Message $job.State } else { Set-Result -ScriptOutput $ScriptOutput -Failed -Message $job.State } break } } finally { Write-Debug "Script '$DisplayName' ran for $($timer.Elapsed.TotalSeconds) seconds. Final job's state: $($job.State)" Remove-Job -Job $job -Force | Out-Null } } while(($MaxRetryOnFailure --) -gt 0) } catch { if($_.TargetObject -is [bool] -and $_.TargetObject -eq $true) { # This is a voluntary interruption of the flow... Do nothing } else { # This is an actual exception... Handle it Add-Error -ScriptOutput $ScriptOutput -ErrorRecord $_ if($ContinueOnError.IsPresent -and $ContinueOnError) { Set-Result -ScriptOutput $ScriptOutput -SucceededWithIssues -Message $_ } else { Set-Result -ScriptOutput $ScriptOutput -Failed -Message $_ } } } finally { $ErrorActionPreference = $BackupErrorActionPreference } } Begin { <# .SYNOPSIS Update the ScriptOutput object to indicate the Success or Failure of the operation #> Function Set-Result { [CmdletBinding(DefaultParameterSetName='Succeeded')] param( [Parameter(Mandatory)] [ref]$ScriptOutput, [Parameter(Mandatory = $false)] [string]$Message = 'Done', [Parameter(Mandatory,ParameterSetName='Failed')] [switch]$Failed, [Parameter(Mandatory,ParameterSetName='SucceededWithIssues')] [switch]$SucceededWithIssues ) Process { $Result = [pscustomobject]@{ Message = $Message Result = 'Succeeded' } if($Failed.IsPresent -and $Failed) { $Result.Result = 'Failed' } if($SucceededWithIssues.IsPresent -and $SucceededWithIssues) { $Result.Result = 'SucceededWithIssues' } $ScriptOutput.Value.Result = $Result } } <# .SYNOPSIS Check the Success of Failure status from the ScriptOutput object, return $true in case of Success, or $false otherwise. #> Function Test-Result { [CmdletBinding()] param( [Parameter(Mandatory)] [ref]$ScriptOutput ) Process { switch ($ScriptOutput.Value.Result.Result) { 'Succeeded' { return $true } 'SucceededWithIssues' { return $true } 'Failed' { return $false } } return $false } } <# .SYNOPSIS Add to the process' environment variables #> Function Add-Environment { [CmdletBinding()] param( [Parameter(Mandatory)] [ref]$ScriptOutput, [Parameter(Mandatory)] [string]$Name, [Parameter(Mandatory)] [string]$Value ) Process { Write-Debug "Adding to process' environment: $($Name.ToUpper() -replace '\W','_') = $($Value)" [System.Environment]::SetEnvironmentVariable(($Name.ToUpper() -replace '\W','_'), $Value, [System.EnvironmentVariableTarget]::Process) } } <# .SYNOPSIS Update the ScriptOutput object, adding some user defined variables (variables are later expanded when generating the Script's content) #> Function Add-Variable { [CmdletBinding()] param( [Parameter(Mandatory)] [ref]$ScriptOutput, [Parameter(Mandatory)] [string]$Name, [Parameter(Mandatory)] [string]$Value ) Process { Write-Debug "Adding to process' variables: $($Name) = $($Value)" $ScriptOutput.Value.Variable += ,[pscustomobject]@{ Name=$Name Value=[System.Environment]::ExpandEnvironmentVariables($Value) IsSecret=$false IsOutput=$false IsReadOnly=$false } } } <# .SYNOPSIS Update the ScriptOutput object, adding some ErrorRecord to the Error array #> Function Add-Error { [CmdletBinding()] param( [Parameter(Mandatory)] [ref]$ScriptOutput, [Parameter(Mandatory)] [object]$ErrorRecord ) Process { Write-Debug "Adding an error to the result: $($ErrorRecord.ToString())" $ScriptOutput.Value.Error += ,$ErrorRecord } } <# .SYNOPSIS Detect and expand user defined variables in a string #> Function Expand-Variables { [CmdletBinding()] param( [Parameter(Mandatory)] [ref]$ScriptOutput, [Parameter(Mandatory,ValueFromPipeline)] [string]$String ) Process { $ExpandedString = $_ # Expand the Variables found in the command $ScriptOutput.Value.Variable.GetEnumerator() | Foreach-Object { $ExpandedString = $ExpandedString -replace [Regex]::Escape("`$($($_.Name))"),"$($_.Value)" } # [System.Environment]::ExpandEnvironmentVariables($ExpandedString) | Write-Output $ExpandedString | Write-Output } } } } Function Test-VisualStudioOnlineString { <# .SYNOPSIS Test if a string is a valid VSO command .DESCRIPTION Test if a string is a valid VSO command .PARAMETER String The string to test .EXAMPLE # Test a string '##vso[task.complete result=Succeeded;]Task completed successfully' | Test-VisualStudioOnlineString #> [CmdletBinding(DefaultParameterSetName='Default')] [OutputType([bool])] param( [Parameter(Mandatory, ValueFromPipeline)] [AllowEmptyString()] [AllowNull()] [string] $String ) Process { return -not -not ($String | ConvertFrom-VisualStudioOnlineString) } } Function Write-VisualStudioOnlineString { <# .SYNOPSIS Write a string in the Visual Studio Online format .DESCRIPTION Write a string in the Visual Studio Online format .PARAMETER Message The message to write .PARAMETER Format The format of the message 'group', 'endgroup', 'section', 'warning', 'error', 'debug', 'command' .PARAMETER CompleteTask Complete the task 'Succeeded', 'SucceededWithIssues', 'Failed' .PARAMETER SetTaskVariable Set a task variable .PARAMETER SetTaskSecret Set a task secret .PARAMETER PrependTaskPath Prepend a path to the task .PARAMETER UploadTaskFile Upload a file to the task .PARAMETER SetTaskProgress Set the task progress .PARAMETER LogIssue Log an issue 'warning', 'error' .PARAMETER AddBuildTag Add a build tag .PARAMETER Name The name of the variable .PARAMETER IsSecret Is the variable a secret .PARAMETER IsReadOnly Is the variable read-only .PARAMETER IsOutput Is the variable an output .PARAMETER Value The value of the variable .PARAMETER Path The path to prepend or upload .PARAMETER Progress The progress value .PARAMETER Type The type of issue .PARAMETER SourcePath The source path of the issue .PARAMETER LineNumber The line number of the issue .PARAMETER ColNumber The column number of the issue .PARAMETER Code The code of the issue .PARAMETER Tag The tag to add to the build .EXAMPLE # Write a message 'Task completed successfully' | Write-VisualStudioOnlineString -Format 'section' .EXAMPLE # Complete a task Write-VisualStudioOnlineString -CompleteTask -Result 'Succeeded' -Message 'Task completed successfully' .EXAMPLE # Set a task variable Write-VisualStudioOnlineString -SetTaskVariable -Name 'VariableName' -Value 'VariableValue' .EXAMPLE # Set a task secret Write-VisualStudioOnlineString -SetTaskSecret -Value 'SecretValue' .EXAMPLE # Prepend a path to the task Write-VisualStudioOnlineString -PrependTaskPath -Path 'C:\Path\To\Prepend' .EXAMPLE # Upload a file to the task Write-VisualStudioOnlineString -UploadTaskFile -Path 'C:\Path\To\Upload' .EXAMPLE # Set the task progress Write-VisualStudioOnlineString -SetTaskProgress -Progress 50 -Message 'Task is 50% complete' .EXAMPLE # Log an issue Write-VisualStudioOnlineString -LogIssue -Type 'warning' -Message 'This is a warning' .EXAMPLE # Add a build tag Write-VisualStudioOnlineString -AddBuildTag -Tag 'tag' .OUTPUTS System.String .LINK https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=powershell #> [CmdletBinding(DefaultParameterSetName='Format')] [OutputType([string])] param( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='Format')] [Parameter(Mandatory = $false, ValueFromPipeline, ParameterSetName='Command-TaskComplete')] [Parameter(Mandatory = $false, ValueFromPipeline, ParameterSetName='Command-TaskSetProgress')] [Parameter(Mandatory = $false, ValueFromPipeline, ParameterSetName='Command-TaskLogIssue')] [AllowEmptyString()] [AllowNull()] [string] $Message, [Parameter(Mandatory = $false, ParameterSetName='Format')] [ValidateSet('group', 'endgroup', 'section', 'warning', 'error', 'debug', 'command')] [string] $Format, [Parameter(Mandatory, ParameterSetName='Command-TaskComplete')] [switch] $CompleteTask, [Parameter(Mandatory, ParameterSetName='Command-TaskSetVariable')] [switch] $SetTaskVariable, [Parameter(Mandatory, ParameterSetName='Command-TaskSetSecret')] [switch] $SetTaskSecret, [Parameter(Mandatory, ParameterSetName='Command-TaskPrependPath')] [switch] $PrependTaskPath, [Parameter(Mandatory, ParameterSetName='Command-TaskUploadFile')] [switch] $UploadTaskFile, [Parameter(Mandatory, ParameterSetName='Command-TaskSetProgress')] [switch] $SetTaskProgress, [Parameter(Mandatory, ParameterSetName='Command-TaskLogIssue')] [switch] $LogIssue, [Parameter(Mandatory, ParameterSetName='Command-BuildAddBuildTag')] [switch] $AddBuildTag, [Parameter(Mandatory, ParameterSetName='Command-TaskComplete')] [ValidateSet('Succeeded', 'SucceededWithIssues', 'Failed')] [string] $Result, [Parameter(Mandatory, ParameterSetName='Command-TaskSetVariable')] [string] $Name, [Parameter(Mandatory = $false, ParameterSetName='Command-TaskSetVariable')] [switch] $IsSecret, [Parameter(Mandatory = $false, ParameterSetName='Command-TaskSetVariable')] [switch] $IsReadOnly, [Parameter(Mandatory = $false, ParameterSetName='Command-TaskSetVariable')] [switch] $IsOutput, [Parameter(Mandatory = $false, ValueFromPipeline, ParameterSetName='Command-TaskSetVariable')] [Parameter(Mandatory = $false, ValueFromPipeline, ParameterSetName='Command-TaskSetSecret')] [AllowEmptyString()] [AllowNull()] [string] $Value, [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='Command-TaskPrependPath')] [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='Command-TaskUploadFile')] [string] $Path, [Parameter(Mandatory, ParameterSetName='Command-TaskSetProgress')] [ValidateRange(0, 100)] [int] $Progress, [Parameter(Mandatory, ParameterSetName='Command-TaskLogIssue')] [ValidateSet('warning', 'error')] [string] $Type, [Parameter(Mandatory = $false, ParameterSetName='Command-TaskLogIssue')] [string] $SourcePath, [Parameter(Mandatory = $false, ParameterSetName='Command-TaskLogIssue')] [int] $LineNumber, [Parameter(Mandatory = $false, ParameterSetName='Command-TaskLogIssue')] [int] $ColNumber, [Parameter(Mandatory = $false, ParameterSetName='Command-TaskLogIssue')] [int] $Code, [Parameter(Mandatory, ParameterSetName='Command-BuildAddBuildTag')] [ValidateScript({ $_ -match '^[a-z][\w\.\-]+$' })] [string] $Tag ) Process { switch($PSCmdlet.ParameterSetName) { 'Format' { return "##[$Format]$Message" } 'Command-TaskComplete' { return "##vso[task.complete result=$Result]$Message" } 'Command-TaskSetVariable' { $IsSecretString = "$("$($IsSecret.IsPresent -and $IsSecret)".ToLower())" $IsOutputString = "$("$($IsOutput.IsPresent -and $IsOutput)".ToLower())" $IsReadOnlyString = "$("$($IsReadOnly.IsPresent -and $IsReadOnly)".ToLower())" return "##vso[task.setvariable variable=$Name;isSecret=$IsSecretString;isOutput=$IsOutputString;isReadOnly=$IsReadOnlyString]$Value" } 'Command-TaskSetSecret' { return "##vso[task.setsecret]$Value" } 'Command-TaskPrependPath' { return "##vso[task.prependpath]$Path" } 'Command-TaskUploadFile' { return "##vso[task.uploadfile]$Path" } 'Command-TaskSetProgress' { return "##vso[task.setprogress value=$Progress;]$Message" } 'Command-TaskLogIssue' { $Properties = @() if ($SourcePath) { $Properties += "sourcepath=$SourcePath" } if ($LineNumber) { $Properties += "linenumber=$LineNumber" } if ($ColNumber) { $Properties += "colnumber=$ColNumber" } if ($Code) { $Properties += "code=$Code" } return "##vso[task.logissue type=$Type;$($Properties -join ';')]$Message" } 'Command-BuildAddBuildTag' { return "##vso[build.addbuildtag]$Tag" } } return } } |