Modules/businessdev.ALbuild.Marketplace/Public/Submit-BcMarketplaceApp.ps1

function Submit-BcMarketplaceApp {
    <#
    .SYNOPSIS
        Submits an AL app to Microsoft Marketplace (AppSource) end to end.
 
    .DESCRIPTION
        Orchestrates a Marketplace submission: authenticates (Partner Center and product-ingestion
        scopes), resolves the product by name, uploads the app (and library apps) and creates a
        submission, waits for validation, and optionally promotes it to live. Requires a valid
        ALbuild license.
 
    .PARAMETER ProductName
        The Marketplace product (offer) name. OPTIONAL - defaults to the main app's manifest name.
        Provide it only to override (e.g. when the offer name differs from the app name).
 
    .PARAMETER AppPath
        A folder of built .app files, or a single .app file. The main app and its library (dependency)
        apps are selected automatically and TEST apps are excluded - so a task can just point at the
        build output folder without naming a specific, version-stamped file.
 
    .PARAMETER LibraryAppPath
        Optional additional folder(s) or .app file(s) whose apps are submitted as library apps, for offers
        whose main app and library app are built by different pipelines/repositories and so arrive as
        separate release artifacts (e.g. Address Validation + its Extension License library). Test apps are
        excluded. Only valid with -AppPath.
 
    .PARAMETER AppFile
        (Legacy) An explicit main .app file. Prefer -AppPath. Ignored when -AppPath is given.
 
    .PARAMETER LibraryAppFile
        Optional dependent/library .app files (only used with the legacy -AppFile).
 
    .PARAMETER TenantId
        Azure AD tenant id for the Partner Center service principal.
 
    .PARAMETER ClientId
        Service principal client id.
 
    .PARAMETER ClientSecret
        Service principal client secret.
 
    .PARAMETER PublisherId
        Numeric publisher/Seller ID (Partner Center > Settings > Account settings > Identifiers). Required
        by the Partner Center ingestion API (sent as the 'x-ms-publisherId' header).
 
    .PARAMETER AutoPromote
        Promote to live after successful validation.
 
    .PARAMETER TimeoutMinutes
        Validation timeout. Default 30.
 
    .OUTPUTS
        PSCustomObject with ProductId, SubmissionId, State, Substate, Promoted.
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Path')]
    [OutputType([PSCustomObject])]
    param(
        [string] $ProductName,
        [Parameter(Mandatory, ParameterSetName = 'Path')] [string] $AppPath,
        [Parameter(ParameterSetName = 'Path')] [string[]] $LibraryAppPath = @(),
        [Parameter(Mandatory, ParameterSetName = 'Legacy')] [string] $AppFile,
        [Parameter(ParameterSetName = 'Legacy')] [string[]] $LibraryAppFile = @(),
        [Parameter(Mandatory)] [string] $TenantId,
        [Parameter(Mandatory)] [string] $ClientId,
        [Parameter(Mandatory)] [object] $ClientSecret,
        [Parameter(Mandatory)] [string] $PublisherId,
        [switch] $AutoPromote,
        [int] $TimeoutMinutes = 30
    )

    Assert-ALbuildLicensed -Feature 'Marketplace'

    # Resolve the apps to submit. -AppPath (folder or file) auto-selects the main app + library apps and
    # drops test apps; -AppFile is the explicit legacy path.
    if ($PSCmdlet.ParameterSetName -eq 'Path') {
        $resolveArgs = @{ Path = $AppPath }
        if (@($LibraryAppPath).Count -gt 0) { $resolveArgs['LibraryPath'] = $LibraryAppPath }
        $set = Resolve-BcMarketplaceAppSet @resolveArgs
        $AppFile = $set.MainApp.File
        $LibraryAppFile = @($set.LibraryApps | ForEach-Object { $_.File })
        if (-not $ProductName) {
            $ProductName = $set.MainApp.Name
            Write-ALbuildLog "No product name supplied; using the main app's manifest name '$ProductName'."
        }
    }
    elseif (-not $ProductName) {
        # Legacy path with no explicit name: derive it from the app's manifest.
        $ProductName = (Expand-BcAppFile -Path $AppFile).Name
        Write-ALbuildLog "No product name supplied; using the app's manifest name '$ProductName'."
    }
    if (-not $ProductName) { throw 'Could not determine the Marketplace product name (no -ProductName and no readable app manifest).' }

    # Product lookup uses the Graph product-ingestion scope; the submission uses the Partner Center scope
    # (which requires the PublisherId / x-ms-publisherId header).
    $graphAuth = New-BcMarketplaceAuthContext -TenantId $TenantId -ClientId $ClientId -ClientSecret $ClientSecret -Scope 'https://graph.microsoft.com/.default' -PublisherId $PublisherId
    $partnerAuth = New-BcMarketplaceAuthContext -TenantId $TenantId -ClientId $ClientId -ClientSecret $ClientSecret -Scope 'https://api.partner.microsoft.com/.default' -PublisherId $PublisherId

    Write-ALbuildLog "Resolving product '$ProductName' ..."
    $products = @(Get-BcMarketplaceProduct -AuthContext $graphAuth -Name $ProductName)
    if ($products.Count -eq 0) { throw "Marketplace product '$ProductName' was not found (product name = the offer 'alias')." }
    if ($products.Count -gt 1) { throw "Multiple products match '$ProductName' ($(($products | ForEach-Object { $_.alias }) -join ', ')); use a more specific name." }
    $product = $products[0]
    Write-ALbuildLog "Product '$($product.alias)' ($($product.id))."

    if (-not $PSCmdlet.ShouldProcess($product.alias, 'Create and validate Marketplace submission')) { return }

    # New-BcMarketplaceSubmission uploads the package(s), creates the preview submission, waits for
    # validation and (with -AutoPromote) promotes to live - the proven Publish-AppSourceApp flow.
    return New-BcMarketplaceSubmission -AuthContext $partnerAuth -ProductId $product.id `
        -AppFile $AppFile -LibraryAppFile $LibraryAppFile -AutoPromote:$AutoPromote -TimeoutMinutes $TimeoutMinutes
}