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 handles various publishing scenarios, including publishing multiple apps from file paths or from a URL containing a zipped collection of apps. The function supports retries if the initial publishing attempts fail and provides detailed feedback on publishing results, handling common outcomes such as successful publishing, duplicate package detection, or apps that are already installed. The function supports two authentication methods: Business Central OAuth authentication context (`bcAuthContext`) or classic credentials (`pscredential`). Based on the outcome of each publishing attempt, the function either proceeds to the next app or retries the publishing process up to a specified number of times. .PARAMETER appFile Specifies the app file path(s) or a URL pointing to a ZIP file containing apps. Multiple paths can be provided as a space-separated string. .PARAMETER devServerUrl Specifies the URL of the development endpoint where the apps will be published. .PARAMETER bcAuthContext Specifies the Business Central OAuth authentication context to be used for publishing. .PARAMETER credential Specifies the credentials to use if `bcAuthContext` is not provided. .PARAMETER environment Specifies the environment name in Business Central. This is used when authenticating with `bcAuthContext`. .PARAMETER sslVerificationDisabled A switch that disables SSL verification for connections to the development server. .PARAMETER syncMode Specifies the synchronization mode for publishing apps. .PARAMETER dependencyPublishingOption Specifies how dependencies should be handled during publishing. .PARAMETER timeoutMinutes Specifies the timeout for publishing operations, in minutes. The default is 10 minutes. .PARAMETER maxRetries Specifies the maximum number of retries if the publishing process fails. The default is 3 retries. .PARAMETER WaitingTimeInSecond Specifies the waiting time between retries, in seconds. The default is 90 seconds. .EXAMPLE Publish-BcAppToDevEndpointWithRetry ` -appFile "C:\Apps\MyApp1.app C:\Apps\MyApp2.app" ` -devServerUrl "https://navdev.smartcloud.com.ua:7149/localizationMoldova" ` -bcAuthContext $bcAuthContext ` -syncMode 'ForceSync' ` -dependencyPublishingOption 'Ignore' ` -maxRetries 3 ` -WaitingTimeInSecond 90 #> function Publish-BcAppToDevEndpointWithRetry { param ( [Parameter(Mandatory = $true)] $appFile, [Parameter(Mandatory = $true)] [string] $devServerUrl, [Hashtable] $bcAuthContext, [pscredential] $credential, [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] $retryCount = 0 ) $params = @{} $appHashTable = @{} $appPathsList = @() $appNamesArray = @() if ($bcAuthContext) { $params = @{ "bcAuthContext" = $bcAuthContext "environment" = $environment } } elseif ($credential) { $params = @{ "credential" = $credential } } function Validate-AndCreateAppHashTable { param ( [array]$appNamesArray, [array]$appPathsList ) $appHashTable = @{} if ($appNamesArray.Count -eq $appPathsList.Count) { for ($i = 0; $i -lt $appNamesArray.Count; $i++) { $appHashTable[$appNamesArray[$i]] = $appPathsList[$i] } } else { Write-Warning "The appNamesArray and appPathsList arrays have different lengths. Please check the data." } return $appHashTable } function Get-AppNamesFromPaths { param ( [string[]]$appPaths ) $appNamesArray = @() foreach ($appPath in $appPaths) { $appName = (Get-AppJsonFromAppFile -appFile $appPath).name $appNamesArray += $appName } return $appNamesArray } if ($appFile -like "https://*") { $tempFolder = [System.IO.Path]::Combine($env:TEMP, [System.Guid]::NewGuid().ToString()) $tempZipPath = [System.IO.Path]::Combine($tempFolder, "latest.zip") New-Item -ItemType Directory -Path $tempFolder | Out-Null try { $destinationFolder = [System.IO.Path]::Combine($tempFolder, "extracted") New-Item -ItemType Directory -Path $destinationFolder | Out-Null CopyAppFilesToFolder -appFiles $appFile -folder $destinationFolder $appPathsList = @(Get-ChildItem -Path $destinationFolder -Filter "*.app" -Recurse | Select-Object -ExpandProperty FullName) $appNamesArray = @(Get-AppNamesFromPaths -appPaths $appPathsList) $appHashTable = Validate-AndCreateAppHashTable -appNamesArray $appNamesArray -appPathsList $appPathsList } catch { Write-Host "Error: $($_.Exception.Message)" } } else { $appPathsList = @($appFile -split "(?<=\.app) ") $appNamesArray = @(Get-AppNamesFromPaths -appPaths $appPathsList) $appHashTable = Validate-AndCreateAppHashTable -appNamesArray $appNamesArray -appPathsList $appPathsList } while ($retryCount -le $maxRetries -and $appHashTable.Count -gt 0) { Write-Host "Publishing to tenant" & { Publish-BcAppToDevEndpoint @params ` -devServerUrl $devServerUrl ` -appFile $appHashTable.Values ` -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' -or $publishResult -match "\u041E\u0431\u043D\u0430\u0440\u0443\u0436\u0435\u043D \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E\u0449\u0438\u0439\u0441\u044F \u0418\u0414 \u043F\u0430\u043A\u0435\u0442\u0430") { $successCount = ($publishResult | Select-String -Pattern 'successfully published').Matches.Count $alreadyInstalledCount = ($publishResult | Select-String -Pattern 'was already installed').Matches.Count $totalAppsCount = $appHashTable.Count if ($publishResult -match "\u041E\u0431\u043D\u0430\u0440\u0443\u0436\u0435\u043D") { $duplicateCount = ($publishResult | Select-String -Pattern "\u041E\u0431\u043D\u0430\u0440\u0443\u0436\u0435\u043D \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E\u0449\u0438\u0439\u0441\u044F \u0418\u0414 \u043F\u0430\u043A\u0435\u0442\u0430").Matches.Count } else { $duplicateCount = ($publishResult | Select-String -Pattern 'A duplicate package ID is detected').Matches.Count } if (($successCount + $duplicateCount + $alreadyInstalledCount) -eq $totalAppsCount) { Write-Host "All apps were installed" break } else { $appKeys = $appHashTable.Keys.Clone() foreach ($app in $appKeys) { $patternSuccessfully = "App .*$app" $patternDuplicate = "A duplicate package ID is detected" $patternDuplicateRU = "\u041E\u0431\u043D\u0430\u0440\u0443\u0436\u0435\u043D \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E\u0449\u0438\u0439\u0441\u044F \u0418\u0414 \u043F\u0430\u043A\u0435\u0442\u0430" if ($publishResult -match "$patternSuccessfully.*successfully published") { Write-Output "App $app was successfully published" $appHashTable.Remove($app) } elseif ($publishResult -match "$patternDuplicate.*name: '$app'" -or $publishResult -match "$patternDuplicateRU.*именем `"$app`"") { Write-Output "App $app duplicate package" $appHashTable.Remove($app) } else { Write-Output "App $app was NOT successfully published" } } Write-Host "The remaining apps have not been published:" foreach ($value in $appHashTable.Values) { Write-Host $value } } } if ($retryCount -ge $maxRetries) { Write-Host "Max retries reached. Stopping further attempts." break } $retryCount++ Write-Host "Start redeploy: $retryCount" Write-Host "Time waiting: $WaitingTimeInSecond" Start-Sleep -Seconds $WaitingTimeInSecond } if ($appFile -like "https://*") { Remove-Item -Path $tempFolder -Recurse -Force } } |