AppSource/New-AppSourceSubmission.ps1

<#
 .Synopsis
  Create a new AppSource submission (submit a new version of your app for validation)
 .Description
  Returns a PSCustomObject with submission details
 .Parameter authContext
  Authentication Context from New-BcAuthContext
 .Parameter productId
  Id of the product for which you want to create a new submission
 .Parameter appFile
  Path of the main app File
 .Parameter libraryAppFiles
  An array of app files to be included as library app files. If this array consists of a single file, it will be uploaded as-is - if multiple files are provided, they will be zipped together and uploaded
 .Parameter autoPromote
  Include this switch if you want to automatically promote the submission to production / Go Live after validation/preview
 .Parameter doNotWait
  Include this switch if you do not want to wait for the submission to pass or fail (note that if you include autoPromote, the function will wait for first part of validation)
 .Parameter force
  If another submission is in progress, it will be cancelled if you include the force switch
 .Parameter silent
  Include this switch if you do not want the method to display URLs etc.
 .Parameter doNotCheckVersionNumber
  Include this switch avoid checking whether the new version number is greater than the existing version number in Partner Center
 .Example
  New-AppSourceSubmission -authContext $authContext -productId $product.Id -appFile $appFile
 .Example
  New-AppSourceSubmission -authContext $authContext -productId $product.Id -appFile $appFile -libraryAppFiles @($libraryApp1,$libraryApp2) -autoPromote -doNotWait -silent
#>

function New-AppSourceSubmission {
    Param(
        [Parameter(Mandatory=$true)]
        [HashTable] $authContext,
        [Parameter(Mandatory=$true)]
        [string] $productId,
        [Parameter(Mandatory=$false)]
        [string] $appFile = "",
        [Parameter(Mandatory=$false)]
        [string[]] $libraryAppFiles = @(),
        [switch] $autoPromote,
        [switch] $doNotWait,
        [switch] $force,
        [switch] $silent,
        [switch] $doNotCheckVersionNumber
    )

$telemetryScope = InitTelemetryScope -name $MyInvocation.InvocationName -parameterValues $PSBoundParameters -includeParameters @()
try {
    $authContext = Renew-BcAuthContext -bcAuthContext $authContext
    if ($telemetryScope) {
        if ($authContext.ClientID) {
            AddTelemetryProperty -telemetryScope $telemetryScope -key "client" -value (GetHash -str $authContext.ClientID)
        }
        AddTelemetryProperty -telemetryScope $telemetryScope -key "product" -value (GetHash -str $productId)
        AddTelemetryProperty -telemetryScope $telemetryScope -key "autoPromote" -value "$autoPromote"
    }
    
    $product = Get-AppSourceProduct -authContext $authContext -productId $productId -silent:($silent.IsPresent) -includeSetup
    if ($product) {
        if ($product.Setup.packageType -eq "Connect") {
            throw "Product $($product.Name) is a Connect App, you cannot submit an app to a Connect app"
        }
    }
    else {
        throw "No product found with ProductID=$productID with this account"
    }

    $submission = Get-AppSourceSubmission -authContext $authContext -productId $productId -silent:($silent.IsPresent)
    if ($submission) {
        if ($submission.state -eq "InProgress") {
            if ($submission.substate -eq "Failed") {
                # ignore
            }
            elseif ($force) {
                Cancel-AppSourceSubmission -authContext $authContext -productId $productId -submissionId $submission.id -silent:($silent.IsPresent)
            }
            else {
                throw "An AppSource submission is in progress. If you want to cancel an in progress submission, you need to add -force"
            }
        }
        elseif (!($submission.state -eq "Published" -and ($submission.substate -eq "ReadyToPublish" -or $submission.substate -eq "InStore"))) {
            throw "An AppSource submission already running. You cannot create a new submission, when an existing submission is in substate=$($submission.substate)"
        }
    }

    $variantID = ''
    $branchesPackage = @(Invoke-IngestionApiGetCollection -authContext $authContext -path "/products/$productId/branches/getByModule(module=Package)" -silent:($silent.IsPresent) | Where-Object { 
        $thisVariantID = ''
        if ($_.PSObject.Properties.name -eq "variantID") { $thisVariantID = $_.variantID }
        $variantID -eq $thisVariantID
    })
    if ($branchesPackage.Count -ne 1) {
        throw "Unable to locate package from Ingestion API"
    }
    $packageCurrentDraftInstanceID = $branchesPackage[0].currentDraftInstanceID
    
    $appVersionNumber = ""
    if ($appFile) {
        try {
            $tempFolder = Join-Path $env:TEMP ([Guid]::NewGuid().ToString())
            Extract-AppFileToFolder -appFilename $appFile -appFolder $tempFolder -generateAppJson
            $appJsonFile = Join-Path $tempFolder 'app.json'
            $appJson = Get-Content $appJsonFile -Encoding UTF8 | ConvertFrom-Json
            Remove-Item $tempFolder -Recurse -Force
            $appVersionNumber = [System.Version]$appJson.version
        }
        catch {
            throw "Unable to extract app file and determine version number"
        }
    }

    $tempFolder = ""
    $libraryAppFile = ""
    if ($libraryAppFiles -and ($libraryAppFiles.Count -gt 0)) {
        if ($libraryAppFiles.Count -eq 1) {
            $libraryAppFile = $libraryAppFiles[0]
        }
        else {
            $tempFolder = Join-Path $env:TEMP ([GUID]::NewGuid().ToString())
            New-Item -Path $tempFolder -ItemType Directory | Out-Null
            $libraryAppFile = Join-Path $tempFolder "$([System.IO.Path]::GetFileNameWithoutExtension($appFile)).libraries.zip"
            Compress-Archive -Path $libraryAppFiles -DestinationPath $libraryAppFile -CompressionLevel Fastest
        }
    }
    
    $packageConfigurations = @(Invoke-IngestionApiGetCollection -authContext $authContext -path "/products/$productId/packageConfigurations/getByInstanceID(instanceID=$($packageCurrentDraftInstanceID))" -silent:($silent.IsPresent))
    if ($packageConfigurations.Count -ne 1) {
        $packageConfigurations | fl | Out-Host
        throw "unable to locate package configuration"
    }
    $packageConfiguration = $packageConfigurations[0]

    0..1 | ForEach-Object {
        if ($_ -eq 0) {
            $parameterName = 'AppFile'
            $file = $appFile
            $resourceType = "Dynamics365BusinessCentralAddOnExtensionPackage"
        }
        else {
            $parameterName = 'LibraryAppFiles'
            $file = $libraryAppFile
            $resourceType = "Dynamics365BusinessCentralAddOnLibraryExtensionPackage"
        }
        if ($PSBoundParameters.ContainsKey($parameterName)) {
            $packageConfiguration.packageReferences = @($packageConfiguration.packageReferences | Where-Object { $_.type -ne $resourceType })
        }
        if ($file) {
            $body = @{
                "resourceType" = $resourceType
                "fileName" = [System.IO.Path]::GetFileName($file)
            }
            $packageUpload = Invoke-IngestionApiPost -authContext $authContext -path "/products/$productId/packages" -Body $body -silent:($silent.IsPresent)
        
            $uri = [System.Uri] $packageUpload.fileSasUri
            $storageAccountName = $uri.DnsSafeHost.Split(".")[0]
            $container = $uri.LocalPath.Substring(1).split('/')[0]
            $blobname = $uri.LocalPath.Substring(1).split('/')[1]
            $sasToken = $uri.Query

            if (!(get-command New-AzureStorageContext -ErrorAction SilentlyContinue)) {
                Set-Alias -Name New-AzureStorageContext -Value New-AzStorageContext
                Set-Alias -Name Set-AzureStorageBlobContent -Value Set-AzStorageBlobContent
            }

            $storageContext = New-AzureStorageContext -StorageAccountName $storageAccountName -SasToken $sasToken
            Set-AzureStorageBlobContent -File $file -Container $container -Blob $blobname -Context $storageContext -Force | Out-Null
        
            $packageUpload.state = "Uploaded"
            $packageUploaded = Invoke-IngestionApiPut -authContext $authContext -path "/products/$productId/packages/$($packageUpload.id)" -Body ($packageUpload | ConvertTo-HashTable) -silent:($silent.IsPresent)
            if ($packageUploaded.state -ne "Processed") {
                throw "Could not process package"
            }

            $packageConfiguration.packageReferences += @([PSCustomObject]@{
                "type" = $resourceType
                "value" = $packageUploaded.id
            })
        }
    }
    if ($tempFolder -and (Test-Path $tempFolder -PathType Container)) {
        Remove-Item $tempFolder -Recurse -Force
    }

    $result = Invoke-IngestionApiPut -authContext $authContext -path "/products/$productId/packageConfigurations/$($packageConfiguration.id)" -Body ($packageConfiguration | ConvertTo-HashTable -recurse) -silent:($silent.IsPresent)
    
    $body = [ordered]@{
        "resourceType" = "SubmissionCreationRequest"
        "targets" = @(
            [ordered]@{
                "type" = "Scope"
                "value" = "preview"
            }
        )
        "resources" = @(
            [ordered]@{
                "type" = "Package"
                "value" = $packageCurrentDraftInstanceID
            }
        )
    }
    if ($appVersionNumber) {
        $branchesProperty = @(Invoke-IngestionApiGetCollection -authContext $authContext -path "/products/$productId/branches/getByModule(module=Property)" -silent:($silent.IsPresent) | Where-Object { 
            $thisVariantID = ''
            if ($_.PSObject.Properties.name -eq "variantID") { $thisVariantID = $_.variantID }
            $variantID -eq $thisVariantID
        })
        if ($branchesProperty.Count -ne 1) {
            throw "Unable to locate properties from Ingestion API"
        }
        $propertyCurrentDraftInstanceID = $branchesProperty[0].currentDraftInstanceID

        $properties = @(Invoke-IngestionApiGetCollection -authContext $authContext -path "/products/$productId/properties/getByInstanceID(instanceID=$propertyCurrentDraftInstanceID)" -silent:($silent.IsPresent))
        if ($properties.Count -ne 1) {
            $properties | fl | Out-Host
            throw "unable to locate properties"
        }
        $property = $properties[0]
        if (!$doNotCheckVersionNumber) {
            $prevVersion = [System.Version]"0.0.0.0"
            if ([System.Version]::TryParse($property.appVersion, [ref] $prevVersion)) {
                if ($prevVersion -gt $appVersionNumber) {
                    throw "The new version number is lower than the existing version number in Partner Center"
                }
            }
        }
        $property.appVersion = $appVersionNumber.ToString()
        $result = Invoke-IngestionApiPut -authContext $authContext -path "/products/$productId/properties/$($property.id)" -Body ($property | ConvertTo-HashTable -recurse) -silent:($silent.IsPresent)
        $body.resources += @(
            [ordered]@{
                "type" = "Property"
                "value" = $propertyCurrentDraftInstanceID
            }
        )
    }
    
    $submission = Invoke-IngestionApiPost -authContext $authContext -path "/products/$productId/submissions" -Body $body -silent:($silent.IsPresent)
    
    if ($doNotWait.IsPresent -and !$autoPromote.IsPresent) {
        Write-Host -ForegroundColor Green "New AppSource submission created"
        $submission
    }
    else {
        $jobs = @{
            "Automated validation" = "NotStarted"
            "Preview Creation" = "NotStarted"
            "Publisher Signoff" = "NotStarted"
            "Certification" = "NotStarted"
            "Publish" = "NotStarted"
        }
        $promoted = $false
        $lastName = ""
        do {
            Start-Sleep -Seconds 30
            $authContext = Renew-BcAuthContext -bcAuthContext $authContext -silent

            $complete = $false
            $failed = $false
            $status = @(Invoke-IngestionApiGetCollection -authContext $authContext -path "/products/$productId/submissions/$($submission.id)/workflowdetails" -silent)
            if ($status.Count -ne 2) {
                $status | fl | Out-Host
                throw "Unexpected error when trying to get status for submission. Please consult Partner Center UI."
            }
            0..1 | ForEach-Object {
                $st = $status[$_]
                $st.workflowSteps | ForEach-Object {
                    if ($jobs."$($_.Name)" -eq $_.State) {
                        if ($_.state -eq "InProgress") {
                            Write-Host -NoNewline '.'
                        }
                        elseif ($_.state -eq "NotStarted") {
                        }
                    }
                    else {
                        if ($jobs."$($_.Name)" -eq "NotStarted") {
                            Write-Host -NoNewline $_.Name
                        }
                        if ($_.State -eq "Success") {
                            Write-Host -ForegroundColor Green ' Success'
                        }
                        elseif ($_.state -eq "InProgress") {
                            Write-Host -NoNewline '.'
                        }
                        else {
                            Write-Host -ForegroundColor Red ' Failure'
                            $failed = $true
                        }
                        $jobs."$($_.Name)" = $_.State
                    }
                }
            }
            $sm = Invoke-IngestionApiGet -authContext $authContext -path "/products/$productId/submissions/$($submission.id)" -silent
            if ($sm.state -eq "Published" -and $sm.substate -eq "ReadyToPublish") {
                if ($autoPromote.IsPresent) {
                    if (!$promoted) {
                        Promote-AppSourceSubmission -authContext $authContext -productId $productId -submissionId $submission.id -silent:($silent.IsPresent) | Out-Null
                        $promoted = $true
                        if ($doNotWait.IsPresent) {
                            $complete = $true
                        }
                    }
                }
                else {
                    $complete = $true
                }
            }
            elseif ($sm.state -eq "Published" -and $sm.substate -eq "InStore") {
                $complete = $true
            }
        } while (!$complete -and !$failed)
        
        if ($failed) {
            Write-Host -ForegroundColor Red "New AppSource submission failed"
        }
        else {
            Write-Host -ForegroundColor Green "New AppSource submission succeeded"
        }
        $sm
    }
}
catch {
    TrackException -telemetryScope $telemetryScope -errorRecord $_
    throw
}
finally {
    TrackTrace -telemetryScope $telemetryScope
}
}
Export-ModuleMember -Function New-AppSourceSubmission