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 }) } # Read each project's identity so intra-repo dependencies (e.g. an app depending on another app # in the same repo) are pinned as a satisfied baseline in the dry-run - they are produced by the # build, not fetched from a feed, so without this the dry-run fails with "no compatible package". $projectApps = foreach ($proj in $ProjectFolder) { # Use [IO.Path]::Combine, not Join-Path: Join-Path resolves the leading drive qualifier and # throws 'Cannot find drive' when the path names a drive that does not exist on this host (e.g. # a Windows 'C:\...' path evaluated on a Linux agent). Combine is a pure string join, so the # subsequent Test-Path simply reports the path as absent instead of failing cross-platform. $appJsonPath = [System.IO.Path]::Combine($proj, 'app.json') if (Test-Path -LiteralPath $appJsonPath) { $aj = Get-Content -LiteralPath $appJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json $ajId = if ($aj.PSObject.Properties['id']) { "$($aj.id)" } else { '' } $ajVer = if ($aj.PSObject.Properties['version']) { "$($aj.version)" } else { '1.0.0.0' } [PSCustomObject]@{ Folder = $proj; Id = $ajId; Version = $ajVer } } } $projectApps = @($projectApps) # 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) { # Pin the *other* repo apps as satisfied (built locally, not from a feed). $siblings = @($projectApps | Where-Object { $_.Folder -ne $proj -and $_.Id } | ForEach-Object { [PSCustomObject]@{ Id = $_.Id; Version = $_.Version } }) $resolveArgs = @{ ProjectFolder = $proj TargetApplication = $ver TargetPlatform = $ver InstalledApps = @($InstalledApps + $siblings) 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" } |