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 bcAuthContext
  The authentication context used to retrieve applications via the API.
 
.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.
 
.PARAMETER portForServerInstance
  The port used to access the API to retrieve the installed applications.
 
.Example
    Publish-BcAppsUsePowerShellNew `
    -appPaths $appPaths `
    -ServerInstance "localizationapps" `
    -force:([bool]::Parse("${{ parameters.withForce }}")) `
    -uninstallApps:([bool]::Parse("${{ parameters.uninstallApps }}")) `
    -bcAuthContext $authContext `
    -portForServerInstance 7148
#>

function Publish-BcAppsUsePowerShellNew {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]]$appPaths,
        [Parameter(Mandatory = $true)]
        [string]$ServerInstance,
        [Hashtable]$bcAuthContext,
        [switch]$force,
        [switch]$uninstallApps,
        [switch]$ScopeTenant,
        [switch]$installUninstalledApps,
        [int]$portForServerInstance
    )

    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
    }

    if ($portForServerInstance -ne $null -and $portForServerInstance) {
        $installedApps = Get-BcInstalledExtensionsOnPrem -serverInstance $ServerInstance -bcAuthContext $bcAuthContext -port $portForServerInstance
    } else {
        $installedApps = Get-BcInstalledExtensionsOnPrem -serverInstance $ServerInstance -bcAuthContext $bcAuthContext
    }

    #test
    Write-Host "##[group]Search apps not instaling"
    $matchingApps = @{}

    foreach ($app in $installedApps) {
        if ($app.publisher -eq "SMART business LLC" -and $app.isInstalled -eq $false) {
            $installedVersion = "$($app.versionMajor).$($app.versionMinor).$($app.versionBuild).$($app.versionRevision)"
            
            if ($appInfo.ContainsKey($app.displayName) -and $appInfo[$app.displayName] -eq $installedVersion) {
                $matchingApps[$app.displayName] = $installedVersion
            }
        }
    }

    $matchingApps
    if ($installUninstalledApps -and $matchingApps.Count -gt 0) {
        foreach ($app in $matchingApps.Keys) {
            $appName = $app
            $version = $matchingApps[$app]
            Write-Output "Installing $appName ...."
            Publish-BCApp -ServerInstance $ServerInstance -appName $appName -appVersion $version -Sync -force:$force -InstallDataUpgrade
            Write-Output "Successfully installed $appName version $version"
        }
    } else {
        if (-not $installUninstalledApps) {
            Write-Output "The parameter installUninstalledApps is set to false."
        } else {
            Write-Output "There are no apps to install."
        }
    }
    Write-Host "##[endgroup]"
    #test

    foreach ($app in $installedApps) {
        if ($app.publisher -eq "SMART business LLC" -and $app.isInstalled -eq $true) {
            $version = "$($app.versionMajor).$($app.versionMinor).$($app.versionBuild).$($app.versionRevision)"
            $installedSmartApps[$app.displayName] = $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 {
                                $UnpublishResult = $null
                                & { Unpublish-NAVApp -ServerInstance $ServerInstance -Name $appName -Version $version } *>&1 | Tee-Object -Variable UnpublishResult
                                if ([string]::IsNullOrEmpty($UnpublishResult)) {
                                    Write-Output "Successfully Unpublish $appName version $version"
                                } else {
                                    Write-Output "Failed to Unpublish $appName version $version. Error: $UnpublishResult"
                                    if (-not $failedApps.ContainsKey($appName)) {
                                        $failedApps[$appName] = @()
                                    }
                                    $failedApps[$appName] += $version
                                }
                            } catch {
                                Write-Output "Exception occurred while unpublishing $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 -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
    )

    if ($Publish) {
        #try {
        Write-Host "Publishing $appName ... "
        if ($ScopeTenant) {
            Publish-NAVApp -ServerInstance $ServerInstance -Path $appPath -SkipVerification -Force -Scope Tenant
        } else {
            Publish-NAVApp -ServerInstance $ServerInstance -Path $appPath -SkipVerification -Force
        }
        #}
        #catch {
        # Write-Error "Failed to publish $appName version $appVersion to $ServerInstance : $_"
        # return
        #}
    }
    if ($Sync) {
        #try {
        Write-Host "Syncing $appName ... "
        if ($force) {
            Sync-NAVApp -ServerInstance $ServerInstance -Name $appName -Version $appVersion -Mode ForceSync -ErrorAction Stop
        } else {
            Sync-NAVApp -ServerInstance $ServerInstance -Name $appName -Version $appVersion -ErrorAction Stop
        }
        #}
        #catch {
        # Write-Error "Failed to synchronize $appName version $appVersion on $ServerInstance : $_"
        # return
        #}
    }

    $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 ... "
            Install-NAVApp -ServerInstance $ServerInstance -Name $appName -Version $appVersion 
            #}
            #catch {
            # Write-Error "Failed to install $appName version $appVersion on $ServerInstance : $_"
            # return
            #}
        }

        if ($upgrade) {
            #try {
            Write-Host "Upgrading $appName ... "
            Start-NAVAppDataUpgrade -ServerInstance $ServerInstance -Name $appName -Version $appVersion 
            #}
            #catch {
            # Write-Error "Failed to start data upgrade for $appName version $appVersion on $ServerInstance : $_"
            # return
            #}
        }
    }

    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
}