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()
}