Public/Steps/Invoke-HDTPowerShellStep.ps1
|
function Invoke-HDTPowerShellStep { <# .SYNOPSIS Runs a user PowerShell script from the workspace. .DESCRIPTION The extensibility point: - name: Vendor BIOS Update type: PowerShell script: Scripts\Update-VendorBios.ps1 log: BiosUpdate.log The script runs through the injected IScriptInvoker - never with a bare call operator and never by launching a process - so a sequence carrying a PowerShell step is provable under Pester with nothing executed. The step contract test greps this file for the cmdlet names that would break that, which is why they do not appear even in prose. It receives the LIVE variable dictionary as -Variable, so a script may read every resolved variable and, being live, an assignment a previous step made is visible to it. A relative script path is resolved against the workspace root here, so the invoker is handed an absolute path and the same sequence works from a share or from standalone media. EVERYTHING THE SCRIPT WROTE IS LOGGED. GetTranscript() returns what the invoker captured, and each line is written through Write-HDTLog, which puts it in HDT.jsonl, HDT.log and the executing step's own log at once. That is a hard requirement - "an existing script that only uses Write-Host still lands in the log without modification" - and it is why IScriptInvoker has a transcript at all. A SCRIPT THAT THREW IS A FAILED STEP, NOT A FAILED RUN. The exception is caught, logged and returned as Failed with its message. continueOnError, the retry policy and the failure classification all belong to the loop, which cannot make any of those decisions about an exception that flew past it. .PARAMETER Step A flattened step from Import-HDTSequenceDocument. Its Property carries `script`. .PARAMETER Context A New-HDTExecutionContext context. .OUTPUTS A New-HDTStepResult. Data carries whatever object the script emitted. .EXAMPLE Invoke-HDTPowerShellStep -Step $step -Context $context #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $Step, [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $Context ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $property = $Step.Property $scriptPath = '' if ($null -ne $property -and $property.Contains('script')) { $scriptPath = [string] $property['script'] } if ([string]::IsNullOrWhiteSpace($scriptPath)) { $message = "step '{0}' declares no script. A PowerShell step names a script relative to the workspace root." -f $Step.Name Write-HDTLog -Context $Context.Log -Message $message -Severity Error -Event step.fail -Component 'PowerShell' return (New-HDTStepResult -Status Failed -Message $message) } $invoker = $Context.Service.GetRequired('ScriptInvoker', 'PowerShell') # [System.IO.Path]::Combine, not Join-Path: Join-Path resolves the drive # qualifier through the PowerShell provider and throws "Cannot find drive" # for a workspace on a drive this session cannot see - which is every # WinPE-side path evaluated from a technician's desk, and every test. $resolved = $scriptPath if (-not [System.IO.Path]::IsPathRooted($scriptPath)) { $resolved = [System.IO.Path]::Combine([string] $Context.WorkspaceRoot, $scriptPath) } $result = $null $failure = $null try { $result = $invoker.Invoke($resolved, $Context.Variable) } catch { $failure = $_ } foreach ($line in @($invoker.GetTranscript())) { if ([string]::IsNullOrWhiteSpace($line)) { continue } Write-HDTLog -Context $Context.Log -Message $line -Component 'PowerShell' -Source $scriptPath } if ($null -ne $failure) { Write-HDTLog -Context $Context.Log -Message $failure.Exception.Message -Severity Error ` -Event step.fail -Component 'PowerShell' ` -Data ([ordered] @{ script = $resolved }) return (New-HDTStepResult -Status Failed -Message $failure.Exception.Message) } Write-HDTLog -Context $Context.Log -Message ("ran {0}" -f $resolved) -Event step.complete ` -Component 'PowerShell' -Data ([ordered] @{ script = $resolved }) return (New-HDTStepResult -Status Completed -Message ("ran {0}" -f $resolved) -Data $result) } |