private/workflow/Invoke-OSDCloudWorkflowTask.ps1

function Invoke-OSDCloudWorkflowTask {
    [CmdletBinding()]
    param (
        [switch]
        $Test
    )
    #=================================================
    $Error.Clear()
    Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Start"
    #=================================================
    # Re-apply the OSDCloud Property override layer to $global:OSDCloudDeploy so authoritative
    # overrides win over any changes made by the workflow UI, and so the snapshot below
    # inherits the final values.
    <#
    if ($global:OSDCloudEnv -and (Get-Command -Name 'Set-OSDCloudEnvOverride' -ErrorAction Ignore)) {
        Set-OSDCloudEnvOverride -Target $global:OSDCloudDeploy -ResolveOperatingSystem -AddMissingKeys
    }
    #>

    #=================================================
    $operatingSystemCloudObject = $global:OSDCloudDeploy.OperatingSystemCloudObject
    $workflowSettingsUser = $global:OSDCloudWorkflowSettingsUser
    if (-not $workflowSettingsUser -and (Get-Command -Name 'Initialize-OSDCloudWorkflowSettingsUser' -ErrorAction Ignore)) {
        $settingsArchitecture = if ($global:OSDCloudDeploy.OSArchitecture -ieq 'arm64') { 'ARM64' } else { 'AMD64' }
        Initialize-OSDCloudWorkflowSettingsUser -WorkflowName $global:OSDCloudDeploy.WorkflowName -Architecture $settingsArchitecture | Out-Null
        $workflowSettingsUser = $global:OSDCloudWorkflowSettingsUser
    }
    $global:OSDCloudWorkflowInvoke = $null
    $global:OSDCloudWorkflowInvoke = [ordered]@{
        DeploymentDisk            = $global:OSDCloudDeploy.DeploymentDisk
        DiskPartition             = [pscustomobject]@{
            DiskNumber = $global:OSDCloudDeploy.DeploymentDiskNumber
        }
        FileInfoWindowsImage       = $null
        DriverPackCacheObject      = $global:OSDCloudDeploy.DriverPackCacheObject
        DriverPackName             = $global:OSDCloudDeploy.DriverPackName
        DriverPackCloudObject      = $global:OSDCloudDeploy.DriverPackCloudObject
        DriverPackCloudTest        = $global:OSDCloudDeploy.DriverPackCloudTest
        Force                      = $global:OSDCloudDeploy.Force
        LaunchMethod               = $global:OSDCloudDeploy.LaunchMethod
        LogsPath                   = "$env:TEMP\osdcloud-logs"
        OperatingSystem            = $global:OSDCloudDeploy.OperatingSystem
        OperatingSystemCacheObject = $global:OSDCloudDeploy.OperatingSystemCacheObject
        OperatingSystemCloudObject = $operatingSystemCloudObject
        OperatingSystemCloudTest   = $global:OSDCloudDeploy.OperatingSystemCloudTest
        OSBuild                    = $global:OSDCloudDeploy.OSBuild
        OSEditionId                = $global:OSDCloudDeploy.OSEditionId
        RecoveryPartition          = $workflowSettingsUser.RecoveryPartition
        SkipFirmwareUpdate         = $global:OSDCloudDeploy.SkipFirmwareUpdate
        TimeEnd                    = $null
        TimeSpan                   = $null
        TimeStart                  = [datetime](Get-Date)
        ParamsExpandWindowsImage   = $null
        USBPartitions              = $null
        WindowsEdition             = $null
        WindowsImage               = $null
        WindowsImageIndex          = $null
        WindowsImagePath           = $null
        WinpeRestart               = [bool]$workflowSettingsUser.WinpeRestart
        WinpeShutdown              = [bool]$workflowSettingsUser.WinpeShutdown
    }
    #=================================================
    #region OSDCloud Deployment Analytics
    $eventName = 'Deploy-OSDCloud'
    function Send-EventDeployOSDCloud {
        param(
            [Parameter(Mandatory)]
            [string]$EventName,
            [Parameter(Mandatory)]
            [string]$ApiKey,
            [Parameter(Mandatory)]
            [string]$DistinctId,
            [Parameter()]
            [hashtable]$Properties
        )

        try {
            $payload = [ordered]@{
                api_key    = $ApiKey
                event      = $EventName
                properties = $Properties + @{
                    distinct_id = $DistinctId
                }
                timestamp  = (Get-Date).ToString('o')
            }

            $body = $payload | ConvertTo-Json -Depth 4 -Compress
            Invoke-RestMethod -Method Post `
                -Uri 'https://us.i.posthog.com/capture/' `
                -Body $body `
                -ContentType 'application/json' `
                -TimeoutSec 2 `
                -ErrorAction Stop | Out-Null

            Write-Verbose "[$(Get-Date -format s)] [OSDCloud] Event sent: $EventName"
        }
        catch {
            Write-Verbose "[$(Get-Date -format s)] [OSDCloud] Failed to send event: $($_.Exception.Message)"
        }
    }
    # UUID
    $deviceUUID = [System.String]$global:OSDCoreDevice.UUID
    # Convert the UUID to a hash value to protect user privacy and ensure a consistent identifier across events
    [string]$distinctId = $global:OSDCoreDevice.EndpointSHA
    if ([string]::IsNullOrWhiteSpace($distinctId) -and -not [string]::IsNullOrWhiteSpace($deviceUUID)) {
        $distinctId = [System.BitConverter]::ToString([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($deviceUUID))).Replace("-", "")
    }
    if ([string]::IsNullOrWhiteSpace($distinctId)) {
        $distinctId = [System.Guid]::NewGuid().ToString()
    }

    $computerInfo = Get-ComputerInfo -ErrorAction Ignore
    if ($env:SystemDrive -eq 'X:') {
        $deploymentPhase = 'WinPE'
        $osName = 'Microsoft WindowsPE'
    }
    else {
        $deploymentPhase = 'Windows'
        $osName = [string]$computerInfo.OsName
    }
    $eventProperties = @{
        OSDManufacturer                  = $OSDCoreDevice.OSDManufacturer
        OSDModel                         = $OSDCoreDevice.OSDModel
        OSDProduct                       = $OSDCoreDevice.OSDProduct
        deviceBaseBoardProduct           = $OSDCoreDevice.BaseBoardProduct
        deviceBiosReleaseDate            = $OSDCoreDevice.BiosReleaseDate
        deviceBiosVersion                = $OSDCoreDevice.BiosVersion
        deviceComputerManufacturer       = $OSDCoreDevice.ComputerManufacturer
        deviceComputerModel              = $OSDCoreDevice.ComputerModel
        deviceComputerSystemFamily       = $OSDCoreDevice.ComputerSystemFamily
        deviceComputerSystemProduct      = $OSDCoreDevice.ComputerSystemProduct
        deviceComputerSystemSKU          = $OSDCoreDevice.ComputerSystemSKU
        deviceComputerSystemType         = $OSDCoreDevice.ComputerSystemType
        deviceKeyboardLayout             = $OSDCoreDevice.KeyboardLayout
        deviceKeyboardName               = $OSDCoreDevice.KeyboardName
        deviceOSArchitecture             = $OSDCoreDevice.OSArchitecture
        deviceOSVersion                  = $OSDCoreDevice.OSVersion
        deviceProcessorArchitecture      = $OSDCoreDevice.ProcessorArchitecture
        deviceSystemFirmwareHardwareId   = $OSDCoreDevice.SystemFirmwareHardwareId
        deviceTimeZone                   = $OSDCoreDevice.TimeZone
        deviceTotalPhysicalMemoryGB      = $OSDCoreDevice.TotalPhysicalMemoryGB
        winArchitecture                  = [string]$env:PROCESSOR_ARCHITECTURE
        winBuildLabEx                    = [string]$computerInfo.WindowsBuildLabEx
        winBuildNumber                   = [string]$computerInfo.OsBuildNumber
        winCountryCode                   = [string]$computerInfo.OsCountryCode
        winEditionId                     = [string]$computerInfo.WindowsEditionId
        winInstallationType              = [string]$computerInfo.WindowsInstallationType
        winLanguage                      = [string]$computerInfo.OsLanguage
        winName                          = [string]$osName
        winTimeZone                      = [string]$computerInfo.TimeZone
        winVersion                       = [string]$computerInfo.OsVersion
        osdcloudModuleVersion      = [string]$($MyInvocation.MyCommand.Module.Version)
        osdcloudWorkflowName       = [string]$global:OSDCloudDeploy.WorkflowName
        osdcloudWorkflowTaskName   = [string]$global:OSDCloudDeploy.WorkflowTaskName
        osdcloudDriverPackName     = [string]$global:OSDCloudDeploy.DriverPackName
        osdcloudOSName             = [string]$operatingSystemCloudObject.OSName
        osdcloudOSVersion          = [string]$operatingSystemCloudObject.OSVersion
        osdcloudOSActivationStatus = [string]$operatingSystemCloudObject.OSActivation
        osdcloudOSBuild            = [string]$operatingSystemCloudObject.OSBuild
        osdcloudOSBuildVersion     = [string]$operatingSystemCloudObject.OSBuildVersion
        osdcloudOSLanguageCode     = [string]$operatingSystemCloudObject.OSLanguageCode
        deploymentPhase            = [string]$deploymentPhase
        idOSDeployDevice           = [string]$global:OSDCoreDevice.idOSDeployDevice # OSDeploy Device Hash
        idOSDeployBoot            = [string]$global:OSDCoreDevice.idOSDeployBoot # OSDeploy Boot GUID
        # idRegisteredEmail = [string]$global:OSDCoreDevice.idRegisteredEmail
        idRegisteredLicense        = [string]$global:OSDCoreDevice.idRegisteredLicense
    }
    $postApi = 'phc_2h7nQJCo41Hc5C64B2SkcEBZOvJ6mHr5xAHZyjPl3ZK'
    Send-EventDeployOSDCloud -EventName $eventName -ApiKey $postApi -DistinctId $distinctId -Properties $eventProperties
    #endregion
    #=================================================
    # Start Workflow Task Execution
    if ($null -ne $global:OSDCloudDeploy.WorkflowTaskObject) {
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO]"

        foreach ($step in $global:OSDCloudDeploy.WorkflowTaskObject.steps) {
            # Set the current step in the global variable
            $global:OSDCloudCurrentStep = $step
            #=================================================
            # Should we skip this step? (support both 'skip' and legacy 'disable')
            if (($step.skip -eq $true) -or ($step.disable -eq $true)) {
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] [Skip:True] $($step.name)"
                continue
            }
            #=================================================
            # Can we test this step in full Windows OS (not WinPE)?
            if (($env:SystemDrive -ne 'X:') -and ($step.testinfullos -ne $true)) {
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] [Skip:FullOS] $($step.name)"
                continue
            }
            #=================================================
            # Can we pause before this step?
            if ($step.pause -eq $true) {
                Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] [Pause:True] $($step.name)"
                Pause
            }
            #=================================================
            # Command or ScriptBlock
            $command = $null
            $commandline = $null
            if ($step.command) {
                $command = $step.command

                if (($command -is [string]) -and ($command.Contains(" "))) {
                    $commandline = $command
                }
                elseif (-not (Get-Command $command -ErrorAction Ignore)) {
                    Write-Host -ForegroundColor DarkRed "[$(Get-Date -format s)] [Step command does not exist] $($step.command)"
                    continue
                }
            }
            elseif ($step.scriptblock) {
                $command = [scriptblock]::Create($step.scriptblock)
            }
            else {
                Write-Host -ForegroundColor DarkRed "[$(Get-Date -format s)] [Step does not contain a command] $($step.name)"
                continue
            }
            #=================================================
            # Arguments
            $arguments = @()
            if ($step.args) {
                # Only process if args is an array of strings, not an empty object
                if ($step.args -is [array]) {
                    [array]$arguments = @($step.args)
                    $arguments = $arguments | Where-Object { $_ -is [string] } | ForEach-Object { $_.Trim() } # Trim whitespace from arguments
                    $arguments = $arguments | Where-Object { $_ -ne "" } # Remove empty arguments
                }
            }
            #=================================================
            # Parameters
            $parameters = $null
            if ($step.parameters) {
                $parameters = $null
                $parameters = [ordered]@{}
                ($step.parameters).psobject.properties | ForEach-Object { $parameters[$_.Name] = $_.Value }

                if ($parameters.Count -eq 0) {
                    $parameters = $null
                }
            }

            # Execute
            if ($step.scriptblock) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name) [ScriptBlock:$($step.scriptblock)]"
                if ($Test) { continue }
                & $command
            }
            elseif ($commandline) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name)"
                if ($Test) { continue }
                # Parse the command line into a command and arguments to avoid Invoke-Expression
                $parseErrors = $null
                $tokens = [System.Management.Automation.PSParser]::Tokenize($commandline, [ref]$parseErrors)

                if ($parseErrors -and $parseErrors.Count -gt 0) {
                    Write-Error "Failed to parse command line for step '$($step.name)': $commandline"
                    continue
                }

                if (-not $tokens -or $tokens.Count -eq 0) {
                    Write-Error "Empty or invalid command line for step '$($step.name)': $commandline"
                    continue
                }

                # First token is the command; subsequent CommandArgument tokens are arguments
                $exeToken = $tokens | Where-Object { $_.Type -eq 'Command' } | Select-Object -First 1
                if (-not $exeToken) {
                    Write-Error "No executable command found in command line for step '$($step.name)': $commandline"
                    continue
                }

                $exe = $exeToken.Content
                $cmdArgs = @()
                foreach ($tok in $tokens) {
                    if ($tok -eq $exeToken) { continue }
                    if ($tok.Type -eq 'CommandArgument') {
                        $cmdArgs += $tok.Content
                    }
                }

                & $exe @cmdArgs
            }
            elseif ($command -and ($arguments.Count -ge 1) -and ($parameters.Count -ge 1)) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name) [Arguments:$arguments]"
                ($parameters | Out-String).Trim()
                if ($Test) { continue }
                & $command @parameters @arguments
            }
            elseif ($command -and ($arguments.Count -ge 1)) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name) [Arguments:$arguments]"
                if ($Test) { continue }
                & $command @arguments
            }
            elseif ($command -and ($parameters.Count -ge 1)) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name)"
                ($parameters | Out-String).Trim()
                if ($Test) { continue }
                & $command @parameters
            }
            elseif ($command) {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] $($step.name)"
                if ($Test) { continue }
                & $command
            }
            else {
                Write-Host -ForegroundColor DarkCyan "[$(Get-Date -format s)] No command to execute."
                continue
            }
        }
        # End of workflow steps
        Write-Host -ForegroundColor Green "[$(Get-Date -format s)] Workflow Task execution done."
        #=================================================
        # End the function
        $Message = "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] End"
        Write-Verbose -Message $Message; Write-Debug -Message $Message
        #=================================================
    }
}