src/functions/qrun.ps1
|
function Invoke-QRun { [CmdletBinding()] param( [ArgumentCompleter({ param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) try { $mapPath = if ($fakeBoundParameters.map) { $fakeBoundParameters.map } else { "./.process.map.ps1" } $localMapExists = Test-Path $mapPath $mapNotFound = $false $map = try { Import-ConfigMap -Map $fakeBoundParameters.map -Fallback "./.process.map.ps1" -ErrorAction Stop } catch { if ($_.Exception.Message -match '^map file .* not found$|^No map provided and fallback .* not found$|^map is null and defaultMapFile is not provided$') { $mapNotFound = $true $null } else { throw } } if ($mapNotFound) { return @("!init", "!state", "help", "list") | ? { $_.startswith($wordToComplete) } } $map = $map | Assert-ConfigMap $completions = Get-EntryCompletion $map -language "build" @PSBoundParameters if (!$localMapExists) { $completions = @("!init" | ? { $_.startswith($wordToComplete) }) + $completions } return @(@("!state", "help", "list") | ? { $_.startswith($wordToComplete) }) + $completions } catch { return "ERROR [-entry]: $($_.Exception.Message) $($_.ScriptStackTrace)" } })] [Parameter(Position = 0)] $entry = $null, [switch]$Follow, [switch]$Force, [switch]$Status, [switch]$Reset, [Parameter(DontShow)] $map = "./.process.map.ps1", [Parameter(ValueFromRemainingArguments = $true, DontShow = $true)] [string[]]$RemainingArguments ) dynamicparam { if (-not $entry -or $entry -in @('help', 'list', '!init', '!state')) { return } try { $dynMap = . $ImportConfigMap -Map $map -Fallback "./.process.map.ps1" | Assert-ConfigMap return Get-ProcessDynamicParam -Map $dynMap -ProcessKey $entry -Follow:$Follow } catch { return } } process { if ($entry -eq "help") { Write-Host "" Write-Host "QRUN" -ForegroundColor Cyan Write-Host "Run multi-step processes with durable step state" -ForegroundColor Gray Write-Host "" Write-Host "USAGE:" -ForegroundColor Yellow Write-Host " qrun <process> [-Force] [-Status] [-Reset]" -ForegroundColor White Write-Host " qrun <process>.<step> [-Follow] [-Force]" -ForegroundColor White Write-Host " qrun !state [show|clean] [<process>]" -ForegroundColor White Write-Host "" Write-Host "OPTIONS:" -ForegroundColor Yellow Write-Host " -Follow With a step path, also run all following steps" -ForegroundColor White Write-Host " -Force Rerun succeeded steps; also allow starting mid-process when prior steps are incomplete" -ForegroundColor White Write-Host " -Status Show persisted step statuses without running" -ForegroundColor White Write-Host " -Reset Clear persisted state for the process" -ForegroundColor White Write-Host "" Write-Host "STATE:" -ForegroundColor Yellow Write-Host " qrun Show process state (same as !state)" -ForegroundColor White Write-Host " qrun !state Show all process state (alias for !state show)" -ForegroundColor White Write-Host " qrun !state show [name] Show state for all processes, or one process" -ForegroundColor White Write-Host " qrun !state clean [name] Clean all process state, or one process" -ForegroundColor White Write-Host "" Write-Host "Dot paths work like qbuild: 'qrun bootstrap.prepare' runs that step; add -Follow to continue." -ForegroundColor Gray Write-Host "" return } if ($entry -eq "list") { $listMap = . $ImportConfigMap -Map $map -Fallback "./.process.map.ps1" if (!$listMap) { Write-Help -invocation $MyInvocation -mapPath "./.process.map.ps1" return } Write-MapHelp -map $listMap -invocation $MyInvocation -language "build" return } if ($entry -eq "!init") { if (Test-Path $map) { throw "map file '$map' already exists" } Initialize-ProcessMap -file $map return } if (-not $entry -or $entry -eq "!state") { $stateArgs = @($RemainingArguments | Where-Object { $_ }) $action = 'show' $processFilter = $null if ($entry -eq "!state" -and $stateArgs.Count -ge 1) { if ($stateArgs[0] -in @('show', 'clean')) { $action = $stateArgs[0] if ($stateArgs.Count -ge 2) { $processFilter = $stateArgs[1] } if ($stateArgs.Count -gt 2) { throw "!state $action accepts at most one process name." } } else { throw "!state expects 'show' or 'clean', got '$($stateArgs[0])'. Try: qrun !state show" } } $stateMap = . $ImportConfigMap -Map $map -Fallback "./.process.map.ps1" -ErrorAction Ignore if (!$stateMap) { Write-Help -invocation $MyInvocation -mapPath "./.process.map.ps1" return } $baseDir = if ($stateMap._baseDir) { $stateMap._baseDir } else { (Get-Location).Path } $sourceFile = if ($stateMap._sourceFile) { [string]$stateMap._sourceFile } else { $null } $operationContext = New-ConfigMapOperationContext Invoke-ProcessStateCommand ` -Action $action ` -ProcessKey $processFilter ` -Map $stateMap ` -BaseDir $baseDir ` -MapFile $sourceFile ` -OperationContext $operationContext return } $mapPath = $map $map = . $ImportConfigMap -Map $map -Fallback "./.process.map.ps1" -ErrorAction Ignore if (!$map) { Write-Help -invocation $MyInvocation -mapPath "./.process.map.ps1" return } $operationContext = New-ConfigMapOperationContext $target = Resolve-ProcessTarget -Map $map -Entry $entry -OperationContext $operationContext $processKey = $target.ProcessKey $processEntry = $target.ProcessEntry $stepNames = @($target.StepNames) $fromStep = $target.ImpliedStep if ($Follow -and -not $fromStep) { throw "-Follow requires a step path (e.g. qrun bootstrap.restore -Follow)" } $baseDir = if ($processEntry._baseDir) { $processEntry._baseDir } elseif ($map._baseDir) { $map._baseDir } else { (Get-Location).Path } $sourceFile = if ($map._sourceFile) { [string]$map._sourceFile } else { $null } $statePath = Get-ProcessStatePath -ProcessKey $processKey -BaseDir $baseDir -MapFile $sourceFile if ($Reset) { Reset-ProcessState -StatePath $statePath Write-Host "Reset process state for '$processKey'" -ForegroundColor Yellow if (-not $Status -and -not $fromStep -and -not $Follow -and -not $Force) { return } } $state = Read-ProcessState -StatePath $statePath -ProcessKey $processKey -StepNames $stepNames -MapFile $sourceFile if ($Status) { Write-ProcessStatus -ProcessKey $processKey -StepNames $stepNames -State $state return } $skippedPrior = @() if ($fromStep) { $skippedPrior = @(Assert-ProcessPriorSteps -StepNames $stepNames -FromStep $fromStep -State $state -Force:$Force) } $stepsToConsider = Get-ProcessStepsToRun -StepNames $stepNames -FromStep $fromStep -Follow:$Follow $total = $stepNames.Count Write-Host "" Write-Host "PROCESS " -NoNewline -ForegroundColor Cyan Write-Host $processKey -ForegroundColor White if ($fromStep) { $rangeLabel = if ($Follow) { "$fromStep …" } else { $fromStep } Write-Host "Step: $rangeLabel" -ForegroundColor DarkGray } Write-Host "State: $statePath" -ForegroundColor DarkGray Write-Host "" foreach ($priorName in $skippedPrior) { $priorIndex = [array]::IndexOf($stepNames, $priorName) + 1 $priorStatus = if ($state.steps[$priorName]) { [string]$state.steps[$priorName].status } else { 'Pending' } Write-Host "[$priorIndex/$total] " -NoNewline -ForegroundColor DarkGray Write-Host "SKIP" -NoNewline -ForegroundColor DarkYellow Write-Host " $priorName" -NoNewline -ForegroundColor White Write-Host " (forced; was $priorStatus)" -ForegroundColor DarkGray } $bound = @{} foreach ($key in $PSBoundParameters.Keys) { if ($key -in @('entry', 'Follow', 'Force', 'Status', 'Reset', 'map', 'RemainingArguments')) { continue } $bound[$key] = $PSBoundParameters[$key] } $passthrough = @($RemainingArguments) | Where-Object { $null -ne $_ } if ($passthrough.Count -gt 0) { $bound._context = @{ additionalArgs = @($passthrough) } } $ran = 0 $skipped = 0 foreach ($stepName in $stepsToConsider) { $stepIndex = [array]::IndexOf($stepNames, $stepName) + 1 $prefix = "[$stepIndex/$total]" $stepStatus = if ($state.steps[$stepName]) { [string]$state.steps[$stepName].status } else { 'Pending' } if ($stepStatus -eq 'Succeeded' -and -not $Force) { Write-Host "$prefix " -NoNewline -ForegroundColor DarkGray Write-Host "SKIP" -NoNewline -ForegroundColor DarkCyan Write-Host " $stepName" -NoNewline -ForegroundColor White Write-Host " (already succeeded)" -ForegroundColor DarkGray $skipped++ continue } Write-Host "$prefix " -NoNewline -ForegroundColor DarkGray Write-Host "RUN " -NoNewline -ForegroundColor Yellow Write-Host " $stepName..." -ForegroundColor White $state.steps[$stepName] = [ordered]@{ status = 'Running' startedAt = (Get-Date).ToString('o') finishedAt = $null error = $null } Write-ProcessState -StatePath $statePath -State $state $stepEntry = Get-ProcessStepEntry -ProcessEntry $processEntry -StepName $stepName try { Push-Location $baseDir try { $settingsEntry = if ($stepEntry -is [System.Collections.IDictionary]) { $stepEntry } else { $processEntry } Invoke-WithEntrySettings -Map $map -EntryKey "$processKey.$stepName" -Entry $settingsEntry -OperationContext $operationContext -ScriptBlock { Invoke-EntryCommand -entry $stepEntry -key "exec" -bound $bound } } finally { Pop-Location } $state.steps[$stepName] = [ordered]@{ status = 'Succeeded' startedAt = $state.steps[$stepName].startedAt finishedAt = (Get-Date).ToString('o') error = $null } Write-ProcessState -StatePath $statePath -State $state Write-Host "$prefix " -NoNewline -ForegroundColor DarkGray Write-Host "OK " -NoNewline -ForegroundColor Green Write-Host " $stepName" -ForegroundColor White $ran++ } catch { $errMsg = $_.Exception.Message $state.steps[$stepName] = [ordered]@{ status = 'Failed' startedAt = $state.steps[$stepName].startedAt finishedAt = (Get-Date).ToString('o') error = $errMsg } Write-ProcessState -StatePath $statePath -State $state Write-Host "$prefix " -NoNewline -ForegroundColor DarkGray Write-Host "FAIL" -NoNewline -ForegroundColor Red Write-Host " $stepName" -ForegroundColor White Write-Host " $errMsg" -ForegroundColor DarkRed throw } } Write-Host "" Write-Host "Process '$processKey' finished" -NoNewline -ForegroundColor Green Write-Host " (ran: $ran, skipped: $skipped)" -ForegroundColor DarkGray Write-Host "" } } Set-Alias -Name "qrun" -Value "Invoke-QRun" -Force |