Private/Invoke-ExternalCommand.ps1

function Invoke-ExternalCommand(
    [Parameter(Mandatory)]
    [scriptblock]$Command,
    [int[]]$SuccessExitCodes = @(0),
    [string]$ErrorMessage,
    [switch]$Passthru
) {
    $stdoutAndErr = & $Command 2>&1

    # Split stdout and stderr -- https://stackoverflow.com/a/68106198/33244
    # The [string[]] cast converts the [ErrorRecord] instances to strings too.
    $stdout, [string[]]$stderr = $stdoutAndErr.Where({ $_ -is [string] }, 'Split')
    if ($SuccessExitCodes -notcontains $LASTEXITCODE) {
        # If the command failed, we throw an exception with the stderr output.

        if (-not $ErrorMessage) {
            $ErrorMessage = "Command exited with code $LASTEXITCODE."
        }

        $exceptionMessages = @()
        if ($stderr) {
            $exceptionMessages = @("$ErrorMessage. Output is:") + $stderr
        } else {
            $exceptionMessages = @($ErrorMessage)
        }

        throw $($exceptionMessages -join [Environment]::NewLine)
    }

    if ($Passthru) {
        # If the Passthru switch is specified, we return both stdout and stderr.
        $result = [PSCustomObject]@{
            StdOut   = $stdout
            StdErr   = $stderr
            ExitCode = $LASTEXITCODE
        }
        # Reset $LASTEXITCODE so callers (e.g. GitHub Actions run: steps) are not
        # affected by non-zero exit codes from tools that exit non-zero on success
        # (e.g. gstat -z exits 1 on Windows even when it succeeds).
        $global:LASTEXITCODE = 0
        return $result
    }

    # Reset $LASTEXITCODE for the same reason.
    $global:LASTEXITCODE = 0
}