Publish/Publish-BcAppToDevEndpoint.ps1

 <#
 .Synopsis
  Publish App to a NAV/BC Instance via Dev Endpoint
 .Parameter appFile
  Path of the app you want to publish
 .Parameter syncMode
  Specify Add, Clean or Development based on how you want to synchronize the database schema. Default is Add
 .Parameter credential
  Specify the credentials for the admin user if you use DevEndpoint and authentication is set to UserPassword
 .Example
  Publish-BcAppDevEndpoint -devServerUrl "https://navdev.smartcloud.com.ua:7149/localizationapps" -devAuthType "Windows" -appFile "D:\temp-so\SMART business_SMART AL Dev Tools_1.0.0.0.app"
#>

function Publish-BcAppToDevEndpoint {
    Param (
        [string] $devServerUrl = "",
        [switch] $sslVerificationDisabled,
        [string] $devAuthType = "UserPassword",
        [Parameter(Mandatory=$true, ParameterSetName="AppFile")]
        $appFile,
        [Parameter(Mandatory=$true, ParameterSetName="AppFilter")]
        $appPath,
        [Parameter(Mandatory=$true, ParameterSetName="AppFilter")]
        $appNames,
        [Parameter(Mandatory=$false)]
        [ValidateSet('Add','Clean','Development','ForceSync')]
        [string] $syncMode,
        [Parameter(Mandatory=$false)]
        [ValidateSet('Default','Ignore','Strict')]
        [string] $dependencyPublishingOption,
        [int] $timeoutMinutes = 7,
        [Parameter(Mandatory=$false)]
        [string] $tenant = "default",
        [Hashtable] $bcAuthContext,
        [string] $environment,
        [pscredential] $credential    )


    if (!$bcAuthContext -and !$credential) {
        Write-Host "##vso[task.logissue type=error;]Authentication token not valid!"
        Write-Error "##vso[task.complete result=Failed;]"
    }
    
    Add-Type -AssemblyName System.Net.Http

    if ($appPath -and $appNames) {
        $filteredAppPaths = $appNames | splitToArray | findAppByNameInFolder($appPath)

        Write-Host "Found apps in $appPath using filter '$appNames':"
        $filteredAppPaths | Foreach-Object { $_.FullName } 

        $appFile = $filteredAppPaths
    }

    $appFolder = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString())
    $appFiles = CopyAppFilesToFolder -appFiles $appFile -folder $appFolder
    $force = $true

    $successCounter = 0
    $failCounter = 0

    try {
        if ($appFolder) {
            $appFiles = @(Sort-AppFilesByDependencies -appFiles $appFiles -WarningAction SilentlyContinue)
        }
        $appFiles | Where-Object { $_ } | ForEach-Object {
            try {

                $appFile = $_

                if ($bcAuthContext) {
                    $bcAuthContext = Renew-BcAuthContext -bcAuthContext $bcAuthContext
                }

                if ($environment) {
                    $devServerUrl = "$($bcContainerHelperConfig.apiBaseUrl.TrimEnd('/'))/v2.0/$environment"
                    $tenant = ""
                }
        
                $handler = New-Object System.Net.Http.HttpClientHandler
                if ($bcAuthContext) {
                    $HttpClient = [System.Net.Http.HttpClient]::new($handler)
                    $HttpClient.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", $bcAuthContext.AccessToken)
                } else {
                    if ($devAuthType -eq "Windows") {
                        $handler.UseDefaultCredentials = $true
                    }
                    $HttpClient = [System.Net.Http.HttpClient]::new($handler)
                    if ($devAuthType -eq "UserPassword") {
                        if (!($credential)) {
                            throw "You need to specify credentials when you are not using Windows Authentication"
                        }
                        $pair = ("$($Credential.UserName):"+[System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password)))
                        $bytes = [System.Text.Encoding]::ASCII.GetBytes($pair)
                        $base64 = [System.Convert]::ToBase64String($bytes)
                        $HttpClient.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Basic", $base64);
                    }
                }

                if ($timeoutMinutes -gt 0) {
                    $HttpClient.Timeout = [System.TimeSpan]::FromMinutes($timeoutMinutes);
                } else {
                    $HttpClient.Timeout = [System.Threading.Timeout]::InfiniteTimeSpan                
                }
                $HttpClient.DefaultRequestHeaders.ExpectContinue = $false

                if ($sslVerificationDisabled) {
                    if (-not ([System.Management.Automation.PSTypeName]"SslVerification").Type)
                    {
                        Add-Type -TypeDefinition "
                            using System.Net.Security;
                            using System.Security.Cryptography.X509Certificates;
                            public static class SslVerification
                            {
                                private static bool ValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { return true; }
                                public static void Disable() { System.Net.ServicePointManager.ServerCertificateValidationCallback = ValidationCallback; }
                                public static void Enable() { System.Net.ServicePointManager.ServerCertificateValidationCallback = null; }
                            }"

                    }
                    Write-Host "Disabling SSL Verification"
                    [SslVerification]::Disable()
                }


                $schemaUpdateMode = "synchronize"
                if ($syncMode -eq "Clean") {
                    $schemaUpdateMode = "recreate";
                }
                elseif ($syncMode -eq "ForceSync") {
                    $schemaUpdateMode = "forcesync"
                }
                $url = "$devServerUrl/dev/apps?SchemaUpdateMode=$schemaUpdateMode"
                if ($tenant) {
                    $url += "&tenant=$tenant"
                }
                if ($dependencyPublishingOption -eq "Ignore" -or $dependencyPublishingOption -eq "Strict") {
                    $url += "&dependencyPublishingOption=$dependencyPublishingOption"
                }
                        
                $appName = [System.IO.Path]::GetFileName($appFile)
                        
                $multipartContent = [System.Net.Http.MultipartFormDataContent]::new()
                $FileStream = [System.IO.FileStream]::new($appFile, [System.IO.FileMode]::Open)
                try {
                    $fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
                    $fileHeader.Name = "$AppName"
                    $fileHeader.FileName = "$appName"
                    $fileHeader.FileNameStar = "$appName"
                    $fileContent = [System.Net.Http.StreamContent]::new($FileStream)
                    $fileContent.Headers.ContentDisposition = $fileHeader
                    $multipartContent.Add($fileContent)
                    Write-Host "Publishing $appName to $url"
                    $result = $HttpClient.PostAsync($url, $multipartContent).GetAwaiter().GetResult()
                    if (!$result.IsSuccessStatusCode) {
                        $message = "Status Code $($result.StatusCode) : $($result.ReasonPhrase)"
                        try {
                            $resultMsg = $result.Content.ReadAsStringAsync().Result
                            try {
                                $json = $resultMsg | ConvertFrom-Json
                                $message += "`n$($json.Message)"
                            }
                            catch {
                                $message += "`n$resultMsg"
                            }
                        }
                        catch {}
                        Write-Host -ForegroundColor Red $message
    
                        $failCounter += 1
                    } else {
                        $successCounter += 1
                        Write-Host -ForegroundColor Green "App $([System.IO.Path]::GetFileName($appFile)) successfully published"
                    }
                }
                catch  {
                         Write-Host "Error Message: " $_.Exception.Message
    
                         $failCounter += 1
                    }
                finally {
                    $FileStream.Close()
                }
                    
                if ($sslverificationdisabled) {
                    Write-Host "Re-enabling SSL Verification"
                    [SslVerification]::Enable()
                }
            }
            catch {
                $failCounter += 1
    
                Write-Host "App $([System.IO.Path]::GetFileName($appFile)) not published: $_"
            }
        }
    }
    finally {
        Remove-Item $appFolder -Recurse -Force
    }

    Write-Host "$successCounter apps published successfully"
    if ($failCounter -gt 0) {
        Write-Host "##vso[task.complete result=SucceededWithIssues ;]"
        Write-Host "##vso[task.logissue type=warning;]$failCounter apps not published"
    }
}

Write-Verbose "Function imported: Publish-BcAppToDevEndpoint"