AdoCommandUtils.ps1
<#PSScriptInfo
.VERSION 1.0 .GUID 00b59a1d-a330-47ff-9a30-394080089364 .AUTHOR davidfreer@me.com .COMPANYNAME .COPYRIGHT .TAGS ado utils log command .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .DESCRIPTION Ado Logging Command Utils .RELEASENOTES Hello World .PRIVATEDATA #> Param() if ($PSVersionTable.PSVersion.Major -lt 7 -and $PSVersionTable.PSVersion.Minor -lt 4) { class ValidateNotNullOrWhiteSpaceAttribute : System.Management.Automation.ValidateEnumeratedArgumentsAttribute { [void] ValidateElement($element) { if (-not ($element -is [string]) -and -not ($element -is [string[]])) { throw [System.Management.Automation.ParameterBindingException]::New($element) } $element | % { if ([string]::IsNullOrWhiteSpace($element)) { throw [System.Management.Automation.ParameterBindingException]::New("Can not contain an empty or whitespace value") } } } } } class AdoCommandProperties { hidden [hashtable]$KVPS = [ordered]@{} AdoCommandProperties([hashtable]$Members) { $this.KVPS += $Members } [string] ToString() { $Flattened = $this.KVPS.GetEnumerator() | % { if ($null -eq $_.Value -or ($_.Value -is [string] -and [string]::IsNullOrWhiteSpace($_.Value))) { return } "{0}={1}" -f $_.Key.ToLower(), $_.Value } return ($Flattened ?? "") -join ";" } } function Add-AdoArtifactAssociation { <# .SYNOPSIS Wrapper for ##vso[artifact.associate]artifact location .DESCRIPTION Create a link to an existing Artifact. Artifact location must be a file container path, VC path or UNC share path. .EXAMPLE Add-AdoArtifactAssociation "\\MyShare\\MyDropLocation" -Name MyServerDrop -Type filepath echo "##vso[artifact.associate type=filepath;artifactname=MyServerDrop]\\MyShare\\MyDropLocation" .EXAMPLE Add-AdoArtifactAssociation "$/MyTeamProj/MyFolder" -Name MyTfvcPath -Type versioncontrol echo "##vso[artifact.associate type=versioncontrol;artifactname=MyTfvcPath]$/MyTeamProj/MyFolder" .EXAMPLE Add-AdoArtifactAssociation "#/1/build" -Name MyServerDrop -Type container echo "##vso[artifact.associate type=container;artifactname=MyServerDrop]#/1/build" .EXAMPLE adoassociation "refs/tags/MyGitTag" -Name MyTag -Type gitref echo "##vso[artifact.associate type=gitref;artifactname=MyTag]refs/tags/MyGitTag" .EXAMPLE adoassociation "MyTfvcLabel" -Name MyTag -Type gitref echo "##vso[artifact.associate type=gitref;artifactname=MyTag]MyTfvcLabel" .EXAMPLE adoassociation "MyTfvcLabel" -Name MyTag -Type tfvclabel echo "##vso[artifact.associate type=tfvclabel;artifactname=MyTag]MyTfvcLabel" .EXAMPLE adoassociation "https://downloads.visualstudio.com/foo/bar/package.zip" -Name myDrop -Type mfartifacttype echo "##vso[artifact.associate type=mfartifacttype;artifactname=myDrop]https://downloads.visualstudio.com/foo/bar/package.zip" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#associate-initialize-an-artifact #> [Alias("adoassociation")] param( # Artifact Location [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$Value, # Artifact Name [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true)] [string]$Name, # Artifact Type [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true)] [string]$Type ) process { $Props = [AdoCommandProperties]::New(@{ Type=$Type; ArtifactName=$Name; }) Write-Host "##vso[artifact.associate $Props]$Value" } } function Add-AdoBuildTag { <# .SYNOPSIS Wrapper for ##vso[build.addbuildtag]build tag .DESCRIPTION Add a tag for current build. You can expand the tag with a predefined or user-defined variable. For example, here a new tag gets added in a Bash task with the value last_scanned-$(currentDate). You can't use a colon with AddBuildTag .EXAMPLE Add-AdoBuildTag foobar echo "##vso[build.addbuildtag]foobar" .EXAMPLE Add-AdoBuildTag 'foobar/bazz/1.0.0' echo "##vso[build.addbuildtag]foobar/bazz/1.0.0" .EXAMPLE buildtag 'foobar:1.0.0' -ColonReplacement ' ' echo "##vso[build.addbuildtag]foobar 1.0.0" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#addbuildtag-add-a-tag-to-the-build #> [CmdletBinding()] [Alias('buildtag')] param( # Build tags to add [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string[]]$Value, # By default a colon (:) will produce an error in build tags. This parameter will replace any colons [ValidateNotNull()] [char]$ColonReplacement ) process { $Value | % { $Tag = $_ if ($ColonReplacement) { $Tag = $Tag -replace ':', $ColonReplacement } if ($Tag -imatch ':') { throw "Build tags can not contain colons" } Write-Host "##vso[build.addbuildtag]$Tag" } } } function Export-ToAdoPath { <# .SYNOPSIS Wrapper for ##vso[task.prependpath]local file path .DESCRIPTION Update the PATH environment variable by prepending to the PATH. The updated environment variable will be reflected in subsequent tasks. .EXAMPLE Export-ToAdoPath 'c:\my\directory\path' echo "##vso[task.prependpath]c:\my\directory\path" .EXAMPLE adopath 'c:\my\directory\path' echo "##vso[task.prependpath]c:\my\directory\path" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#prependpath-prepend-a-path-to-the--path-environment-variable #> [Alias("adopath")] param( # Paths to prepend [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string[]]$Value ) process { $Value | % { Write-Host "##vso[task.prependpath]$_" } } } function Publish-AdoArtifact { <# .SYNOPSIS Wrapper for ##vso[artifact.upload]local file path .DESCRIPTION Upload a local file into a file container folder, and optionally publish an artifact as artifactname. .EXAMPLE Publish-AdoArtifact 'c:\testresult.trx' -Name uploadedresult echo "##vso[artifact.upload artifactname=uploadedresult]c:\my\directory\path" .EXAMPLE adoartifact 'c:\testresult.trx' -Name uploadedresult -ContainerFolder TRX echo "##vso[artifact.upload artifactname=uploadedresult]c:\my\directory\path" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#upload-upload-an-artifact #> [CmdletBinding()] [Alias("adoartifact")] param( # Artifact local file path [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [Alias("Path")] [string]$LocalFilePath, # Artifact Name [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true)] [string]$Name, # Container Folder [ValidateNotNullOrWhiteSpace()] [string]$ContainerFolder ) process { $Props = [AdoCommandProperties]::New(@{ ArtifactName = $Name; ContainerFolder = $ContainerFolder; }) Write-Host "##vso[artifact.upload $Props]$LocalFilePath" } } function Publish-AdoFile { <# .SYNOPSIS Wrapper for ##vso[task.uploadfile]local file path .DESCRIPTION Upload user interested file as additional log information to the current timeline record. The file shall be available for download along with task logs. .EXAMPLE Publish-AdoFile 'c:\additionalfile.log' echo "##vso[task.uploadfile]c:\additionalfile.log" .EXAMPLE adofile 'c:\additionalfile.log' echo "##vso[task.uploadfile]c:\additionalfile.log" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#uploadfile-upload-a-file-that-can-be-downloaded-with-task-logs #> [Alias("adofile")] param( # Local file paths [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string[]]$Value ) process { $Value | % { Write-Host "##vso[task.uploadfile]$_" } } } function Publish-AdoSummary { <# .SYNOPSIS Wrapper for ##vso[task.uploadsummary]local file path .DESCRIPTION Upload and attach summary Markdown from a .md file in the repository to current timeline record. This summary shall be added to the build/release summary and not available for download with logs. The summary should be in UTF-8 or ASCII format. The summary will appear on the Extensions tab of your pipeline run. Markdown rendering on the Extensions tab is different from Azure DevOps wiki rendering. .EXAMPLE Publish-AdoSummary '$(System.DefaultWorkingDirectory)/testsummary.md' echo "##vso[task.uploadsummary]/vst/testsummary.md" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#uploadsummary-add-some-markdown-content-to-the-build-summary #> [CmdletBinding()] [Alias('adosummary')] param( # Summaries to publish [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage="File names where markdown file exists")] [string[]]$Value ) process { $Value | % { Write-Host "##vso[task.uploadsummary]$_" } } } function Set-AdoEndpoint { <# .SYNOPSIS Wrapper for ##vso[task.setendpoint]value .DESCRIPTION Set a service connection field with given value. Value updated will be retained in the endpoint for the subsequent tasks that execute within the same job. .EXAMPLE Set-AdoEndpoint testvalue -ServiceId 000-0000-0000 -Field authParameter -Key AccessToken echo "##vso[task.setendpoint id=000-0000-0000;field=authParameter;key=AccessToken]testvalue" .EXAMPLE adoendpoint testvalue -ServiceId 000-0000-0000 -Field dataParameter -Key userVariable echo "##vso[task.setendpoint id=000-0000-0000;field=dataParameter;key=userVariable]testvalue" .EXAMPLE adoendpoint 'https://example.com/service' -ServiceId 000-0000-0000 -Field url echo "##vso[task.setendpoint id=000-0000-0000;field=url]https://example.com/service" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#setendpoint-modify-a-service-connection-field #> [CmdletBinding()] [Alias("adoendpoint")] param( # Value of end point. Value updated will be retained in the endpoint for the subsequent tasks that execute within the same job [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$Value, # Service Connection Id [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true)] [Alias("id")] [string]$ServiceId, # Field Type, one of authParameter, dataParameter, url [ValidateSet('authParameter', 'dataParameter', 'url')] [Parameter(Mandatory = $true)] [string]$Field, # key, required unless field = url [ValidateNotNullOrWhiteSpace()] [string]$Key ) process { $Props = [AdoCommandProperties]::New(@{ Id = $Id; Field = $Field; Key = $Key; }) if ($Field -ne 'url' -and -not($Key)) { throw "Key parameter is required for $Field" } Write-Host "##vso[task.setendpoint $Props]$Value" } } function Set-AdoSecret { <# .SYNOPSIS Wrapper for ##vso[task.setsecret]value .DESCRIPTION The value is registered as a secret for the duration of the job. The value will be masked out from the logs from this point forward. This command is useful when a secret is transformed (e.g. base64 encoded) or derived. Note: Previous occurrences of the secret value will not be masked .EXAMPLE $Secret = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Env:OLDSECRET)) Set-AdoSecret $Secret echo "##vso[task.setsecret]$Secret" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#setsecret-register-a-value-as-a-secret #> [Alias("adosecret")] param( [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$Value ) Write-Host "##vso[task.setsecret]$Value" } function Set-AdoTaskCompletion { <# .SYNOPSIS Wrapper for ##vso[task.complete]current operation .DESCRIPTION Finish the timeline record for the current task, set task result and current operation. When result not provided, set result to succeeded. .EXAMPLE Set-AdoTaskCompletion DONE echo "##vso[task.complete result=Succeeded;]DONE" .EXAMPLE adocompletion -SucceededWithIssues echo "##vso[task.complete result=SucceededWithIssues;]" .EXAMPLE adocompletion -Failed echo "##vso[task.complete result=Failed;]" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=powershell#complete-finish-timeline #> [CmdletBinding()] [Alias("adocompletion")] param( [Parameter(ValueFromPipeline = $true)] [string]$Value = '', [switch]$SucceededWithIssues, [switch]$Failed ) process { $Result = 'Succeeded' if ($SucceededWithIssues.IsPresent) { $Result = 'SucceededWithIssues' } elseif ($Failed.IsPresent) { $Result = 'Failed' } Write-Host "##vso[task.complete result=$Result;]$Value" } } function Set-AdoVariable { <# .SYNOPSIS Convienence function for writing variables out to host in a pipe-functionality friendly manner .EXAMPLE Set-AdoVariable myVarName "Hello World" echo "##vso[task.setvariable variable=myVarName]Hello World" .EXAMPLE & { ... some other script logic ... } | adovar myVarName -Output echo "##vso[task.setvariable variable=myVarName;isOutput=true]Hello World" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=powershell#setvariable-initialize-or-modify-the-value-of-a-variable #> [CmdletBinding()] [Alias('adovar')] param( [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true)] [string]$Name, [ValidateNotNullOrWhiteSpace()] [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [string]$Value, [switch]$Secret, [switch]$Readonly, [switch]$Output ) process { $Properties = "variable=$Name" if ($Secret.IsPresent) { $Properties += ";isSecret=true" } if ($Readonly.IsPresent) { $Properties += ";isReadonly=true" } if ($Output.IsPresent) { $Properties += ";isOutput=true" } Write-Debug "Setting variable: '$Properties' to '$Value'" Write-Host "##vso[task.setvariable $Properties]$Value" } } function Trace-AdoProgress { <# .SYNOPSIS Convienence function for displaying progress for a given group of elements in a pipe-functionality friendly manner .EXAMPLE Trace-AdoProgress @('My first element', 'My second element', 'My third element') ` -AtCompletion { $_.Value + " Completed" } .EXAMPLE $Scriptblocks = @( { git fetch --prune }, { git pull }, { git merge develop }, { git status } ) Trace-AdoProgress $Scriptblocks -AtCompletion { $_.Value + " Completed" } .OUTPUTS PSObject .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=powershell#setprogress-show-percentage-completed #> [CmdletBinding()] [Alias("adoprogress")] param ( [ValidateNotNullOrWhiteSpace()] [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [psobject]$Value, # The statement to echo after iterated elements complete (returned value must be a string) # The $_ variable given to the scriptblock is [psobject]@{ Index = [int]; $Value = [string] # This scriptblock is intentionally invoked without context [ValidateNotNullOrEmpty()] [scriptblock]$AtCompletion, # The statement to echo before starting [string]$BeforeProgress, # The statement to echo after completion [string]$AfterProgress ) process { if (-not [string]::IsNullOrWhiteSpace($BeforeProgress)) { Write-Host $BeforeProgress } Write-Host "##vso[task.setprogress value=0;]" $ContainsAtCompletion = $PSBoundParameters.ContainsKey('AtCompletion') $i = 0 $Prev = $null; $Next = $null; for ($step = 100/$Value.Length; $step -le 100; $step+= 100/$Value.Length) { $Next = if (-not($i + 1 -lt $Value.Length)) { $null } else { $Value[$i + 1].ToString().Trim() } $Result = [psobject]@{ Next = $Next; Previous = $Prev; Value = $(if ($Value[$i] -is [scriptblock]) { & $Value[$i] } else { $Value[$i] }) 2>&1 Index = $i } $Prev = $Next $During = '{0}/{1} Completed' -f ($i + 1), $Value.Length if ($ContainsAtCompletion) { $AtCompletionScriptResult = $AtCompletion.InvokeWithContext($null, [psvariable]::new('_', [psobject]@{ Index = $i; Value = $Value[$i].ToString().Trim() })) if ($AtCompletionScriptResult.Count -eq 1 -and $AtCompletionScriptResult[0] -is [string]) { $During = $AtCompletionScriptResult[0] } elseif ($Value[$i] -is [scriptblock]) { $During += " | Executed: {0}" -f $Value[$i].ToString().Trim() } } $Result | Write-Output "##vso[task.setprogress value={0:n0};]{1}" -f $step, $During | Write-Host $i = $i + 1 } if (-not [string]::IsNullOrWhiteSpace($AfterProgress)) { Write-Host $AfterProgress } } } function Write-AdoIssue { <# .SYNOPSIS Wrapper for ##vso[task.logissue]error/warning message .DESCRIPTION Log an error or warning message in the timeline record of the current task. .EXAMPLE Write-AdoIssue "Foobar Error" echo "##vso[task.logissue type=error]Foobar Error" .EXAMPLE adoissue "Exception: StackOverflow" -SourcePath $SourceClassName -LineNumber 1 -ColumnNumber 5 echo "##vso[task.logissue type=error;sourcepath=MyClass.java;linenumber=1;columnnumber=5]Exception: StackOverflow" .EXAMPLE adoissue "This is a warning" -Type warning echo "##vso[task.logissue type=warning]This is a warning" .OUTPUTS NONE .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=powershell#logissue-log-an-error-or-warning #> [Alias('adoissue')] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [ValidateNotNullOrWhiteSpace()] [string]$Message, # defaults to warning # possible values are error, warning [ValidateSet('error', 'warning')] [string]$Type = 'warning', [string]$SourcePath, [Alias("Line")] [string]$LineNumber, [Alias("Col")] [string]$ColumnNumber, [string]$Code ) process { $Props = [AdoCommandProperties]::New(@{ Type = $Type; SourcePath = $SourcePath; LineNumber = $LineNumber; ColumnNumber = $ColumnNumber; Code = $Code; }) Write-Host "##vso[task.logissue $Props]$Message" } } function Write-AdoLog { <# .SYNOPSIS Writes to host (and Output) channels a generic azure devops log clause while also echoing the input of each passed argument .DESCRIPTION The Write-AdoLog function aims to make ado logging a 1 line statement. Using this function will echo to host as well as echoing the passed in arguments. The default logging command is debug All logging types support -PassThru per normal -PassThru conventions. Otherwise this is used as a convience function to print what commands/scriptblocks/value you're about to execute in a pipeline and also then execute said command/scriptblock/value .EXAMPLE Write-AdoLog "Hello World" echo "##[debug]Hello World" .EXAMPLE "Hello World Section" | adolog -Type section echo "##[section]Hello World Section" .EXAMPLE adolog { node --version } -Type command echo "##[command]node --version" v18.16.0 .EXAMPLE $Result = { node --version } | adolog -Type command -PassThru echo "##[command]node --version" v18.16.0 In this example, by specifying -PassThru the result of the command `node --version` is written to host but also to the output channel (Write-Output) implicitly. Therefore you can use the result of the command later in your script if you need to. Otherwise this is used as a convience function to print what command you're about to execute in a pipeline and also execute it $Result -replace 'v18', 'v19' #outputs v19.16.0 .EXAMPLE adolog { ls . } -Type group -Header "Current Working Directory" echo "##[group]Current Working Directory" folder1 folder2 file1.txt config.json echo "##[endgroup]" .INPUTS ANY .OUTPUTS ANY .LINK https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=powershell#formatting-commands #> [CmdletBinding()] [Alias('adolog')] param( # Value can be of any type. When value is of type scriptblock it is invoked using the call operator [Parameter(ValueFromPipeline=$true, Mandatory=$true)] [ValidateNotNullOrWhiteSpace()] $Value, # Any ado logging command. Defaults to debug. When Type is 'group' or 'section' pass a title to -Header parameter. See examples [ValidateNotNullOrEmpty()] [string]$Type = 'debug', # This parameter is only used for group & section logging command types [string]$Header = '', # Passes the resolved value of -Value to the powershell pipeline (Write-Output) [switch]$PassThru ) process { switch ($Type.ToLower()) { group { Write-Host "##[group]$Header" $Result = $( if ($Value -is [scriptblock]) { & $Value } else { $Value } ) 2>&1 if ($LASTEXITCODE -ne 0) { Write-Error $Result } Write-Host $Result Write-Host "##[endgroup]" } { @('command','section') -contains $_ } { $Description = if ([string]::IsNullOrWhiteSpace($Header)) { $ExecutionContext.InvokeCommand.ExpandString($MyInvocation.BoundParameters.Value).Trim() } else { $Header } Write-Host "##[$_]$Description" $Result = $( if ($Value -is [scriptblock]) { & $Value } else { $Value } ) 2>&1 if ($LASTEXITCODE -ne 0) { Write-Error $Result } Write-Host $Result } Default { Write-Host "##[$_]$Value" } } if ($PassThru.IsPresent) { $Result } } } |