Publish/Publish-BcAppToDevEndpointWithRetry.ps1

<#
.SYNOPSIS
Function to publish apps to a Microsoft Dynamics 365 Business Central environment with retry capabilities in case of failure.
 
.DESCRIPTION
The `Publish-BcAppToDevEndpointWithRetry` function automates the process of publishing apps to a Microsoft Dynamics 365 Business Central development environment. It supports two authentication methods (using `bcAuthContext` or `credentials`) and allows for retrying the publishing process in case previous attempts fail. The function handles different publishing outcomes such as successful publishing, duplicate package detection, or already installed apps, and adjusts the process accordingly.
 
Authentication parameters (`bcAuthContext` or `credentials`) are used to establish a connection to the Business Central environment. During execution, the function retries publishing if not all apps were successfully installed on the first attempt, and it logs the results for further analysis.
 
After each failed attempt, the function pauses before making the next attempt until the maximum number of retries, defined by the `$maxRetries` parameter, is reached.
 
.PARAMETER appPath
The path to the directory containing the app files (.app) that need to be published. This parameter is mandatory.
 
.PARAMETER devServerUrl
The URL of the Business Central development server where the apps will be published. This parameter is mandatory.
 
.PARAMETER bcAuthContext
A hashtable containing the authentication context for Business Central. This parameter is an alternative to `credentials` and is used for OAuth2 authentication. Either `bcAuthContext` or `credentials` must be specified.
 
.PARAMETER credentials
A `PSCredential` object containing the credentials for authentication. This parameter is an alternative to `bcAuthContext` and is used for basic authentication. Either `credentials` or `bcAuthContext` must be specified.
 
.PARAMETER environment
The name of the Business Central environment where the publishing will take place. This is used together with `bcAuthContext` to specify the environment within a tenant.
 
.PARAMETER sslVerificationDisabled
A switch that disables SSL certificate verification when connecting to the server. Useful when using self-signed certificates.
 
.PARAMETER syncMode
The synchronization mode to be used during app publishing. Possible values are 'Add', 'Clean', 'Development', 'ForceSync'. The default value is 'ForceSync'.
 
.PARAMETER dependencyPublishingOption
The option to manage dependencies during app publishing. Possible values are 'Default', 'Ignore', 'Strict'. The default value is 'Ignore'.
 
.PARAMETER timeoutMinutes
The maximum time in minutes to wait for the app publishing to complete. The default value is 10 minutes.
 
.PARAMETER maxRetries
The maximum number of retry attempts in case of publishing failure. The default value is 3.
 
.PARAMETER WaitingTimeInSecond
The waiting time in seconds between publishing attempts. The default value is 90 seconds.
 
.PARAMETER Interval
The interval in seconds used for the delay between checks of the publishing results. The default value is 30 seconds.
 
.PARAMETER retryCount
The initial retry count. This parameter is used to track the number of attempts during the function execution. The default value is 0.
 
.EXAMPLE
 
Publish-BcAppToDevEndpointWithRetry `
    -appPath "C:\Apps" `
    -devServerUrl "https://navdev.smartcloud.com.ua:7149/localizationMoldova" `
    -bcAuthContext $bcAuthContext `
    -syncMode 'ForceSync'
#>


function Publish-BcAppToDevEndpointWithRetry {
    param (
        [Parameter(Mandatory=$true)]
        [string] $appPath,
        [Parameter(Mandatory=$true)]
        [string] $devServerUrl,
        [Hashtable] $bcAuthContext,
        [pscredential] $credentials,
        [string] $environment,
        [switch] $sslVerificationDisabled,
        [ValidateSet('Add','Clean','Development','ForceSync')]
        [string] $syncMode = 'ForceSync',
        [ValidateSet('Default','Ignore','Strict')]
        [string] $dependencyPublishingOption = 'Ignore',
        [int] $timeoutMinutes = 10,
        [int] $maxRetries = 3,
        [int] $WaitingTimeInSecond = 90,
        [int] $Interval = 30,
        [int] $retryCount = 0
    )

    $params = @{}

    if ($PSBoundParameters.ContainsKey('bcAuthContext')) {
        $params = @{
            "bcAuthContext" = $bcAuthContext
            "environment" = $environment
        }
    }
    elseif ($PSBoundParameters.ContainsKey('credentials')) {
        $params = @{
            "credentials" = $credentials
        }
    }

    while ($retryCount -lt $maxRetries -and $appPaths.Count -gt 0) {
        Write-Host "Publishing to tenant"

        & {
            Publish-BcAppToDevEndpoint @params `
                -devServerUrl $devServerUrl `
                -appFile $appPaths `
                -syncMode $syncMode `
                -dependencyPublishingOption $dependencyPublishingOption `
                -timeoutMinutes $timeoutMinutes 
        } *>&1 | Tee-Object -Variable publishResult

        if ($publishResult -match 'successfully published' -or $publishResult -match 'A duplicate package ID is detected.' -or $publishResult -match 'was already installed') {
            $successCount = ($publishResult | Select-String -Pattern 'successfully published').Matches.Count
            $duplicateCount = ($publishResult | Select-String -Pattern 'A duplicate package ID is detected').Matches.Count
            $alreadyInstaledCount = ($publishResult | Select-String -Pattern 'was already installed').Matches.Count
            $totalAppsCount = ($appNamesList -split ',').Count
            echo "successCount $successCount"
            echo "duplicateCount $duplicateCount"
            echo "alreadyInstaledCount $alreadyInstaledCount"
            echo "totalAppsCount $totalAppsCount"

            if (($successCount + $duplicateCount + $alreadyInstaledCount) -eq $totalAppsCount) {
                echo "successCount+duplicateCount+alreadyInstaledCount = totalAppsCount"
                break
            } else {
                $remainingAppList = $appNamesList -split ',\s*'
                foreach ($app in $remainingAppList) {
                    $patternSuccessfully = "App SMART business LLC_$app"
                    $patternDuplicate = "A duplicate package ID is detected"
                    if ($publishResult -match "$patternSuccessfully.*successfully published") {
                        Write-Output "App $app was successfully published"
                        $remainingAppList = $remainingAppList | Where-Object {$_ -ne $app}
                    } elseif ($publishResult -match "$patternDuplicate.*name: '$app'") {
                        Write-Output "App $app duplicate package"
                        $remainingAppList = $remainingAppList | Where-Object {$_ -ne $app}
                    } else {
                        Write-Output "App $app was NOT successfully published"
                    }
                }
                $appNamesList = $remainingAppList -join ', '
                $appPaths = @($appNamesList.Split(',').Trim() | Foreach-Object { (Get-ChildItem -Path (Join-Path $appPath ("*_"+$_+"_*.app"))) })
                echo "appNamesList $appNamesList"
                echo "appPaths $appPaths"
            }
        } 
        $retryCount++
        echo "Start redeploy: $retryCount"
        
        Start-Sleep -Seconds $retryIntervalSeconds  

        for ($i = $retryIntervalSeconds; $i -le $totalWaitingTime; $i += $retryIntervalSeconds) {
            Write-Host -NoNewline "`r$i second(s)..."
            Start-Sleep -Seconds $retryIntervalSeconds
        }
        echo "\nRedeploy apps: $appNamesList"
    }
}