Modules/businessdev.ALbuild.Core/Public/Assert-ALbuildModuleComplete.ps1
|
function Assert-ALbuildModuleComplete { <# .SYNOPSIS Verify an installed businessdev.ALbuild version folder is complete and loadable. .DESCRIPTION The 2026-08 outage was a single .ps1 file lost during a concurrent, non-atomic Install-Module: the version still LOOKED installed, so every task imported a module that could not load (0x80131047). This check catches exactly that - a version folder that is present but missing a file. It is cheap enough to run on every task (the steady-state path is: read the '.albuild-complete' marker, re-count the module's PowerShell files, compare) yet strict enough to notice a lost file: 1. '<Path>\businessdev.ALbuild.psd1' exists and parses (Import-PowerShellDataFile). 2. Every path in the manifest's NestedModules exists under <Path>. 3. The '.albuild-complete' marker exists and its recorded PowerShell-file count + psd1 hash match the folder as it is now (a lost/added/edited .ps1|.psm1|.psd1 changes the count or the hash). A folder with no marker is treated as INCOMPLETE (never trusted) - so a version installed by the old, marker-less bootstrap is re-verified/repaired on first use rather than trusted forever. .PARAMETER Path The version folder to verify, e.g. '...\Modules\businessdev.ALbuild\2.18.2'. .PARAMETER PassThru Return a findings object instead of throwing: { Complete, Path, Version, Reason, ExpectedCount, ActualCount }. Without it, an incomplete folder throws with the reason. .OUTPUTS Nothing (throws on failure), or a PSCustomObject with -PassThru. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path, [switch] $PassThru ) $version = Split-Path -Path $Path -Leaf $result = [ordered]@{ Complete = $false; Path = $Path; Version = $version; Reason = ''; ExpectedCount = $null; ActualCount = $null } $fail = { param([string] $reason) $result.Reason = $reason if ($PassThru) { return [PSCustomObject]$result } throw "businessdev.ALbuild $version at '$Path' is incomplete: $reason" } if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return (& $fail 'the version folder does not exist') } # 1. Manifest present + parseable. $manifest = Join-Path $Path 'businessdev.ALbuild.psd1' if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) { return (& $fail 'the module manifest (.psd1) is missing') } $psd = $null try { $psd = Import-PowerShellDataFile -Path $manifest -ErrorAction Stop } catch { return (& $fail "the module manifest does not parse: $($_.Exception.Message)") } # 2. Every declared nested module exists. foreach ($nested in @($psd.NestedModules)) { $nestedPath = Join-Path $Path $nested if (-not (Test-Path -LiteralPath $nestedPath -PathType Leaf)) { return (& $fail "nested module '$nested' is missing") } } # 3. Marker present + inventory matches. The marker (JSON) was written from the freshly-downloaded, # complete staging copy at install time; a later-lost file makes the live count/hash disagree. $markerPath = Join-Path $Path '.albuild-complete' if (-not (Test-Path -LiteralPath $markerPath -PathType Leaf)) { return (& $fail "no '.albuild-complete' marker (installed by an old bootstrap, or never completed)") } $marker = $null try { $marker = Get-Content -LiteralPath $markerPath -Raw | ConvertFrom-Json } catch { return (& $fail "the '.albuild-complete' marker does not parse: $($_.Exception.Message)") } $inv = Get-ALbuildModuleInventory -Path $Path $result.ExpectedCount = $marker.fileCount $result.ActualCount = $inv.FileCount if ([int]$marker.fileCount -ne [int]$inv.FileCount) { return (& $fail "PowerShell file count is $($inv.FileCount) but the marker recorded $($marker.fileCount) (a file was lost or added)") } if ("$($marker.manifestHash)" -and "$($marker.manifestHash)" -ne "$($inv.ManifestHash)") { return (& $fail 'the manifest hash does not match the marker (the .psd1 changed)') } $result.Complete = $true if ($PassThru) { return [PSCustomObject]$result } } |