Modules/businessdev.ALbuild.Containers/Private/Repair-BcArtifactCache.ps1
|
function Repair-BcArtifactCache { <# .SYNOPSIS Removes an incomplete/corrupt BC artifact from the shared cache so the container re-downloads it. .DESCRIPTION The BC container downloads its artifact into the mounted cache (host BcArtifactCacheFolder -> C:\dl) on first run and reuses it thereafter. An interrupted download - a killed container, an agent restart, a full disk, a dropped network - leaves the artifact folder present but INCOMPLETE (e.g. without 'manifest.json'). The image then finds the folder, assumes the artifact is cached, skips the re-download, and exits immediately with "Cannot find path 'C:\dl\<type>\<version>\<country>\manifest.json'". This validates the cached app (country) and platform artifacts for the given URL and deletes any that are incomplete, so the next container start performs a clean re-download. The judgement is made by Test-BcArtifactFolderIntact: a fingerprinted folder is re-counted against what was recorded, anything else is checked structurally. The check used to be "manifest.json parses", and that was not enough. A cache existed on a build agent with a perfectly valid manifest and database but WITHOUT its Applications.<country> and Extensions folders. It passed validation, the container started, and the failure surfaced minutes later as AL compiler errors inside the test-toolkit install - because the toolkit had fallen back to the source packages under C:\Applications. Three product pipelines were red for a day. A folder that does not exist is fine (nothing cached yet - it will be fetched). .PARAMETER CacheFolder The host artifact cache mounted into the container at C:\dl (BcArtifactCacheFolder). .PARAMETER ArtifactUrl The artifact URL of the form https://.../{type}/{version}/{country}. .OUTPUTS System.String[] - the cache folders that were removed (empty when everything is valid/absent). #> [CmdletBinding(SupportsShouldProcess)] [OutputType([string[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $CacheFolder, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ArtifactUrl ) if (-not (Test-Path -LiteralPath $CacheFolder)) { return @() } # Parse .../{type}/{version}/{country} (query strings / SAS tokens are ignored by AbsolutePath). try { $segments = ([uri]$ArtifactUrl).AbsolutePath.Trim('/').Split('/') } catch { Write-ALbuildLog -Level Verbose "Could not parse artifact URL '$ArtifactUrl' for cache validation; skipping."; return @() } if ($segments.Count -lt 3) { Write-ALbuildLog -Level Verbose "Artifact URL '$ArtifactUrl' is not {type}/{version}/{country}; skipping cache validation."; return @() } $type = $segments[0]; $version = $segments[1]; $country = $segments[2] $versionRoot = Join-Path (Join-Path $CacheFolder $type) $version $removed = [System.Collections.Generic.List[string]]::new() $targets = @( [PSCustomObject]@{ Folder = (Join-Path $versionRoot $country); IsPlatform = $false } [PSCustomObject]@{ Folder = (Join-Path $versionRoot 'platform'); IsPlatform = $true } ) foreach ($t in $targets) { if (-not (Test-Path -LiteralPath $t.Folder)) { continue } $check = Test-BcArtifactFolderIntact -Path $t.Folder -IsPlatform:$t.IsPlatform if ($check.IsIntact) { # A folder that is structurally sound but carries no fingerprint (downloaded by the container # image, by BcContainerHelper, or by an ALbuild before fingerprints existed) is ADOPTED rather # than re-downloaded: record what is there now, so any later loss is detected instead of # silently reused. Re-downloading multiple GB just to gain a marker would be the wrong trade. if ($check.MarkerState -ne 'Fingerprinted') { try { $inventory = Get-BcArtifactInventory -Path $t.Folder Set-Content -LiteralPath (Join-Path $t.Folder '.albuild-complete') -Encoding UTF8 -Value ( [PSCustomObject]@{ completedOn = (Get-Date -Format 'o'); files = $inventory.Files; bytes = $inventory.Bytes; adopted = $true } | ConvertTo-Json -Compress) Write-ALbuildLog -Level Verbose "Adopted the existing cached artifact at '$($t.Folder)' ($($inventory.Files) file(s)); later loss will now be detected." } catch { Write-ALbuildLog -Level Verbose "Could not fingerprint '$($t.Folder)': $($_.Exception.Message)." } } continue } Write-ALbuildLog -Level Warning "Cached BC artifact at '$($t.Folder)' is not usable - $($check.Reason). Removing it so a clean copy is fetched." if ($PSCmdlet.ShouldProcess($t.Folder, 'Remove incomplete cached BC artifact')) { try { Remove-Item -LiteralPath $t.Folder -Recurse -Force -ErrorAction Stop; $removed.Add($t.Folder) } catch { Write-ALbuildLog -Level Warning "Could not remove '$($t.Folder)': $($_.Exception.Message). A stale artifact may still cause the container to fail; clear it manually." } } } if ($removed.Count -eq 0) { Write-ALbuildLog -Level Verbose "Cached BC artifact for '$type/$version/$country' is valid (or not yet present)." } return $removed.ToArray() } |