Public/New-NSPConditionalAccessDeploymentRun.ps1

function New-NSPConditionalAccessDeploymentRun {
    <#
    .SYNOPSIS
        Creates a run journal from a plan's approved items. Performs none of them.
    .DESCRIPTION
        The journal (.nsp-conditionalaccess\runs\<TenantId>\<RunId>.json, Git-ignored) holds, in plan
        order, every Approved item with its stages. Each stage is saved as it moves
        Pending -> Running -> Succeeded / Failed, so a run survives a closed window or a reboot.
        Invoke-NSPConditionalAccessDeploymentRun performs it.
 
        Refuses a plan with no approved items. Items that depend on a prerequisite (DependsOn)
        must have every prerequisite approved in the same plan.
    .EXAMPLE
        $run = New-NSPConditionalAccessDeploymentRun -Plan $plan.Path
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Plan,
        [string]$TenantProfile,
        [string]$ConfigRoot
    )

    $content = Get-Content -LiteralPath $Plan -Raw | ConvertFrom-Json
    if (-not $content.PlanId) { throw "$Plan is not a deployment plan." }
    $approved = @($content.Items | Where-Object Decision -eq 'Approved')
    if (-not $approved.Count) { throw 'The plan has no approved items. Use Set-NSPConditionalAccessPlanDecision first.' }
    $approvedIds = @($approved | ForEach-Object ItemId)
    foreach ($item in $approved) {
        $missing = @($item.DependsOn | Where-Object { $_ -and $_ -notin $approvedIds })
        if ($missing.Count) { throw "$($item.ItemId) depends on $($missing -join ', '), which is not approved." }
    }

    if (-not $TenantProfile) {
        $TenantProfile = Join-Path (Get-NSPConditionalAccessLocalPath -Kind TenantProfiles -ConfigRoot $ConfigRoot) "$($content.TenantId).json"
    }
    if (-not (Test-Path -LiteralPath $TenantProfile)) { throw "Tenant profile not found: $TenantProfile" }

    $versions = @{}
    foreach ($definition in Get-NSPConditionalAccessDefinition) { $versions[$definition['Id']] = $definition['Version'] }
    $runId = (Get-Date).ToUniversalTime().ToString('yyyyMMdd-HHmmss') + '-' + ([guid]::NewGuid().ToString('N').Substring(0, 6))
    $run = [pscustomobject][ordered]@{
        SchemaVersion = 1
        RunId = $runId
        PlanId = $content.PlanId
        PlanPath = (Resolve-Path -LiteralPath $Plan).Path
        ProfilePath = (Resolve-Path -LiteralPath $TenantProfile).Path
        TenantId = $content.TenantId
        TenantDomain = $content.TenantDomain
        CreatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
        Status = 'Pending'
        Items = @($approved | ForEach-Object {
                $item = $_
                [pscustomobject][ordered]@{
                    ItemId = $item.ItemId
                    Kind = $item.Kind
                    Action = $item.Action
                    DefinitionId = $item.DefinitionId
                    DefinitionVersion = if ($item.DefinitionId) { $versions[$item.DefinitionId] } else { $null }
                    DisplayName = $item.DisplayName
                    DesiredBody = $item.DesiredBody
                    DesiredHash = $item.DesiredHash
                    LiveObjectId = $item.LiveObjectId
                    LiveHash = $item.LiveHash
                    LiveState = $item.LiveState
                    ObjectId = $item.LiveObjectId
                    Result = 'Pending'
                    Notes = @()
                    Stages = @(Get-NSPConditionalAccessRunStage -Action $item.Action | ForEach-Object {
                            [pscustomobject]@{ Name = $_; Status = 'Pending'; Message = $null; StartedAtUtc = $null; FinishedAtUtc = $null }
                        })
                }
            })
        Path = $null
    }
    $run.Path = Join-Path (Join-Path (Get-NSPConditionalAccessLocalPath -Kind Runs -ConfigRoot $ConfigRoot) $content.TenantId) "$runId.json"
    if ($PSCmdlet.ShouldProcess($run.Path, "Create run journal for $($approved.Count) approved item(s)")) {
        Save-NSPConditionalAccessJson -InputObject $run -Path $run.Path
    }
    $run
}