Modules/businessdev.ALbuild.Containers/Private/Get-BcImageName.ps1

function Get-BcImageName {
    <#
    .SYNOPSIS
        Derives the local image tag for a version-specific Business Central image.
 
    .DESCRIPTION
        The image cache is keyed by what actually determines its content: the artifact (type, version,
        country) and the generic base image it was built on. Two containers built from the same artifact
        on the same base are interchangeable; anything else is not, so it must not share a tag.
 
        The base tag is part of the name on purpose. A ltsc2022 and a ltsc2025 image of the same BC
        version are different images with the same artifact, and silently reusing one for the other is
        how a container ends up failing to start on a host whose kernel does not match.
 
        Docker tags allow only lowercase letters, digits, '.', '_' and '-', so the version's dots become
        dashes and everything is lowercased.
 
    .PARAMETER ArtifactUrl
        The artifact URL, '.../<type>/<version>/<country>'.
 
    .PARAMETER BaseImage
        The generic image the version image is built from.
 
    .EXAMPLE
        Get-BcImageName -ArtifactUrl 'https://.../onprem/26.3.36158.36321/de'
        albuild-bc:onprem-26-3-36158-36321-de-ltsc2022
 
    .OUTPUTS
        System.String
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ArtifactUrl,
        [string] $BaseImage = 'mcr.microsoft.com/businesscentral:ltsc2022'
    )

    # Parse as a URI rather than splitting on '/': a naive split counts the scheme ('https:') as a path
    # segment, so 'https://host/onprem' would look like three segments and yield a nonsense tag instead
    # of an error - and a wrong-but-plausible tag silently aliases two different artifacts onto one image.
    $uri = $null
    $path = if ([uri]::TryCreate($ArtifactUrl, [System.UriKind]::Absolute, [ref]$uri)) { $uri.AbsolutePath } else { ($ArtifactUrl -split '\?')[0] }
    # The @() wraps the WHOLE pipeline: a Where-Object that yields one item collapses to a scalar, and
    # .Count on a scalar throws under Set-StrictMode.
    $parts = @(@($path.TrimEnd('/') -split '/') | Where-Object { $_ })
    if ($parts.Count -lt 3) { throw "Cannot derive an image name from '$ArtifactUrl': expected '.../<type>/<version>/<country>'." }

    $country = $parts[-1]
    $version = $parts[-2]
    $type = $parts[-3]
    $baseTag = if ($BaseImage -match ':([^:/]+)$') { $Matches[1] } else { 'latest' }

    $tag = "$type-$($version -replace '\.', '-')-$country-$baseTag".ToLowerInvariant() -replace '[^a-z0-9._-]', ''
    return "albuild-bc:$tag"
}