Publish/Publish-BcAppsUsePowerShellNew.ps1
|
<#
.SYNOPSIS Publishes applications to an On-prem instance using PowerShell commands. .DESCRIPTION This function publishes applications to an On-prem instance of Business Central. It utilizes various parameters to control the publishing process, including server instance details, authentication context, and several flags to manage application synchronization and installation. .PARAMETER appPaths A collection of paths to the applications that will be published. .PARAMETER ServerInstance The name of the server instance where the applications will be published. .PARAMETER force A flag that forces the execution of Sync-NAVApp. .PARAMETER uninstallApps A flag that removes all dependent applications. .PARAMETER ScopeTenant By default, applications are published as Global. If this parameter is used, applications will be published as PTE. .PARAMETER installUninstalledApps A flag that allows the installation of applications that were published but not installed. This works only for the versions you want to install. .Example Publish-BcAppsUsePowerShellNew ` -appPaths $appPaths ` -ServerInstance "localizationapps" ` -force:([bool]::Parse("${{ parameters.withForce }}")) ` -uninstallApps:([bool]::Parse("${{ parameters.uninstallApps }}")) #> function Publish-BcAppsUsePowerShellNew { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]]$appPaths, [Parameter(Mandatory = $true)] [string]$ServerInstance, [switch]$force, [switch]$uninstallApps, [switch]$ScopeTenant, [switch]$installUninstalledApps ) Write-Host "Force mode: $force" $appInfo = @{} $installApps = [ordered]@{} $removeApps = [ordered]@{} $targetApps = @() $DependentAppNames = @{} $DependentAppNamesWithoutTarget = @{} $UnpublishApps = @{} $maxAttempts = 5 $attempt = 0 $filteredAppPaths = @() $installedSmartApps = @{} $appsToUpdate = @{} $appNamesToUpdate = @() Get-BcManagementModule -ServerInstance $ServerInstance $installedExtensions = Get-NAVAppInfo -ServerInstance $ServerInstance foreach ($appPath in $appPaths) { $appJson = Get-AppJsonFromAppFile $appPath $appInfo[$appJson.name] = $appJson.version } foreach ($app in $appInfo.Keys) { $targetApps += $app } $installedApps = Get-NAVAppInfo -ServerInstance $ServerInstance -TenantSpecificProperties -Tenant default Write-Host "##[group]Search apps not installing" $matchingApps = @{} foreach ($app in $installedApps) { if ($app.Publisher -eq "SMART business LLC" -and $app.isInstalled -eq $false) { $installedVersion = [version]"$($app.Version)" $newerVersionExists = $false foreach ($installedApp in $installedApps) { if ($installedApp.Name -eq $app.Name -and $installedApp.isInstalled -eq $true) { $currentVersion = [version]"$($installedApp.Version)" if ($currentVersion -gt $installedVersion) { echo "currentVersion $currentVersion" echo "installedVersion $installedVersion" $newerVersionExists = $true break } } } if (!$newerVersionExists -and $appInfo.ContainsKey($app.Name) -and [version]$appInfo[$app.Name] -eq $installedVersion) { $matchingApps[$app.Name] = "$installedVersion" } elseif ($newerVersionExists) { Write-Output "A newer version of $($app.Name) is already installed or published." } } } # Remove matching apps from appInfo AFTER the loop completes $matchingApps if ($matchingApps.Count -gt 0) { if ($installUninstalledApps) { # Apps are already published with the same version — just sync & install them foreach ($appName in $matchingApps.Keys) { $version = $matchingApps[$appName] Write-Output "Installing already-published $appName v$version ..." Publish-BCApp -ServerInstance $ServerInstance -appName $appName -appVersion $version -Sync -force:$force -InstallDataUpgrade Write-Output "Successfully installed $appName version $version" } # Only remove from appInfo after successful install foreach ($appName in $matchingApps.Keys) { if ($appInfo.ContainsKey($appName)) { $appInfo.Remove($appName) Write-Host "Removed $appName from appInfo because it was already published and is now installed." } } } else { # installUninstalledApps is false — keep these apps in $appInfo so the main # publish flow can handle them (it will skip re-publish if already published). Write-Output "The parameter installUninstalledApps is false. Apps already published but not installed will be handled by the main publish flow." } } else { Write-Output "No previously published (uninstalled) apps found matching the deployment set." } Write-Host "##[endgroup]" # Re-query installed apps to reflect any changes made by the installUninstalledApps block above $installedApps = Get-NAVAppInfo -ServerInstance $ServerInstance -TenantSpecificProperties -Tenant default foreach ($app in $installedApps) { if ($app.Publisher -eq "SMART business LLC" -and $app.isInstalled -eq $true) { $version = "$($app.Version)" $installedSmartApps[$app.Name] = $version } } Write-Host "##[group]Apps installed on the environment" $installedSmartApps Write-Host "##[endgroup]" foreach ($appName in $appInfo.Keys) { if ($installedSmartApps.ContainsKey($appName)) { $installedVersion = [version]$installedSmartApps[$appName] $newVersion = [version]$appInfo[$appName] if ($newVersion -gt $installedVersion) { $appsToUpdate[$appName] = $appInfo[$appName] } } else { $appsToUpdate[$appName] = $appInfo[$appName] } } Write-Host "##[group]Sorted apps to install" echo "List of applications to be installed" $appsToUpdate Write-Host "##[endgroup]" foreach ($appName in $appsToUpdate.Keys) { $appNamesToUpdate += $appName } foreach ($appPath in $appPaths) { $appJson = Get-AppJsonFromAppFile $appPath $appName = $appJson.name if ($appNamesToUpdate -contains $appName) { $filteredAppPaths += $appPath } } $sortApps = Sort-AppFilesByDependencies -appFiles $filteredAppPaths 3> $null foreach ($appPath in $sortApps) { $appJson = Get-AppJsonFromAppFile $appPath $appName = $appJson.name $appVersion = $appJson.version $installApps[$appName] = @{ Path = $appPath; Version = $appVersion } } $DependentAppNames = Get-DependentApps -ServerInstance $ServerInstance -TargetApps $appNamesToUpdate -IncludeTargetApps $DependentAppNamesWithoutTarget = Get-DependentApps -ServerInstance $ServerInstance -TargetApps $appNamesToUpdate Write-Host "##[group]Uninstall dependencies of dependent apps (Optionally)" if ($uninstallApps) { foreach ($entry in $DependentAppNames) { $appName = $entry.Key $versions = $entry.Value if ([string]::IsNullOrEmpty($appName) -or [string]::IsNullOrEmpty($versions)) { Write-Output "The app is not published on the environment: $appName" } else { foreach ($version in $versions) { if ([string]::IsNullOrEmpty($version)) { Write-Output "The app version is not specified for $appName" } else { Uninstall-NAVApp -ServerInstance $ServerInstance -Name $appName -Version $version Write-Output "Successfully uninstalled $appName version $version" } } } } } else { Write-Output "The uninstallApps parameter was not set to True." } Write-Host "##[endgroup]" Write-Host "##[group]Install new app" echo "Install new apps" if ($installApps.Count -gt 0) { foreach ($appName in $installApps.Keys) { $appData = $installApps[$appName] $versionList = $appData["Version"] $Path = $appData["Path"] echo "versionList $versionList" echo "Path $Path" foreach ($version in $versionList) { if ($ScopeTenant) { Publish-BCApp -ServerInstance $ServerInstance -appPath $Path -appName $appName -appVersion $version -force:$force -Publish -Sync -InstallDataUpgrade -ScopeTenant } else { Publish-BCApp -ServerInstance $ServerInstance -appPath $Path -appName $appName -appVersion $version -force:$force -Publish -Sync -InstallDataUpgrade } } } } else { Write-Host "No apps found for installation" } Write-Host "##[endgroup]" Write-Host "##[group]Unpublish the old version of the applications that we have updated" foreach ($appName in $appNamesToUpdate) { $matchingApp = $DependentAppNames | Where-Object { $_.Name -eq $appName } if ($matchingApp) { $UnpublishApps[$appName] = $matchingApp.Value } } $remainingApps = $UnpublishApps.Clone() echo "Unpublish apps:" $remainingApps while ($attempt -lt $maxAttempts -and $remainingApps.Count -gt 0) { $attempt++ Write-Output "Attempt $attempt of $maxAttempts" $failedApps = @{} foreach ($entry in $remainingApps.GetEnumerator()) { $appName = $entry.Key $versions = $entry.Value if ([string]::IsNullOrEmpty($appName) -or [string]::IsNullOrEmpty($versions)) { Write-Output "The app is not published on the environment: $appName" } else { foreach ($version in $versions) { if ([string]::IsNullOrEmpty($version)) { Write-Output "The app version is not specified for $appName" } else { if ($installApps.Contains($appName) -and $installApps[$appName].Version -eq $version) { Write-Output "Skipping unpublish for $appName version $version as it was just installed." } else { try { Unpublish-NAVApp -ServerInstance $ServerInstance -Name $appName -Version $version -ErrorAction Stop Write-Output "Successfully unpublished $appName version $version" } catch { if ($_.Exception.Message -match "No published package matches") { Write-Output "Already unpublished: $appName version $version (no matching package found)" } else { Write-Output "Failed to unpublish $appName version $version. Error: $_" if (-not $failedApps.ContainsKey($appName)) { $failedApps[$appName] = @() } $failedApps[$appName] += $version } } } } } } } if ($failedApps.Count -gt 0) { Write-Output "Retrying failed apps..." $remainingApps = $failedApps.Clone() } else { Write-Output "All apps successfully unpublished." break } } Write-Host "##[endgroup]" $remainingDependentApps = $DependentAppNamesWithoutTarget | Where-Object { $installApps.keys -notcontains $_.Name } Write-Host "##[group]Installing all dependencies" echo "Installing all dependencies that were uninstall to install new applications" $remainingDependentApps if ($remainingDependentApps.Count -gt 0) { foreach ($app in $remainingDependentApps.Keys) { $appName = $app $version = $remainingDependentApps[$app] Write-Output "Installing $appName ...." Publish-BCApp -ServerInstance $ServerInstance -appName $appName -appVersion $version -Sync -force:$force -InstallDataUpgrade Write-Output "Successfully Install $appName version $version" } } Write-Host "##[endgroup]" } <# .SYNOPSIS Publishes a Business Central application to an On-prem instance using PowerShell commands. .DESCRIPTION This function publishes a Business Central application to an On-prem instance. It includes parameters to control the publishing, synchronization, and installation processes, allowing for flexible and precise management of the application lifecycle. .PARAMETER ServerInstance The name of the server instance where the application will be published. .PARAMETER appPath The path to the application file that will be published. .PARAMETER appName The name of the application to be published. .PARAMETER appVersion The version of the application to be published. .PARAMETER force A flag that forces the execution of Sync-NAVApp. .PARAMETER Publish A flag that triggers the publishing of the application. .PARAMETER Sync A flag that triggers the synchronization of the application. .PARAMETER InstallDataUpgrade A flag that allows the installation or upgrade of the application data. .PARAMETER ScopeTenant If this parameter is used, the application will be published as Tenant-specific (PTE). Otherwise, it will be published as Global. #> function Publish-BCApp { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ServerInstance, [string]$appPath, [Parameter(Mandatory = $true)] [string]$appName, [Parameter(Mandatory = $true)] [string]$appVersion, [switch]$force, [switch]$Publish, [switch]$Sync, [switch]$InstallDataUpgrade, [switch]$ScopeTenant ) $timeoutMinutes = 10 $navModulePath = (Get-Module "Microsoft.Dynamics.Nav.Management").Path if ($Publish) { try { Write-Host "Publishing $appName ... " $job = Start-Job -ScriptBlock { param($modulePath, $si, $p, $scopeTenant) Import-Module $modulePath -ErrorAction Stop if ($scopeTenant) { Publish-NAVApp -ServerInstance $si -Path $p -SkipVerification -Force -Scope Tenant -ErrorAction Stop } else { Publish-NAVApp -ServerInstance $si -Path $p -SkipVerification -Force -ErrorAction Stop } } -ArgumentList $navModulePath, $ServerInstance, $appPath, $ScopeTenant.IsPresent $completed = $job | Wait-Job -Timeout ($timeoutMinutes * 60) if ($null -eq $completed) { $job | Stop-Job $job | Remove-Job -Force throw "Publish-NAVApp timed out after $timeoutMinutes minutes for $appName" } $jobOutput = $job | Receive-Job -ErrorAction Stop $job | Remove-Job -Force if ($jobOutput -match "is already published for tenant 'default'") { Write-Output "The app is already published for tenant 'default'. Skipping publish..." } else { Write-Output $jobOutput } } catch { if ($_.Exception.Message -match "is already published") { Write-Output "The app $appName is already published. Skipping publish..." } else { Write-Error "Failed to publish $appName version $appVersion to $ServerInstance : $_" throw } } } if ($Sync) { try { Write-Host "Syncing $appName ... " $job = Start-Job -ScriptBlock { param($modulePath, $si, $name, $ver, $forceSync) Import-Module $modulePath -ErrorAction Stop if ($forceSync) { Sync-NAVApp -ServerInstance $si -Name $name -Version $ver -Mode ForceSync -ErrorAction Stop } else { Sync-NAVApp -ServerInstance $si -Name $name -Version $ver -ErrorAction Stop } } -ArgumentList $navModulePath, $ServerInstance, $appName, $appVersion, $force.IsPresent $completed = $job | Wait-Job -Timeout ($timeoutMinutes * 60) if ($null -eq $completed) { $job | Stop-Job $job | Remove-Job -Force throw "Sync-NAVApp timed out after $timeoutMinutes minutes for $appName" } $job | Receive-Job -ErrorAction Stop $job | Remove-Job -Force } catch { Write-Error "Failed to synchronize $appName version $appVersion on $ServerInstance : $_" throw } } $navAppInfoFromDb = Get-NAVAppInfo -ServerInstance $ServerInstance -Name $appName -Version $appVersion -Tenant "default" -TenantSpecificProperties if ($null -eq $navAppInfoFromDb.ExtensionDataVersion -or $navAppInfoFromDb.ExtensionDataVersion -eq $navAppInfoFromDb.Version) { $install = $true } else { $upgrade = $true } if ($InstallDataUpgrade) { if ($install) { try { Write-Host "Installing $appName ... " $job = Start-Job -ScriptBlock { param($modulePath, $si, $name, $ver) Import-Module $modulePath -ErrorAction Stop Install-NAVApp -ServerInstance $si -Name $name -Version $ver -ErrorAction Stop } -ArgumentList $navModulePath, $ServerInstance, $appName, $appVersion $completed = $job | Wait-Job -Timeout ($timeoutMinutes * 60) if ($null -eq $completed) { $job | Stop-Job $job | Remove-Job -Force throw "Install-NAVApp timed out after $timeoutMinutes minutes for $appName" } $job | Receive-Job -ErrorAction Stop $job | Remove-Job -Force } catch { Write-Error "Failed to install $appName version $appVersion on $ServerInstance : $_" throw } } if ($upgrade) { try { Write-Host "Upgrading $appName ... " $job = Start-Job -ScriptBlock { param($modulePath, $si, $name, $ver) Import-Module $modulePath -ErrorAction Stop Start-NAVAppDataUpgrade -ServerInstance $si -Name $name -Version $ver -ErrorAction Stop } -ArgumentList $navModulePath, $ServerInstance, $appName, $appVersion $completed = $job | Wait-Job -Timeout ($timeoutMinutes * 60) if ($null -eq $completed) { $job | Stop-Job $job | Remove-Job -Force throw "Start-NAVAppDataUpgrade timed out after $timeoutMinutes minutes for $appName" } $job | Receive-Job -ErrorAction Stop $job | Remove-Job -Force } catch { Write-Error "Failed to start data upgrade for $appName version $appVersion on $ServerInstance : $_" throw } } } Write-Output "Successfully published $appName version $appVersion to $ServerInstance`n" } function Get-DependentApps { param ( [string]$ServerInstance, [string[]]$TargetApps, [switch]$IncludeTargetApps ) $apps = Get-NAVAppInfo -ServerInstance $ServerInstance -Publisher "SMART business LLC" $dependenciesDict = @{} foreach ($app in $apps) { $appDependencies = (Get-NAVAppInfo -ServerInstance $ServerInstance -Name $app.Name).Dependencies $filteredDependencies = $appDependencies | Where-Object { $_.Publisher -ne "Microsoft" } $dependenciesDict[$app.Name] = $filteredDependencies | ForEach-Object { @{ Name = $_.Name Version = $_.Version } } } $dependentApps = @{} $dependenciesDict.GetEnumerator() | ForEach-Object { $appName = $_.Key $dependencies = $_.Value $isDependent = $dependencies | Where-Object { $targetApps -contains $_.Name } if ($isDependent) { $dependentApps[$appName] = ($apps | Where-Object { $_.Name -eq $appName }).Version } } if ($IncludeTargetApps) { $targetApps | ForEach-Object { $dependentApps[$_] = ($apps | Where-Object { $_.Name -eq $_ }).Version } } $keysToUpdate = $dependentApps.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key } foreach ($key in $keysToUpdate) { $dependentApps[$key] = ($apps | Where-Object { $_.Name -eq $key }).Version } return $dependentApps.GetEnumerator() | Sort-Object -Property Name -Unique } |