Private/Invoke-KadenManagedInitialize.ps1
|
function Invoke-KadenManagedInitialize { <# .SYNOPSIS Apply the allowlisted managed payload to a child repository under a transaction journal. .DESCRIPTION Creates `.kaden/transactions/<id>/` with a lock, stages payload files, and copies only manifest destinations that resolve inside the child root. Source paths must resolve inside the payload root. On failure, files already copied are restored from backup or removed when they were new, the journal status becomes incomplete, and the lock is released. Shared `.kaden.json` and gitignored `.kaden.local.json` are written only after every managed file verifies. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$ChildRoot, [Parameter(Mandatory = $true)][string]$ProjectId, [Parameter(Mandatory = $true)]$Manifest, [Parameter(Mandatory = $true)][string]$PayloadRoot, [Parameter(Mandatory = $true)][string]$ModuleVersion, [Parameter(Mandatory = $false)][switch]$KeepLock ) $operations = @() $journal = $null $journalPath = $null $txnRoot = Join-Path $ChildRoot (Join-Path '.kaden' 'transactions') $lockPath = Join-Path $txnRoot 'update.lock' New-Item -ItemType Directory -Path $txnRoot -Force | Out-Null $transactionId = [guid]::NewGuid().ToString('N') Lock-KadenUpdate -LockPath $lockPath -TransactionId $transactionId $txnDir = Join-Path $txnRoot $transactionId $staging = Join-Path $txnDir 'staging' $backup = Join-Path $txnDir 'backup' New-Item -ItemType Directory -Path $staging -Force | Out-Null New-Item -ItemType Directory -Path $backup -Force | Out-Null $releaseLock = $true try { foreach ($entry in @($Manifest.files)) { $destRel = [string]$entry.destination if (Test-KadenDelimitedInstructionDestination -Destination $destRel) { continue } $sourceRel = [string]$entry.source $expected = [string]$entry.digest $sourcePath = Resolve-KadenContainedPath -Root $PayloadRoot -RelativePath $sourceRel $stagedPath = Join-Path $staging ($destRel -replace '[\\/]', '__') Copy-Item -LiteralPath $sourcePath -Destination $stagedPath -Force $operations += [pscustomobject]@{ destination = $destRel expectedDigest = $expected previousDigest = $null backupPath = $null stagedPath = $stagedPath intentStatus = 'planned' backupStatus = 'pending' replacementStatus = 'pending' verificationStatus = 'pending' } } $journal = [ordered]@{ schemaVersion = 1 transactionId = $transactionId status = 'planned' startedAtUtc = [datetime]::UtcNow.ToString('o') payloadVersion = $ModuleVersion payloadDigest = [string]$Manifest.payloadDigest managedManifestDigest = Get-KadenFileDigest -LiteralPath (Join-Path $PayloadRoot 'kaden-managed-files.json') childRoot = $ChildRoot operations = $operations stateBefore = $null stateAfter = $null verification = $null cleanupEligibleAtUtc = $null failure = $null } $journalPath = Join-Path $txnDir 'journal.json' Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal for ($i = 0; $i -lt $operations.Count; $i++) { $op = $operations[$i] $destPath = Resolve-KadenContainedPath -Root $ChildRoot -RelativePath ([string]$op.destination) $previousDigest = $null $backupPath = $null if (Test-Path -LiteralPath $destPath) { $previousDigest = Get-KadenFileDigest -LiteralPath $destPath $backupPath = Join-Path $backup (($op.destination -replace '[\\/]', '__') + '.bak') } $operations[$i].previousDigest = $previousDigest $operations[$i].backupPath = $backupPath $operations[$i].intentStatus = 'backup-intent-recorded' $operations[$i].backupStatus = if ($backupPath) { 'intent-recorded' } else { 'not-required' } $journal.status = 'backup-incomplete' $journal.operations = $operations Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal if ($backupPath) { Copy-Item -LiteralPath $destPath -Destination $backupPath -Force $operations[$i].backupStatus = 'complete' $journal.operations = $operations Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal } $operations[$i].intentStatus = 'replacement-intent-recorded' $operations[$i].replacementStatus = 'intent-recorded' $journal.status = 'replacement-incomplete' $journal.operations = $operations Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal $destDir = Split-Path -Parent $destPath if (-not (Test-Path -LiteralPath $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } Copy-Item -LiteralPath $op.stagedPath -Destination $destPath -Force $operations[$i].replacementStatus = 'complete' $journal.operations = $operations Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal $actual = Get-KadenFileDigest -LiteralPath $destPath if ($actual -ne $op.expectedDigest) { throw "KDN-TXN-PREFLIGHT-FAILED: digest mismatch after applying '$($op.destination)'." } $operations[$i].verificationStatus = 'verified' $journal.operations = $operations Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal } $sharedPath = Join-Path $ChildRoot '.kaden.json' $stateBefore = $null if (Test-Path -LiteralPath $sharedPath) { $priorShared = Get-Content -LiteralPath $sharedPath -Raw | ConvertFrom-Json $stateBefore = [ordered]@{ managedContentVersion = [string]$priorShared.managedContentVersion projectId = [string]$priorShared.projectId } } $journal.stateBefore = $stateBefore $journal.status = 'payload-verified' $journal.operations = $operations $journal.verification = [pscustomobject]@{ status = 'payload-verified'; atUtc = [datetime]::UtcNow.ToString('o') } Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal $localPath = Join-Path $ChildRoot '.kaden.local.json' $shared = [ordered]@{ schemaVersion = 1 projectId = $ProjectId managedContentVersion = $ModuleVersion } if (Test-Path -LiteralPath $sharedPath) { $existingShared = Get-Content -LiteralPath $sharedPath -Raw | ConvertFrom-Json foreach ($prop in @($existingShared.PSObject.Properties)) { $name = [string]$prop.Name if ($shared.Contains($name)) { continue } $shared[$name] = $prop.Value } } $local = [ordered]@{ schemaVersion = 1 lastAppliedVersion = $ModuleVersion lastAppliedAtUtc = [datetime]::UtcNow.ToString('o') installationStatus = 'complete' } Write-KadenJsonAtomic -LiteralPath $sharedPath -Object $shared Write-KadenJsonAtomic -LiteralPath $localPath -Object $local Add-KadenTransactionalIgnoreEntry -ChildRoot $ChildRoot $journal.status = 'completed' $journal.stateAfter = [pscustomobject]@{ managedContentVersion = $ModuleVersion projectId = $ProjectId } $journal.cleanupEligibleAtUtc = [datetime]::UtcNow.AddDays(30).ToString('o') Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $backup -Recurse -Force -ErrorAction SilentlyContinue if ($KeepLock) { $releaseLock = $false } } catch { $failureMessage = $_.Exception.Message try { Restore-KadenManagedFile -ChildRoot $ChildRoot -Operations $operations } catch { $failureMessage = "$failureMessage Restore failed: $($_.Exception.Message)" } foreach ($op in @($operations)) { if ([string]$op.replacementStatus -eq 'complete' -or [string]$op.verificationStatus -eq 'verified') { $op.replacementStatus = 'pending' $op.verificationStatus = 'pending' } } if ($journalPath -and $journal) { $journal.status = 'incomplete' $journal.operations = $operations $journal.failure = [pscustomobject]@{ atUtc = [datetime]::UtcNow.ToString('o') message = $failureMessage } Write-KadenJsonAtomic -LiteralPath $journalPath -Object $journal } throw $failureMessage } finally { if ($releaseLock) { Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue } } } |