Modules/businessdev.ALbuild.Marketplace/Private/Resolve-BcMarketplaceAppSet.ps1
|
function Resolve-BcMarketplaceAppSet { <# .SYNOPSIS Resolves the set of .app files to submit to Marketplace from a path, excluding test apps. .DESCRIPTION Marketplace/AppSource submissions take a MAIN app plus its dependency (library) apps, and a TEST app must NEVER be published. Release artifacts, however, contain a folder of built .app files whose names carry the version - and often the test app too. This turns a path (a folder or a single .app) into { MainApp; LibraryApps; Excluded } so the caller need only point at the output folder: * reads each .app's manifest on the host (Expand-BcAppFile - no container needed); * EXCLUDES test apps (manifest Target='Test', or a dependency on a Microsoft test/assert library - the same signal AL uses); * picks the MAIN app as the one no other included app depends on (the top of the dependency graph); the remaining non-test apps are its libraries. .PARAMETER Path A folder containing built .app files, or a single .app file. When -LibraryPath is NOT given this must hold the main app AND its libraries; when -LibraryPath IS given it holds only the main app (the top of the graph if several non-test apps are present). .PARAMETER LibraryPath Optional additional folder(s) or .app file(s) whose (non-test) apps are ALL treated as library apps - for offers whose main app and library app are built by different pipelines/repositories and therefore arrive as separate release artifacts (e.g. Address Validation + Extension License). When given, libraries come exclusively from here, not from -Path. .OUTPUTS PSCustomObject: MainApp / LibraryApps / Excluded, each element having File, Id, Name, Publisher. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path, [string[]] $LibraryPath = @() ) # Read a folder-or-.app path into app descriptor objects (with IsTest classification). Returns @(). $readApps = { param([string] $p) if (-not (Test-Path -LiteralPath $p)) { throw "App path '$p' does not exist." } $item = Get-Item -LiteralPath $p $found = @() if ($item.PSIsContainer) { $found = @(Get-ChildItem -LiteralPath $item.FullName -Filter '*.app' -File -Recurse -ErrorAction SilentlyContinue | Sort-Object FullName) } elseif ($item.Extension -ieq '.app') { $found = @($item) } else { throw "App path '$p' is neither a folder nor a .app file." } if (@($found).Count -eq 0) { throw "No .app files were found under '$p'." } $result = foreach ($f in $found) { try { $info = Expand-BcAppFile -Path $f.FullName } catch { Write-ALbuildLog -Level Warning "Skipping '$($f.Name)': could not read its manifest ($($_.Exception.Message))."; continue } # Test app: manifest Target='Test', or a dependency on a Microsoft test/assert library. $target = '' try { $appNode = $info.Manifest.SelectSingleNode("//*[local-name()='App']"); if ($appNode) { $target = "$($appNode.GetAttribute('Target'))" } } catch { Write-Verbose "Could not read the App Target attribute from '$($f.Name)': $($_.Exception.Message)" } $isTest = ($target -ieq 'Test') if (-not $isTest) { foreach ($dep in @($info.Dependencies)) { if ("$($dep.Publisher)" -eq 'Microsoft' -and ("$($dep.Name)" -like '*Test*' -or "$($dep.Name)" -like '*Assert*')) { $isTest = $true; break } } } [PSCustomObject]@{ File = $f.FullName; Id = "$($info.Id)"; Name = "$($info.Name)"; Publisher = "$($info.Publisher)"; DependencyIds = @($info.Dependencies | ForEach-Object { "$($_.Id)".ToLowerInvariant() }); IsTest = $isTest } } return @($result) } $excluded = @() # --- Main app (and, without -LibraryPath, its libraries) come from -Path --------------------- $mainApps = @(& $readApps $Path) $excluded += @($mainApps | Where-Object { $_.IsTest }) $mainCandidates = @($mainApps | Where-Object { -not $_.IsTest }) if ($mainCandidates.Count -eq 0) { throw "All .app files under '$Path' are test apps; there is nothing to publish to Marketplace." } # Main app = the one no OTHER candidate depends on (top of the dependency graph). $dependedOn = @{} foreach ($c in $mainCandidates) { foreach ($d in $c.DependencyIds) { if ($d) { $dependedOn[$d] = $true } } } $roots = @($mainCandidates | Where-Object { -not $dependedOn.ContainsKey($_.Id.ToLowerInvariant()) }) if ($roots.Count -eq 0) { $roots = $mainCandidates } # cyclic/edge case: fall back to all if ($roots.Count -gt 1) { Write-ALbuildLog -Level Warning "Found $($roots.Count) independent apps with no dependant ($(($roots | ForEach-Object { $_.Name }) -join ', ')); submitting '$($roots[0].Name)' as the main app. Point the task at a single product's output, set -LibraryPath, or set the product name explicitly, if that is wrong." } $main = $roots[0] # --- Library apps ---------------------------------------------------------------------------- if (@($LibraryPath).Count -gt 0) { # Libraries arrive as separate artifacts (different repo/pipeline): take every non-test app from # the library path(s), and drop the main app itself if a library folder happens to include it. $libApps = @() foreach ($lp in @($LibraryPath)) { $libApps += @(& $readApps $lp) } $excluded += @($libApps | Where-Object { $_.IsTest }) $libraries = @($libApps | Where-Object { -not $_.IsTest -and $_.File -ne $main.File -and $_.Id -ne $main.Id }) } else { # Single-folder case: everything else under -Path is a library. $libraries = @($mainCandidates | Where-Object { $_.File -ne $main.File }) } foreach ($x in $excluded) { Write-ALbuildLog "Excluding test app '$($x.Name)' ($([System.IO.Path]::GetFileName($x.File))) - test apps are never published to Marketplace." } Write-ALbuildLog "Marketplace app set: main '$($main.Name)' ($([System.IO.Path]::GetFileName($main.File)))$(if ($libraries.Count) { "; $($libraries.Count) library app(s): $(($libraries | ForEach-Object { $_.Name }) -join ', ')" })." [PSCustomObject]@{ MainApp = $main; LibraryApps = $libraries; Excluded = $excluded } } |