Modules/businessdev.ALbuild.Feeds/Public/Select-BcArtifactForDependencies.ps1

function Select-BcArtifactForDependencies {
    <#
    .SYNOPSIS
        Picks the newest Business Central artifact whose build every project's dependencies still
        resolve against - stepping back through minor versions when an ISV has not yet shipped
        packages for the latest BC (dependency-aware selection with downgrade-on-conflict).
 
    .DESCRIPTION
        For 'Select = Latest' pipelines only. Enumerates the latest artifact and up to -MaxStepsBack
        earlier minor versions (crossing a major boundary to the previous major's latest minor when a
        minor reaches x.0), and - newest first - dry-runs Resolve-BcDependencies (-SkipDownload) for
        every project against that build. The first version where *all* projects resolve is chosen.
        If it is older than the latest, 'Downgraded' is set and 'ConflictReason' explains why the
        latest was skipped (the caller emits the pipeline warning). Throws if nothing within the
        window resolves. Because it uses an explicit target build and a synthetic Microsoft baseline,
        no container is needed - the choice is made before the (single) container is created.
 
        A specific -Version pins the choice: enumeration/downgrade only runs when no explicit version
        is given.
 
    .PARAMETER ProjectFolder
        The AL project folder(s) to resolve. Every one must resolve for a version to be chosen.
 
    .PARAMETER Type
        Artifact type (Sandbox or OnPrem). Default 'Sandbox'.
 
    .PARAMETER Country
        Artifact country/localisation.
 
    .PARAMETER Version
        Explicit version/prefix. When given, that version is returned as-is (no downgrade search).
 
    .PARAMETER MaxStepsBack
        Maximum number of minor versions to step back from the latest. Default 5.
 
    .PARAMETER Feeds
        Feed provider objects for the resolver (as Resolve-BcDependencies -Feeds). When omitted the
        resolver auto-loads the project/registered feeds.
 
    .PARAMETER InstalledApps
        Baseline apps pinned in every dry-run - the already-staged external dependencies
        (url/local/universal) so the resolver treats them as satisfied instead of fetching from a feed.
 
    .OUTPUTS
        PSCustomObject: ArtifactUrl, Version, LatestVersion, Downgraded, StepsBack, ConflictReason.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Selects and returns an artifact URL; it does not change persistent system state.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '',
        Justification = 'Selects a build for the full set of a project''s dependencies; the plural is intentional.')]
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]] $ProjectFolder,
        [string] $Type = 'Sandbox',
        [string] $Country = '',
        [string] $Version = '',
        [int] $MaxStepsBack = 5,
        [object[]] $Feeds,
        [object[]] $InstalledApps = @()
    )

    $findArgs = @{ Type = $Type }
    if ($Country) { $findArgs['Country'] = $Country }

    # An explicit version pins the choice - no enumeration/downgrade.
    if ($Version) {
        $url = Find-BcArtifactUrl @findArgs -Version $Version -Select Latest
        if (-not $url) { throw "No '$Type' artifact found for version '$Version' (country '$Country')." }
        return [PSCustomObject]@{ ArtifactUrl = $url; Version = ($url.Split('/')[4]); LatestVersion = ($url.Split('/')[4]); Downgraded = $false; StepsBack = 0; ConflictReason = '' }
    }

    # 1. Build the descending candidate list: latest, then up to -MaxStepsBack earlier minors,
    # crossing to the previous major's latest minor when a minor reaches x.0.
    $latestUrl = Find-BcArtifactUrl @findArgs -Select Latest
    if (-not $latestUrl) { throw "No '$Type' artifact found for country '$Country'." }
    $latestVersion = [version]($latestUrl.Split('/')[4])

    $candidates = [System.Collections.Generic.List[object]]::new()
    $candidates.Add([PSCustomObject]@{ Version = $latestVersion; Url = $latestUrl })
    $curMajor = $latestVersion.Major
    $curMinor = $latestVersion.Minor
    for ($step = 1; $step -le $MaxStepsBack; $step++) {
        if ($curMinor -gt 0) {
            $curMinor--
        }
        else {
            $prevUrl = Find-BcArtifactUrl @findArgs -Version "$($curMajor - 1)" -Select Latest
            if (-not $prevUrl) { break }
            $prevVer = [version]($prevUrl.Split('/')[4])
            $curMajor = $prevVer.Major
            $curMinor = $prevVer.Minor
        }
        $url = Find-BcArtifactUrl @findArgs -Version "$curMajor.$curMinor" -Select Latest
        if (-not $url) { continue }
        $candidates.Add([PSCustomObject]@{ Version = [version]($url.Split('/')[4]); Url = $url })
    }

    # 2. Dry-run each candidate newest-first; the first where every project resolves wins.
    $firstConflict = ''
    $tempLock = Join-Path ([System.IO.Path]::GetTempPath()) ("albuild-dryrun-" + [guid]::NewGuid().ToString('N') + '.lock.json')
    try {
        for ($i = 0; $i -lt $candidates.Count; $i++) {
            $cand = $candidates[$i]
            $ver = "$($cand.Version)"
            $ok = $true
            $why = ''
            foreach ($proj in $ProjectFolder) {
                $resolveArgs = @{
                    ProjectFolder     = $proj
                    TargetApplication = $ver
                    TargetPlatform    = $ver
                    InstalledApps     = $InstalledApps
                    Select            = 'Latest'
                    SkipDownload      = $true
                    LockFilePath      = $tempLock
                }
                if ($Feeds -and $Feeds.Count -gt 0) { $resolveArgs['Feeds'] = $Feeds }
                try { Resolve-BcDependencies @resolveArgs -ErrorAction Stop | Out-Null }
                catch { $ok = $false; $why = "$(Split-Path -Path $proj -Leaf): $($_.Exception.Message)"; break }
            }

            if ($ok) {
                return [PSCustomObject]@{
                    ArtifactUrl    = $cand.Url
                    Version        = $ver
                    LatestVersion  = "$latestVersion"
                    Downgraded     = ($cand.Version -ne $latestVersion)
                    StepsBack      = $i
                    ConflictReason = $firstConflict
                }
            }

            if ($i -eq 0) { $firstConflict = $why }
            Write-ALbuildLog -Level Warning "BC ${ver}: dependencies did not resolve ($why). Trying an earlier version..."
        }
    }
    finally { Remove-Item -LiteralPath $tempLock -Force -ErrorAction SilentlyContinue }

    throw "No Business Central version within $MaxStepsBack minor version(s) of $latestVersion resolves every project's dependencies. Latest conflict: $firstConflict"
}