Write-ADOOutput.ps1

function Write-ADOOutput
{
    <#
    .Synopsis
        Writes ADO Output
    .Description
        Writes formal Output to a ADO step.
 
        This output can be referenced in subsequent steps within a job.
    .Example
        Write-ADOOutput @{
            key = 'value'
        }
    .Example
        Get-Random -Minimum 1 -Maximum 10 | Write-ADOOutput -Name RandomNumber
    .Link
        Write-ADOError
    .Link
        Write-ADODebug
    .Link
        Write-ADOWarning
    #>

    [OutputType([string])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "",
        Justification="Directly outputs in certain scenarios")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("Test-ForUnusableFunction", "",
        Justification="Directly outputs in certain scenarios")]
    param(
    # The InputObject. Values will be converted to a JSON array.
    [Parameter(Mandatory,ValueFromPipeline)]
    [PSObject]
    $InputObject,

    # The Name of the Output. By default, 'Output'.
    [string]
    $Name = 'Output',

    # The JSON serialization depth. By default, 10 levels.
    [int]
    $Depth = 10
    )

    begin {
        $inQ    = [Collections.Queue]::new()
    }
    process {
        #region Output Dictionaries
        if ($InputObject -is [Collections.IDictionary]) {
            $adoOut =
                foreach ($kv in $InputObject.GetEnumerator()) {
                    "##vso[task.setvariable variable=$($kv.Key);isOutput=true]$($kv.Value)"
                }

            if ($env:AGENT_ID -and $DebugPreference -eq 'SilentlyContinue') {
                Write-Host ($adoOut -join [Environment]::NewLine)
            } else {
                $adoOut
            }
        }
        #endregion Output Dictionaries
        #region Output Errors
        elseif ($InputObject -is [Management.Automation.ErrorRecord]) {
            $ADOErrorParams = @{
                Message = $InputObject.Exception.Message
            }
            $stackTraceLine =
                @($InputObject.ScriptStackTrace -split 'at <ScriptBlock>,' -ne '' -match ':*line')[0]
            if ($stackTraceLine) {
                $stackTraceLineParts = @($stackTraceLine -split ':')
                $ADOErrorParams.Line = $stackTraceLineParts[-1] -replace 'line' -replace '\s'

                $file = $stackTraceLineParts[0..($stackTraceLineParts.Count - 2)] -join ':'
                if ($file -notlike '*<*>*') {
                    $ADOErrorParams.File = $file
                }

            }
            Write-ADOError @ADOErrorParams
        }
        #endregion Output Errors
        #region Output Warnings
        elseif ($InputObject -is [Management.Automation.WarningRecord])
        {
            Write-ADOWarning -Message $InputObject.Message
        }
        #endregion Output Warnings
        #region Output Debug and Verbose
        elseif ($InputObject -is [Management.Automation.VerboseRecord] -or
            $InputObject -is [Management.Automation.DebugRecord])
        {
            Write-ADODebug -Message $InputObject.Message
        }
        #endregion Output Debug and Verbose
        #region Enqueue Remaining Input
        else
        {
            $inQ.Enqueue($InputObject)
        }
        #endregion Enqueue Remaining Input
    }

    end {
        $adoOut =
            if ($inQ.Count) {
                "##vso[task.setvariable variable=$name;isOutput=true]$($inQ.ToArray() | ConvertTo-Json -Compress -Depth $depth)"
                $inQ.Clear()
            }

        if ($adoOut) {
            if ($env:AGENT_ID -and $DebugPreference -eq 'SilentlyContinue') {
                Write-Host ($adoOut -join [Environment]::NewLine)
            } else {
                $adoOut
            }
        }
    }
}