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.
 
    .OUTPUTS
        PSCustomObject: MainApp / LibraryApps / Excluded, each element having File, Id, Name, Publisher.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path
    )

    if (-not (Test-Path -LiteralPath $Path)) { throw "App path '$Path' does not exist." }
    $item = Get-Item -LiteralPath $Path
    $files = @()
    if ($item.PSIsContainer) {
        $files = @(Get-ChildItem -LiteralPath $item.FullName -Filter '*.app' -File -Recurse -ErrorAction SilentlyContinue | Sort-Object FullName)
    }
    elseif ($item.Extension -ieq '.app') { $files = @($item) }
    else { throw "App path '$Path' is neither a folder nor a .app file." }
    if (@($files).Count -eq 0) { throw "No .app files were found under '$Path'." }

    $apps = foreach ($f in $files) {
        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 }
    }
    $apps = @($apps)

    $excluded = @($apps | Where-Object { $_.IsTest })
    foreach ($x in $excluded) { Write-ALbuildLog "Excluding test app '$($x.Name)' ($([System.IO.Path]::GetFileName($x.File))) - test apps are never published to Marketplace." }

    $candidates = @($apps | Where-Object { -not $_.IsTest })
    if ($candidates.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 included app depends on (top of the dependency graph); libraries = rest.
    $dependedOn = @{}
    foreach ($c in $candidates) { foreach ($d in $c.DependencyIds) { if ($d) { $dependedOn[$d] = $true } } }
    $roots = @($candidates | Where-Object { -not $dependedOn.ContainsKey($_.Id.ToLowerInvariant()) })
    if ($roots.Count -eq 0) { $roots = $candidates }   # 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, or set the product name explicitly, if that is wrong."
    }
    $main = $roots[0]
    $libraries = @($candidates | Where-Object { $_.File -ne $main.File })

    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 }
}