Modules/businessdev.ALbuild.Pipeline/Private/Invoke-AdoTaskStep.ps1

function Invoke-AdoTaskStep {
    <#
    .SYNOPSIS
        Runs a single extension task (task.ps1) the way the Azure DevOps agent would, locally.
    .DESCRIPTION
        Internal helper for the local pipeline runner. Sets the task's INPUT_* environment variables
        (and any step-level env), runs task.ps1 in a child PowerShell process, echoes its output, and
        captures "##vso[task.setvariable ...]" commands back into the runtime variables so later steps
        can read them via $(var). The INPUT_* variables are removed again after the step so they do
        not leak into the next one.
    .PARAMETER TaskScript
        Path to the task.ps1 to run.
    .PARAMETER Inputs
        The task inputs (name -> already-expanded value).
    .PARAMETER StepEnv
        Step-level environment variables (name -> already-expanded value).
    .PARAMETER Variables
        The runtime variable map (case-insensitive); updated in place from set-variable commands.
    .PARAMETER PowerShellExe
        The PowerShell executable to launch the task with.
    .OUTPUTS
        PSCustomObject (Success, ExitCode).
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [string] $TaskScript,
        [hashtable] $Inputs = @{},
        [hashtable] $StepEnv = @{},
        [Parameter(Mandatory)] [hashtable] $Variables,
        [Parameter(Mandatory)] [string] $PowerShellExe
    )

    $applied = [System.Collections.Generic.List[string]]::new()
    try {
        foreach ($key in $Inputs.Keys) {
            $envName = "INPUT_$($key.ToUpperInvariant())"
            Set-Item -Path "Env:$envName" -Value "$($Inputs[$key])"
            $applied.Add($envName)
        }
        foreach ($key in $StepEnv.Keys) {
            Set-Item -Path "Env:$key" -Value "$($StepEnv[$key])"
            $applied.Add($key)
        }

        $result = Invoke-ALbuildProcess -FilePath $PowerShellExe `
            -Arguments @('-NoProfile', '-NonInteractive', '-File', $TaskScript) -PassThru -RetryCount 0 -StreamOutput
    }
    finally {
        foreach ($envName in $applied) { Remove-Item -Path "Env:$envName" -ErrorAction SilentlyContinue }
    }

    # Output was already echoed live by -StreamOutput; here we only harvest set-variable commands.
    if ($result.StdOut) {
        foreach ($line in ($result.StdOut -split "`r?`n")) {
            $command = ConvertFrom-VsoCommand -Line $line
            if ($command) { $Variables[$command.Name] = $command.Value }
        }
    }
    # Surface the failure reason readably: a failed task.ps1 prints PowerShell's full error-view
    # (source line, '~~~~', 'At line:', '+ CategoryInfo'/'+ FullyQualifiedErrorId') to stderr; reduce
    # it to the core message so the build log shows why it failed, not a raw error dump.
    if ($result.StdErr -and -not $result.Success) {
        $message = Format-BcErrorMessage -Text $result.StdErr
        if ($message) { Write-ALbuildLog -Level Error $message.Trim() }
    }

    return [PSCustomObject]@{ Success = $result.Success; ExitCode = $result.ExitCode }
}