workflow.psm1
|
# Workflow module for Delta-Code # Manages workflow definitions and pipeline execution # Resolve module dependencies from the sibling directory when available (repository # and flat-install layouts) and fall back to module-name resolution (PSGallery). foreach ($dependencyName in @('delta-code', 'satellite', 'workspace')) { $dependencyManifest = Join-Path $PSScriptRoot "$dependencyName.psd1" if (Test-Path -LiteralPath $dependencyManifest) { Import-Module $dependencyManifest -ErrorAction Stop -DisableNameChecking } else { Import-Module $dependencyName -ErrorAction Stop -DisableNameChecking } } Remove-Variable dependencyName, dependencyManifest -ErrorAction SilentlyContinue $script:WorkflowRegistryPath = Join-Path (Split-Path $PSScriptRoot) "data\workflows.json" $script:WorkflowRegistry = @{} $script:WorkflowHistoryPath = Join-Path (Split-Path $PSScriptRoot) "data\workflow-history.json" $script:WorkflowHistory = @() $script:AutomationProfilesPath = Join-Path (Split-Path $PSScriptRoot) "data\automation-profiles.json" $script:AutomationProfiles = @{} function Save-WorkflowRegistry { $data = @{ Workflows = $script:WorkflowRegistry.Values } $data | ConvertTo-Json -Depth 5 | Set-Content -Path $script:WorkflowRegistryPath -Encoding UTF8 } function Load-WorkflowRegistry { if (Test-Path $script:WorkflowRegistryPath) { $data = Get-Content $script:WorkflowRegistryPath -Raw | ConvertFrom-Json if ($data.Workflows) { $script:WorkflowRegistry = @{} foreach ($wf in $data.Workflows) { $script:WorkflowRegistry[$wf.Name] = $wf } } } } function Save-WorkflowHistory { $data = @{ History = $script:WorkflowHistory } $data | ConvertTo-Json -Depth 5 | Set-Content -Path $script:WorkflowHistoryPath -Encoding UTF8 } function Load-WorkflowHistory { if (Test-Path $script:WorkflowHistoryPath) { $data = Get-Content $script:WorkflowHistoryPath -Raw | ConvertFrom-Json if ($data.History) { $script:WorkflowHistory = $data.History } } } function Save-AutomationProfiles { $data = @{ Profiles = $script:AutomationProfiles.Values } $data | ConvertTo-Json -Depth 5 | Set-Content -Path $script:AutomationProfilesPath -Encoding UTF8 } function Load-AutomationProfiles { if (Test-Path $script:AutomationProfilesPath) { $data = Get-Content $script:AutomationProfilesPath -Raw | ConvertFrom-Json if ($data.Profiles) { $script:AutomationProfiles = @{} foreach ($p in $data.Profiles) { $script:AutomationProfiles[$p.Name] = $p } } } } # Load all registries on module import Load-WorkflowRegistry Load-WorkflowHistory Load-AutomationProfiles function Get-DeltaCodeWorkflow { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Name ) Load-WorkflowRegistry if ($script:WorkflowRegistry.ContainsKey($Name)) { return $script:WorkflowRegistry[$Name] } Write-Error "Workflow '$Name' not found in registry" return $null } function Get-DeltaCodeWorkflows { [CmdletBinding()] param( [string[]]$Filter ) Load-WorkflowRegistry $wfs = $script:WorkflowRegistry.Values if ($Filter) { $wfs = $wfs | Where-Object { $Filter -contains $_.Category } } return $wfs } function Register-DeltaCodeWorkflow { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Name, [Parameter(Mandatory = $true, Position = 1)] [string]$Workspace, [Parameter(Mandatory = $false)] [string]$Category = 'General', [Parameter(Mandatory = $false)] [string]$Description = '', [Parameter(Mandatory = $true)] [array]$Steps, [Parameter(Mandatory = $false)] [hashtable]$Metadata = @{} ) Load-WorkflowRegistry Initialize-DeltaCodeSatellites $ws = Get-DeltaCodeWorkspace -Name $Workspace if (-not $ws) { Write-Error "Referenced workspace '$Workspace' not found" return $false } foreach ($step in $Steps) { if ($step.Type -eq 'satellite') { if (-not $step.Satellite) { Write-Error "Step in workflow '$Name' missing Satellite property" return $false } $sat = Get-DeltaCodeSatellite -Name $step.Satellite if (-not $sat) { Write-Error "Workflow '$Name' references unknown satellite: $($step.Satellite)" return $false } } elseif ($step.Type -eq 'builtin') { $validBuiltins = @('Diff', 'WorkspaceList', 'WorkspaceShow', 'WorkspaceUse') if (-not ($validBuiltins -contains $step.Command)) { Write-Error "Unknown builtin command: $($step.Command). Valid: $($validBuiltins -join ', ')" return $false } } else { Write-Error "Unknown step type: $($step.Type). Valid: satellite, builtin" return $false } } $workflow = [PSCustomObject]@{ Name = $Name Workspace = $Workspace Category = $Category Description = $Description Steps = $Steps Metadata = $Metadata Registered = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') } $script:WorkflowRegistry[$Name] = $workflow Save-WorkflowRegistry Write-Verbose "Registered workflow '$Name' with $($Steps.Count) steps" return $true } function Test-DeltaCodeWorkflow { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Name ) Load-WorkflowRegistry $wf = Get-DeltaCodeWorkflow -Name $Name if (-not $wf) { return $false } $valid = $true $issues = @() $ws = Get-DeltaCodeWorkspace -Name $wf.Workspace if (-not $ws) { $issues += "Workspace '$($wf.Workspace)' no longer exists" $valid = $false } foreach ($step in $wf.Steps) { if ($step.Type -eq 'satellite') { $sat = Get-DeltaCodeSatellite -Name $step.Satellite if (-not $sat) { $issues += "Satellite '$($step.Satellite)' not found" $valid = $false } } elseif ($step.Type -eq 'builtin') { $validBuiltins = @('Diff', 'WorkspaceList', 'WorkspaceShow', 'WorkspaceUse') if (-not ($validBuiltins -contains $step.Command)) { $issues += "Unknown builtin command: $($step.Command)" $valid = $false } } } if (-not $valid) { Write-Warning "Workflow '$Name' validation failed: $($issues -join '; ')" } else { Write-Verbose "Workflow '$Name' validated successfully" } return $valid } function Initialize-DeltaCodeWorkflows { [CmdletBinding()] param( [string[]]$SearchPaths ) Load-WorkflowRegistry if (-not $SearchPaths) { $SearchPaths = @( "$PSScriptRoot\workflows", "$PSScriptRoot\..\workflows", (Join-Path $env:USERPROFILE ".delta-code\workflows") ) } foreach ($basePath in $SearchPaths) { if (Test-Path $basePath) { $files = Get-ChildItem -Path $basePath -Filter "*.json" -File foreach ($file in $files) { $config = Get-Content $file.FullName -Raw | ConvertFrom-Json if ($config.Name -and $config.Workspace -and $config.Steps) { Register-DeltaCodeWorkflow -Name $config.Name -Workspace $config.Workspace -Category $config.Category -Description $config.Description -Steps $config.Steps -Metadata $config.Metadata } } } } $names = @($script:WorkflowRegistry.Keys) foreach ($name in $names) { Test-DeltaCodeWorkflow -Name $name } } function Invoke-DeltaCodeWorkflow { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Name, [Parameter(Mandatory = $false)] [hashtable]$InputParams = @{}, [Parameter(Mandatory = $false)] [switch]$DryRun ) Load-WorkflowRegistry Initialize-DeltaCodeWorkspaces $wf = Get-DeltaCodeWorkflow -Name $Name if (-not $wf) { Write-Error "Workflow '$Name' not found" return $false } $wsResult = Set-DeltaCodeWorkspace -Name $wf.Workspace if (-not $wsResult) { Write-Error "Failed to activate workspace '$($wf.Workspace)' for workflow '$Name'" return $false } $activeWs = Get-DeltaCodeActiveWorkspace Write-Host "Executing workflow '$Name' in workspace '$($activeWs.Name)' ($($activeWs.Root))" $stepResults = @() $context = @{} $context.Input = $InputParams foreach ($step in $wf.Steps) { $stepName = if ($step.Name) { $step.Name } else { "$($step.Type):$($step.Satellite -join ' ')$($step.Command)" } Write-Host " Step: $stepName" -ForegroundColor Cyan if ($DryRun) { Write-Host " [DRY RUN] Would execute: $($step.Type) $($step.Satellite -join ' ') $($step.Command)" -ForegroundColor Yellow $stepResults += [PSCustomObject]@{ Name = $stepName; Status = 'DryRun'; Result = $null } continue } try { $result = $null if ($step.Type -eq 'satellite') { $result = Invoke-DeltaCodeSatellite -Name $step.Satellite -Command $step.Command -Args $step.Args } elseif ($step.Type -eq 'builtin') { switch ($step.Command) { 'Diff' { $from = if ($step.From) { $step.From } else { $activeWs.Root } $to = if ($step.To) { $step.To } else { $activeWs.Root } $result = Get-DeltaCodeDiff -From $from -To $to -IgnoreWhitespace $true } 'WorkspaceList' { $result = Get-DeltaCodeWorkspaces | Format-Table Name, Root, Category, Active } 'WorkspaceShow' { $result = Get-DeltaCodeActiveWorkspace | Format-List } 'WorkspaceUse' { $result = Set-DeltaCodeWorkspace -Name $step.Workspace } } } $stepResults += [PSCustomObject]@{ Name = $stepName; Status = 'Success'; Result = $result } $context[$stepName] = $result Write-Host " ✓ Success" -ForegroundColor Green } catch { $stepResults += [PSCustomObject]@{ Name = $stepName; Status = 'Failed'; Error = $_.Exception.Message } Write-Host " ✗ Failed: $($_.Exception.Message)" -ForegroundColor Red if (-not $step.ContinueOnError) { Write-Error "Workflow '$Name' aborted at step '$stepName'" return $false } } } $failed = $stepResults | Where-Object { $_.Status -eq 'Failed' } if ($failed.Count -gt 0) { Write-Host "Workflow '$Name' completed with $($failed.Count) failed step(s)" -ForegroundColor Red return $false } else { Write-Host "Workflow '$Name' completed successfully" -ForegroundColor Green return $true } } function Get-DeltaCodeWorkflowStep { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$WorkflowName, [Parameter(Mandatory = $true, Position = 1)] [int]$StepIndex ) Load-WorkflowRegistry $wf = Get-DeltaCodeWorkflow -Name $WorkflowName if (-not $wf -or $StepIndex -lt 0 -or $StepIndex -ge $wf.Steps.Count) { return $null } return $wf.Steps[$StepIndex] } function Remove-DeltaCodeWorkflow { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Name ) Load-WorkflowRegistry if ($script:WorkflowRegistry.ContainsKey($Name)) { $script:WorkflowRegistry.Remove($Name) Save-WorkflowRegistry Write-Verbose "Removed workflow '$Name'" return $true } return $false } function Record-WorkflowExecution { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Workflow, [Parameter(Mandatory = $true)] [string]$Workspace, [Parameter(Mandatory = $true)] [string]$Status, [Parameter(Mandatory = $true)] [long]$DurationMs, [Parameter(Mandatory = $true)] [array]$Steps, [string[]]$FailedSteps = @() ) Load-WorkflowHistory $record = [PSCustomObject]@{ Workflow = $Workflow Workspace = $Workspace Started = (Get-Date).AddMilliseconds(-$DurationMs).ToString('o') DurationMs = $DurationMs Status = $Status Steps = $Steps FailedSteps = $FailedSteps Timestamp = (Get-Date).ToString('o') } $script:WorkflowHistory += $record if ($script:WorkflowHistory.Count -gt 1000) { $script:WorkflowHistory = $script:WorkflowHistory[-1000..-1] } Save-WorkflowHistory return $record } function Get-DeltaCodeWorkflowHistory { [CmdletBinding()] param( [string]$Workflow, [string]$Workspace, [int]$Limit = 50 ) Load-WorkflowHistory $history = $script:WorkflowHistory if ($Workflow) { $history = $history | Where-Object { $_.Workflow -eq $Workflow } } if ($Workspace) { $history = $history | Where-Object { $_.Workspace -eq $Workspace } } $history = $history | Sort-Object { [DateTime]::Parse($_.Timestamp) } -Descending if ($Limit -gt 0) { $history = $history | Select-Object -First $Limit } return $history } function Get-DeltaCodeWorkflowMetrics { [CmdletBinding()] param( [string]$Workflow, [string]$Workspace, [int]$Limit = 100 ) $history = Get-DeltaCodeWorkflowHistory -Workflow $Workflow -Workspace $Workspace -Limit $Limit if (-not $history -or $history.Count -eq 0) { return @{ Workflow = $Workflow Workspace = $Workspace TotalRuns = 0 SuccessRate = 0 AvgDurationMs = 0 MinDurationMs = 0 MaxDurationMs = 0 StepMetrics = @{} } } $successful = $history | Where-Object { $_.Status -eq 'Success' } $failed = $history | Where-Object { $_.Status -eq 'Failed' } $durations = $history.DurationMs $stepMetrics = @{} foreach ($run in $history) { foreach ($step in $run.Steps) { $stepName = $step.Name if (-not $stepMetrics.ContainsKey($stepName)) { $stepMetrics[$stepName] = @{ Count = 0 Successes = 0 Failures = 0 TotalMs = 0 } } $stepMetrics[$stepName].Count++ if ($run.Status -eq 'Success') { $stepMetrics[$stepName].Successes++ } else { $stepMetrics[$stepName].Failures++ } if ($step.DurationMs) { $stepMetrics[$stepName].TotalMs += $step.DurationMs } } } return @{ Workflow = $Workflow Workspace = $Workspace TotalRuns = $history.Count SuccessRuns = $successful.Count FailedRuns = $failed.Count SuccessRate = if ($history.Count -gt 0) { [math]::Round($successful.Count / $history.Count * 100, 1) } else { 0 } AvgDurationMs = if ($durations.Count -gt 0) { [math]::Round(($durations | Measure-Object -Average).Average, 0) } else { 0 } MinDurationMs = if ($durations.Count -gt 0) { ($durations | Measure-Object -Minimum).Minimum } else { 0 } MaxDurationMs = if ($durations.Count -gt 0) { ($durations | Measure-Object -Maximum).Maximum } else { 0 } StepMetrics = $stepMetrics } } function Register-AutomationProfile { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Name, [Parameter(Mandatory = $true, Position = 1)] [string]$Workflow, [Parameter(Mandatory = $false)] [switch]$DryRun = $false, [Parameter(Mandatory = $false)] [int]$RetryCount = 0, [Parameter(Mandatory = $false)] [switch]$ContinueOnError = $false, [Parameter(Mandatory = $false)] [hashtable]$InputParams = @{}, [Parameter(Mandatory = $false)] [string]$Description = '' ) Load-AutomationProfiles $profile = [PSCustomObject]@{ Name = $Name Workflow = $Workflow DryRun = $DryRun RetryCount = $RetryCount ContinueOnError = $ContinueOnError InputParams = $InputParams Description = $Description Created = (Get-Date).ToString('o') } $script:AutomationProfiles[$Name] = $profile Save-AutomationProfiles Write-Verbose "Registered automation profile '$Name' for workflow '$Workflow'" return $true } function Get-AutomationProfile { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Name ) Load-AutomationProfiles if ($script:AutomationProfiles.ContainsKey($Name)) { return $script:AutomationProfiles[$Name] } Write-Error "Automation profile '$Name' not found" return $null } function Get-AutomationProfiles { [CmdletBinding()] param() Load-AutomationProfiles return $script:AutomationProfiles.Values } function Invoke-DeltaCodeProfile { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Name, [switch]$DryRun ) Load-AutomationProfiles $profile = Get-AutomationProfile -Name $Name if (-not $profile) { return $false } $result = $true $attempt = 0 $maxAttempts = $profile.RetryCount + 1 while ($attempt -lt $maxAttempts) { $attempt++ if ($attempt -gt 1) { Write-Host "Retry attempt $attempt of $maxAttempts for profile '$Name'" -ForegroundColor Yellow Start-Sleep -Seconds 2 } if ($DryRun -or $profile.DryRun) { $result = Invoke-DeltaCodeWorkflow -Name $profile.Workflow -InputParams $profile.InputParams -DryRun } else { $result = Invoke-DeltaCodeWorkflow -Name $profile.Workflow -InputParams $profile.InputParams } if ($result) { Write-Host "Profile '$Name' executed successfully" -ForegroundColor Green return $true } elseif (-not $profile.ContinueOnError) { Write-Error "Profile '$Name' failed and ContinueOnError is not set" return $false } } Write-Error "Profile '$Name' failed after $maxAttempts attempts" return $false } Export-ModuleMember -Function @( 'Get-DeltaCodeWorkflow' 'Get-DeltaCodeWorkflows' 'Register-DeltaCodeWorkflow' 'Test-DeltaCodeWorkflow' 'Initialize-DeltaCodeWorkflows' 'Invoke-DeltaCodeWorkflow' 'Get-DeltaCodeWorkflowStep' 'Remove-DeltaCodeWorkflow' 'Record-WorkflowExecution' 'Get-DeltaCodeWorkflowHistory' 'Get-DeltaCodeWorkflowMetrics' 'Register-AutomationProfile' 'Get-AutomationProfile' 'Get-AutomationProfiles' 'Invoke-DeltaCodeProfile' ) |