Modules/businessdev.ALbuild.RuntimePackages/Public/Get-BcRuntimeWorkSet.ps1
|
function Get-BcRuntimeWorkSet { <# .SYNOPSIS Plans outstanding runtime-package work, grouped by BC platform version across all products. .DESCRIPTION Turns "which (product, app version, platform version) runtime packages are still missing?" into an ordered, sliceable plan. It is a pure function - no feed, blob or artifact calls - so the whole planning policy is unit-testable and a run can be inspected before a single container starts. WHY GROUPING BY PLATFORM VERSION IS THE WHOLE POINT A runtime package is produced server-side by a running BC service tier, and Microsoft only guarantees it on the exact platform version it was produced on - so the (product x platform) matrix is irreducible. What IS reducible is the container count. Measured on build 27197, provisioning a container costs 238-564 s depending on the major, while the value-adding work per product is a constant ~80 s. Iterating platform-versions-per-product, as one pipeline per product does, pays that provisioning cost once per product; iterating products-per-platform-version pays it once, full stop. For a 12-product catalogue over ~192 versions that is ~2 300 container starts against 192. It also removes a failure mode rather than working around it: several products depend on other products in the same catalogue (Extension License, 365 business API). Per-product pipelines had to fetch those dependencies' runtime packages from blob storage and SKIP the platform version when they were not there yet. Built in one container in dependency order, the dependency is simply present - it was built moments earlier, in the same container. LANES A product release invalidates the entire matrix at once, and a full rebuild is hours of work no matter how it is scheduled. Customers, however, pull the newest majors first. -FastLaneMinMajor splits the plan so those versions are built and published first, and the long tail follows behind without holding the release up. SLICES Each slice is the unit of work for one agent job. -MaxVersionsPerSlice bounds how long a single job runs, which matters because a job holds one of the organisation's few parallel slots for its whole lifetime - and because a job that dies loses only its in-flight version, not the run. .PARAMETER Product The catalogue, as objects with: Name - display name (used in logs and the per-app result). AppId - app.json 'id'; keys the blob layout <appId>/<platformVersion>/<file>. AppVersion - the released version to build runtime packages for. MinimumVersion - app.json 'application'; platform versions below this MAJOR are not applicable. DependsOn - OPTIONAL names of other catalogue products this one needs installed first. Projects - OPTIONAL, passed through untouched for the worker. Country - OPTIONAL, passed through untouched. .PARAMETER PlatformArtifact Candidate platform versions as objects with PlatformVersion, ArtifactUrl and Country. Country is part of the grouping key, not decoration: a BC artifact carries ONE localisation, so a container built from the 'de' artifact cannot produce a runtime package for a product that ships against 'w1'. In the current catalogue Banking, ERiC and Sanction Screen build against 'de' and the rest against 'w1', so a platform version yields one container per country in use - still one per country instead of one per product. .PARAMETER ExistingPackage Relative blob paths that already exist ('<appId>/<platformVersion>/<file>.app'). A (product, platform version) pair whose file is already there is reported as skipped rather than rebuilt, which is what makes a re-run after a failure cheap and a scheduled sweep close to free. .PARAMETER SkipPlatformVersion Platform versions to exclude outright - Microsoft occasionally publishes an artifact that cannot produce a working container, and one bad version must not stall the catalogue. .PARAMETER FastLaneMinMajor Platform majors >= this go into the FastLane; everything else into the LongTail. 0 (default) disables the split and puts everything in a single 'All' lane. .PARAMETER Lane Which lane to return: All (default), FastLane or LongTail. .PARAMETER MaxVersionsPerSlice Maximum platform versions per slice. Default 12. .EXAMPLE $plan = Get-BcRuntimeWorkSet -Product $catalogue -PlatformArtifact $artifacts ` -ExistingPackage $blobNames -FastLaneMinMajor 27 -Lane FastLane $plan.Slices | ForEach-Object { Invoke-BcRuntimeFactory -WorkItem $_.Items ... } .OUTPUTS PSCustomObject with Slices (Index, Lane, Items[]) and Summary (per-product counts). #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object[]] $Product, [Parameter(Mandatory)] [ValidateNotNull()] [object[]] $PlatformArtifact, [string[]] $ExistingPackage = @(), [string[]] $SkipPlatformVersion = @(), [ValidateRange(0, [int]::MaxValue)] [int] $FastLaneMinMajor = 0, [ValidateSet('All', 'FastLane', 'LongTail')] [string] $Lane = 'All', [ValidateRange(1, [int]::MaxValue)] [int] $MaxVersionsPerSlice = 12 ) $ordered = Get-BcRuntimeProductOrder -Product $Product # Case-insensitive: blob listings and app.json ids differ in casing across products. $existing = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($p in $ExistingPackage) { if (-not [string]::IsNullOrWhiteSpace($p)) { [void]$existing.Add(($p -replace '\\', '/').Trim('/')) } } $skip = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($s in $SkipPlatformVersion) { if (-not [string]::IsNullOrWhiteSpace($s)) { [void]$skip.Add($s.Trim()) } } # Newest platform version first, in both lanes: whichever lane a version lands in, the versions # customers are most likely to ask for should exist soonest. $candidates = @($PlatformArtifact | Where-Object { -not $skip.Contains("$($_.PlatformVersion)") } | Sort-Object -Property @{ Expression = { [version](ConvertTo-BcVersion $_.PlatformVersion) }; Descending = $true }) $summary = [System.Collections.Generic.List[object]]::new() $counts = @{} foreach ($p in $ordered) { $counts[$p.Name] = [PSCustomObject]@{ Name = $p.Name; Planned = 0; SkippedExisting = 0; NotApplicable = 0 } } $items = [System.Collections.Generic.List[object]]::new() foreach ($artifact in $candidates) { $pv = "$($artifact.PlatformVersion)" $artifactCountry = "$(Get-BcRuntimeProperty -InputObject $artifact -Name 'Country' -Default '')" $pvMajor = ([version](ConvertTo-BcVersion $pv)).Major # $itemLane, NOT $lane: PowerShell variable names are case-insensitive, so a local '$lane' IS the # '$Lane' parameter. Assigning it would overwrite the requested lane on the first iteration and # the filter below would then always match - the fast lane would silently build the whole matrix. $itemLane = if ($FastLaneMinMajor -le 0) { 'All' } elseif ($pvMajor -ge $FastLaneMinMajor) { 'FastLane' } else { 'LongTail' } if ($Lane -ne 'All' -and $itemLane -ne $Lane) { continue } $pending = [System.Collections.Generic.List[object]]::new() foreach ($p in $ordered) { # A BC artifact carries one localisation. Only products built against THIS artifact's country # can be produced in a container started from it - the rest get their own container from # their own country's artifact. $productCountry = "$(Get-BcRuntimeProperty -InputObject $p -Name 'Country' -Default '')" if ($artifactCountry -and $productCountry -and $productCountry -ne $artifactCountry) { continue } $minimum = "$(Get-BcRuntimeProperty -InputObject $p -Name 'MinimumVersion' -Default '')" $minMajor = if ([string]::IsNullOrWhiteSpace($minimum)) { 0 } else { ([version](ConvertTo-BcVersion $minimum)).Major } if ($pvMajor -lt $minMajor) { $counts[$p.Name].NotApplicable++; continue } $publisher = "$(Get-BcRuntimeProperty -InputObject $p -Name 'Publisher' -Default '')" $file = Get-BcRuntimePackageFileName -Publisher $publisher -Name $p.Name -Version "$($p.AppVersion)" $key = "$($p.AppId)/$pv/$file" if ($existing.Contains($key)) { $counts[$p.Name].SkippedExisting++; continue } $counts[$p.Name].Planned++ $pending.Add([PSCustomObject]@{ Name = $p.Name Publisher = $publisher AppId = $p.AppId AppVersion = "$($p.AppVersion)" Country = Get-BcRuntimeProperty -InputObject $p -Name 'Country' -Default '' Projects = Get-BcRuntimeProperty -InputObject $p -Name 'Projects' -Default @() # Transitive chain, in install order. The worker installs exactly this before the # product and tears it down again afterwards. DependencyChain = @(Get-BcRuntimeDependencyChain -Product $ordered -Name $p.Name) TargetPath = "$($p.AppId)/$pv" FileName = $file }) } # A platform version with nothing pending is not worth a container. if ($pending.Count -eq 0) { continue } $items.Add([PSCustomObject]@{ PlatformVersion = $pv Country = $artifactCountry ArtifactUrl = $artifact.ArtifactUrl Lane = $itemLane Products = $pending.ToArray() }) } $slices = [System.Collections.Generic.List[object]]::new() for ($i = 0; $i -lt $items.Count; $i += $MaxVersionsPerSlice) { $end = [Math]::Min($i + $MaxVersionsPerSlice, $items.Count) - 1 $chunk = @($items[$i..$end]) $slices.Add([PSCustomObject]@{ Index = $slices.Count + 1 Lane = $chunk[0].Lane Items = $chunk }) } foreach ($p in $ordered) { $summary.Add($counts[$p.Name]) } return [PSCustomObject]@{ Slices = $slices.ToArray() Summary = $summary.ToArray() TotalVersions = $items.Count TotalPackages = @($items | ForEach-Object { $_.Products }).Count Lane = $Lane } } |