Set-StrictMode -Version Latest $ErrorActionPreference='Stop' Import-Module (Join-Path $PSScriptRoot 'MsPrivate.IO.psm1') -ErrorAction Stop function New-MsPrivateScriptInvoker { param([Parameter(Mandatory)][string]$Root) $runtimeRoot=[IO.Path]::GetFullPath($Root).TrimEnd([IO.Path]::DirectorySeparatorChar)+[IO.Path]::DirectorySeparatorChar if(-not[IO.Directory]::Exists($runtimeRoot)){throw "Runtime root does not exist: $runtimeRoot"} { param([Parameter(Mandatory)][string]$Script,[hashtable]$Parameters=@{}) if($Script-notmatch'^[A-Za-z0-9][A-Za-z0-9.-]*\.ps1$'){throw "Invalid runtime script name: $Script"} $path=[IO.Path]::GetFullPath((Join-Path $runtimeRoot $Script)) if(-not$path.StartsWith($runtimeRoot,[StringComparison]::OrdinalIgnoreCase)-or-not[IO.File]::Exists($path)){throw "Runtime script was not found: $Script"} & $path @Parameters|Out-Host }.GetNewClosure() } function Invoke-MsPrivateGroupPlan { param( [Parameter(Mandatory)][object[]]$Groups, [scriptblock]$OnProgress, [string]$ReportPath ) $seen=@{};$known=@{} foreach($group in $Groups){ $id=[string]$group.Id if($id-notmatch'^[A-Z][A-Za-z0-9-]*$'-or$seen.ContainsKey($id)-or$group.Action-isnot[scriptblock]){throw "Invalid or duplicate operation group: $id"} foreach($dependency in @($group.DependsOn)){if(-not$known.ContainsKey([string]$dependency)){throw "Operation dependency must reference an earlier group: $id/$dependency"}} $seen[$id]=$true;$known[$id]=$true } $entries=@($Groups|ForEach-Object{[pscustomobject]@{Id=[string]$_.Id;Label=[string]$_.Label;DependsOn=@($_.DependsOn);Status='Pending';Error=$null}}) $report=[pscustomobject]@{Schema=1;Kind='MsPrivateRun';CreatedUtc=[DateTime]::UtcNow.ToString('o');CompletedUtc=$null;Status='Running';Entries=$entries} if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8} foreach($group in $Groups){ $entry=@($entries | Where-Object { $_.Id -ceq [string]$group.Id })[0] $blocked=@($entry.DependsOn | Where-Object { $dependency=[string]$_ $dependencyEntry=@($entries | Where-Object { $_.Id -ceq $dependency }) $dependencyEntry.Count -ne 1 -or $dependencyEntry[0].Status -ne 'Succeeded' }) if($blocked.Count){$entry.Status='SkippedDependency';$entry.Error='Required group did not succeed: '+($blocked-join', ');if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8};continue} $entry.Status='Running';if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8};if($OnProgress){&$OnProgress $entry} try{& $group.Action|Out-Host;$entry.Status='Succeeded'}catch{$entry.Status='Failed';$entry.Error=$_.Exception.Message} if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8} } $failed=@($entries | Where-Object { $_.Status -eq 'Failed' }).Count $skipped=@($entries | Where-Object { $_.Status -eq 'SkippedDependency' }).Count $report.Status=if($failed-or$skipped){'PartialFailure'}else{'Completed'};$report.CompletedUtc=[DateTime]::UtcNow.ToString('o');if($ReportPath){Write-MsPrivateJsonAtomic -InputObject $report -Path $ReportPath -Depth 8} $report } Export-ModuleMember -Function Invoke-MsPrivateGroupPlan,New-MsPrivateScriptInvoker