Modules/businessdev.ALbuild.Containers/Private/Test-BcArtifactFolderIntact.ps1
|
function Test-BcArtifactFolderIntact { <# .SYNOPSIS Decides whether a cached BC artifact folder can be trusted, and says why not. .DESCRIPTION Two very different kinds of cached artifact exist side by side on a build agent: 1. Folders ALbuild downloaded itself. Those carry a '.albuild-complete' marker with the file count and total size recorded at publish time, so this can re-count and detect ANY later loss - a missing folder, a truncated file, a partial manual cleanup. 2. Folders somebody else produced - the BC container image downloading into the mounted cache, BcContainerHelper, a copied folder. Those have no marker, so completeness can only be judged structurally. The structural check deliberately goes beyond "manifest.json parses", which is what the previous validation did and why a real failure slipped through: the broken cache HAD a valid manifest and the database file - what it was missing were the Applications.<country> and Extensions folders. The container then started fine and only failed minutes later, inside the test-toolkit install, with AL compiler errors that named none of this. A legacy marker (the old bare-timestamp format) is treated as trustworthy-but-unfingerprinted: it is NOT invalidated, because doing so would throw away every already-good cache on the estate the moment this ships. It is reported so the caller can adopt it by writing a real fingerprint. .PARAMETER Path The extracted artifact folder to check. .PARAMETER IsPlatform The folder is the platform artifact, which has a different expected shape than a country one (no Applications.<country>, and older platform artifacts ship no manifest.json at all). .OUTPUTS PSCustomObject: IsIntact (bool), Reason (string), MarkerState ('None'|'Legacy'|'Fingerprinted'). #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path, [switch] $IsPlatform ) $result = { param($intact, $reason, $marker) [PSCustomObject]@{ IsIntact = $intact; Reason = $reason; MarkerState = $marker } } if (-not (Test-Path -LiteralPath $Path)) { return & $result $false 'the folder does not exist' 'None' } # --- 1. ALbuild-owned folders: re-count against the recorded fingerprint -------------------------- $markerPath = Join-Path $Path '.albuild-complete' if (Test-Path -LiteralPath $markerPath) { $recorded = $null try { $recorded = Get-Content -LiteralPath $markerPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { $recorded = $null } # legacy bare-timestamp marker $hasFingerprint = $recorded -and ($recorded.PSObject.Properties.Name -contains 'files') -and ($recorded.PSObject.Properties.Name -contains 'bytes') if ($hasFingerprint) { $now = Get-BcArtifactInventory -Path $Path if ([int] $recorded.files -eq $now.Files -and [long] $recorded.bytes -eq $now.Bytes) { return & $result $true '' 'Fingerprinted' } return & $result $false ( "content changed since it was cached (recorded $($recorded.files) file(s) / $($recorded.bytes) bytes, " + "found $($now.Files) / $($now.Bytes))") 'Fingerprinted' } # Legacy marker: fall through to the structural check, but report it so the caller can adopt it. $legacy = $true } else { $legacy = $false } # --- 2. Structural check for anything without a usable fingerprint -------------------------------- $markerState = if ($legacy) { 'Legacy' } else { 'None' } $manifestPath = Join-Path $Path 'manifest.json' $manifest = $null if (Test-Path -LiteralPath $manifestPath) { try { $manifest = Get-Content -LiteralPath $manifestPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { return & $result $false "manifest.json is present but not parseable ($($_.Exception.Message))" $markerState } } if ($IsPlatform) { # Deliberately unchanged from the long-standing rule: a parseable manifest.json, or (older BC # platform artifacts ship none) a 'lastused' marker plus real content. Demanding specific folders # here would be guessing - the platform layout differs across BC versions and artifact types, and # a wrong guess deletes valid multi-GB caches across the estate. Loss of platform content is # caught by the fingerprint instead, which needs no assumptions about the layout. if ($manifest) { return & $result $true '' $markerState } if (Test-Path -LiteralPath (Join-Path $Path 'lastused')) { $hasContent = @(Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'lastused' -and $_.Name -ne '.albuild-complete' }).Count -gt 0 if ($hasContent) { return & $result $true '' $markerState } return & $result $false 'only a lastused marker is present, with no content' $markerState } return & $result $false 'neither manifest.json nor a lastused marker is present' $markerState } # Country artifact: the image reads manifest.json, so it must be there and parseable. if (-not $manifest) { return & $result $false 'manifest.json is missing' $markerState } # The manifest names its own database and license file - verify exactly those rather than guessing. foreach ($field in 'database', 'licenseFile') { if ($manifest.PSObject.Properties.Name -notcontains $field) { continue } $named = "$($manifest.$field)" if (-not $named) { continue } if (-not (Test-Path -LiteralPath (Join-Path $Path $named))) { return & $result $false "the file '$named' named by manifest.json ($field) is missing" $markerState } } # The one folder requirement that is EVIDENCE-BASED rather than assumed: a localised artifact carries # its compiled country apps in 'Applications.<country>', and its absence is precisely the defect that # took three product pipelines down - the container silently falls back to the source packages under # C:\Applications, which do not compile for a localised build. w1 legitimately has no such folder, so # this is required only when the manifest names a country other than W1. Nothing else about the # country layout is asserted; the fingerprint covers the rest without guessing. $country = if ($manifest.PSObject.Properties.Name -contains 'country') { "$($manifest.country)" } else { '' } if ($country -and $country -notmatch '^(?i)w1$') { $appsFolder = Join-Path $Path "Applications.$country" if (-not (Test-Path -LiteralPath $appsFolder)) { return & $result $false ("the 'Applications.$country' folder is missing - the container would fall back to the " + 'source packages under C:\Applications, which are recompiled on publish and fail for a localised artifact') $markerState } if (-not (Get-ChildItem -LiteralPath $appsFolder -File -Force -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1)) { return & $result $false "the 'Applications.$country' folder is empty" $markerState } } return & $result $true '' $markerState } |