Business Central Release Pipeline to OnPrem Environments

Published on 29 August 2023 at 07:00

Introduction

In the previous post, a slightly more complex way of building an app was described, particularly when dealing with dependencies among apps. Now follows a description of deploying those apps to required environments using Self Hosted agents.

Prerequisites

  • Created build pipeline in this post

Script for publishing app

In the repository where organized scripts and config files are located, it's necessary to add the following script:

Source code:

function UpublishPublishAll {
    [CmdletBinding()]
    param(            
        [string] $Path,
        [string] $Service  
    )
    function AddToDependencyTree() {
        param(
            [PSObject] $App,
            [PSObject[]] $DependencyArray,
            [PSObject[]] $AppCollection,
            [Int] $Order = 1
        )   
        foreach ($Dependency in $App.Dependencies) {
            $DependencyArray = AddToDependencyTree `
                -App ($AppCollection | Where-Object AppId -eq $Dependency.AppId) `
                -DependencyArray $DependencyArray `
                -AppCollection $AppCollection `
                -Order ($Order - 1)
        }
        if (-not($DependencyArray | Where-Object AppId -eq $App.AppId)) {
            $DependencyArray += $App
            try {
                ($DependencyArray | Where-Object AppId -eq $App.AppId).ProcessOrder = $Order
            }
            catch { }
        }
        else {
            if (($DependencyArray | Where-Object AppId -eq $App.AppId).ProcessOrder -gt $Order) {
                ($DependencyArray | Where-Object AppId -eq $App.AppId).ProcessOrder = $Order
            } 
        }
        $DependencyArray
    }

    $AllAppFiles = Get-ChildItem -Path $Path -Filter "*.app"

    Write-Host "##[command]Publishing Apps to $Service service" -ForegroundColor Green -BackgroundColor Black

    $AllApps = @()
    foreach ($AppFile in $AllAppFiles) {
        $App = Get-NAVAppInfo -Path $AppFile.FullName
        $AllApps += [PSCustomObject]@{
            AppId        = $App.AppId
            Version      = $App.Version
            Name         = $App.Name
            Publisher    = $App.Publisher
            ProcessOrder = 0                            
            Dependencies = $App.Dependencies
            Path         = $AppFile.FullName
        }
    }

    
    $FinalResult = @()

    $AllApps | ForEach-Object {    
        $FinalResult = AddToDependencyTree -App $_ -DependencyArray $FinalResult -AppCollection $AllApps -Order $AllApps.Count
    }

    #Unpublish all apps
    $AppForUnpublish = @()
    foreach ($AppForUnpublish in $FinalResult | Sort-Object ProcessOrder -Descending) {
        try {
            $AppVersion = Get-NAVAppInfo $Service -Id $AppForUnpublish.AppId
            Uninstall-NAVApp -ServerInstance $Service -Force -Name $AppForUnpublish.Name -Version $AppVersion.Version
            Unpublish-NAVApp -Name $AppForUnpublish.Name -ServerInstance $Service -Version $AppVersion.Version
            Write-Host '##[section]Unpublished and uninstalled:' $AppForUnpublish.Name 'Version:' $AppVersion.Version
        }
        catch {
            Write-Host '##[section]There is no installed app:' $AppForUnpublish.Name
        }
    }
    Write-Host '##[command]******************************************************************************'
    #Publish all apps
    $AppForPublish = @()
    foreach ($AppForPublish in $FinalResult | Sort-Object ProcessOrder) {
        Publish-NAVApp -Path $AppForPublish.Path -ServerInstance $Service
        Sync-NAVApp -ServerInstance $Service -Force -Mode ForceSync -Name $AppForPublish.Name -Version $AppForPublish.Version
        try {
            Start-NAVAppDataUpgrade -ServerInstance $Service -Name $AppForPublish.Name -SkipVersionCheck
            Write-Host '##[section]Published and upgraded:' $AppForPublish.Name 'Version:' $AppForPublish.Version 'Path:' $AppForPublish.Path 'Process Order:' $AppForPublish.ProcessOrder -ForegroundColor Green -BackgroundColor Black
        }
        catch {
            Install-NAVApp -Name $AppForPublish.Name -ServerInstance $Service -Version $AppForPublish.Version
            Write-Host '##[section] Published and installed:' $AppForPublish.Name 'Version:' $AppForPublish.Version  'Path:' $AppForPublish.Path 'Process Order:' $AppForPublish.ProcessOrder -ForegroundColor Green -BackgroundColor Black
        }
    }
}

function GetDependentApps() {
    param(
        [string] $ServiceInstance,
        [string] $AppId,
        [PSObject[]] $AppsForDownload
    )
    $Apps = Get-NAVAppInfo -ServerInstance $ServiceInstance
    foreach ($App in $Apps) {
        $AppSettings = Get-NAVAppInfo $ServiceInstance -Id $App.AppId
        $Dependencies = $AppSettings.Dependencies
        foreach ($Dependency in $Dependencies) {
            if ($Dependency.appId -eq $AppId) { 
                $AppsForDownload += [PSCustomObject]@{
                    AppId     = $App.AppId
                    Version   = $App.Version
                    Name      = $App.Name
                    Publisher = $App.Publisher
                }
                $AppsForDownload = GetDependentApps -ServiceInstance $ServiceInstance -AppId $App.AppId -AppsForDownload $AppsForDownload
            }
        }
    }
    $AppsForDownload
}

$ArtifactsDirectory = (Join-Path -Path $env:System_ArtifactsDirectory -ChildPath ($env:Release_PrimaryArtifactSourceAlias + '\Artifacts'))
$settings = (Get-Content (Join-Path -Path $ArtifactsDirectory -ChildPath ($env:ConfigFile)) -Encoding UTF8 | ConvertFrom-Json)
$BuildApp = (Get-Content (Join-Path -Path $ArtifactsDirectory -ChildPath ("app.json")) -Encoding UTF8 | ConvertFrom-Json)
$AppSettings = $settings.Apps | Where-Object { $_.id -eq $BuildApp.id }
$RepositorySettings = (Get-Content (Join-Path -Path $ArtifactsDirectory -ChildPath ($env:RepositoryConfigFile)) -Encoding UTF8 | ConvertFrom-Json)

import-module $env:NAVADMINTOOL | Out-Null


$ArtifactName = $env:ArtifactName
$repositoryType = $env:repositoryType
$resultfile = "result.json"
$OrgName = $env:OrgName
$ProjectName = $env:ProjectName
$branchName = $env:Build_SourceBranch
$PAT = $env:PAT

foreach ($Configuration in $AppSettings.configurations) {
    New-Item -Path $ArtifactsDirectory -Name $Configuration.serverInstance -ItemType "directory" -Force 
    Copy-Item (Join-Path $ArtifactsDirectory -ChildPath($BuildApp.Publisher + "_" + $BuildApp.Name + "_" + $BuildApp.Version + ".app")) -Destination (Join-Path $ArtifactsDirectory -ChildPath($Configuration.serverInstance))
    $ArtifactsDirectory1 = Join-Path $ArtifactsDirectory -ChildPath($Configuration.serverInstance)
    
    #downloading dependent apps
    $AppsForDownload = @()
    $AppsForDownload = GetDependentApps -ServiceInstance $Configuration.serverInstance -AppId $BuildApp.Id -AppsForDownload $AppsForDownload
    #write-host "##[section]Downloading dependent apps for server instance" $Configuration.serverInstance
    foreach ($App in $AppsForDownload) {
        #getting repository name from config
        $DependentSettings = $RepositorySettings.Apps | Where-Object { $_.id -eq $App.AppId }
        $repositoryName = $DependentSettings.repositoryName
        #getting repository name from config

        #getting repository id
        $token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($PAT)"))
        $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/git/repositories/$($repositoryName)?api-version=4.1"
        $response = Invoke-RestMethod -Uri $url -Headers @{Authorization = "Basic $token" }
        $repositoryid = $response.id
        #getting repository id

        #getting latest build id
        $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/build/builds?branchName=$($branchName)&repositoryId=$($repositoryId)&repositoryType=$($repositoryType)&api-version=7.0"
        $response = Invoke-RestMethod -Uri $url -Headers @{Authorization = "Basic $token" } -OutFile $resultfile
        $Build = (Get-Content $resultfile -Encoding UTF8 | ConvertFrom-Json)
        $Build = $Build.value | Where-Object { $_.result -eq 'succeeded' }
        $BuildId = ($Build | Select-Object -ExpandProperty "id" | Select-Object -First 1)
        $buildNumber = ($Build | Select-Object -ExpandProperty "buildNumber" | Select-Object -First 1)
        #getting latest build id

        #downloading dependent app
        $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/build/builds/$($BuildID)/artifacts?artifactName=$($ArtifactName)&api-version=7.0"    
        $response = Invoke-RestMethod -Uri $url -Headers @{Authorization = "Basic $token" }
        $downloadUrl = $response.resource.downloadUrl
        $OutFile = $ArtifactsDirectory1 + "\" + $repositoryName + '.zip'
        Invoke-WebRequest -Uri $downloadUrl -Headers @{Authorization = "Basic $token" } -outFile $OutFile
        write-host "##[command]Downloaded " $OutFile "Build ID" $BuildId "Build Number" $buildNumber "from branch" $branchName
        $destinationPath = $ArtifactsDirectory1 + "\" + $repositoryName
        Expand-Archive -LiteralPath $OutFile -DestinationPath $destinationPath -Force
        #$sourcepath = Join-Path $ArtifactsDirectory1 -ChildPath($repositoryName + "\" + $ArtifactName + "\*")
        $sourcepath = $ArtifactsDirectory1 + "\" + $repositoryName + "\" + $ArtifactName + "\*"
        $folder = $Configuration.serverInstance
        write-host "##[section]Moving app" $repositoryName "to" $folder "folder"
        Copy-Item -Path $sourcepath -Include *app -Destination $ArtifactsDirectory1 -Recurse -Force
        Remove-Item -Path $OutFile -Force
        Remove-Item -Path $destinationPath -Force -Recurse
        #downloading dependent app
        #downloading dependent apps
    }
    UpublishPublishAll -Path $ArtifactsDirectory1 -Service $configuration.serverInstance
}

Release pipeline

In your project create release pipeline

Script Path: 

$(System.DefaultWorkingDirectory)/$(Release.PrimaryArtifactSourceAlias)/Artifacts/PublishApp_v22.ps1

 

or different branch which you are using for dev/uat/prod environement

 

To make release work, you have to link variable group specified in CICD.yml

 

And app is ready to publish using standard power shell commands on all instances specified in config.json

This is for example ProdConfig.json file for one client.

For all environments scripts are the same, only thing what you have to change is pipeline variable (config file)

Conclusion

This describes how apps are deployed to various OnPrem environments using self-hosted agents. The scripts are adapted for universal deployment, with the only aspect requiring attention being the variables in the pipelines. Since I previously mentioned that we have numerous AppSource apps at NavBiz, the next post will provide a description of how to utilize the Automatic AppSource Submission of Business Central apps.

Note that with this approach, you need to set up self-hosted agents as many as there are environments or machines where BC services are running.

Why this approach?

VPN is a must-have today, and this is a simple way to access artifacts generated in the build pipeline. All that's left is to run the script to deploy apps in the client's environment.

 

If this is too much coding and maintaining pipelines, scripts, etc., then Al-Go is the right option for you. The latest preview in Al-Go supports custom deployment (on-prem). Therefore, if you have repositories on GitHub, then Al-Go is the proper choice, because it's powerful, upgradable, and free.


Add comment

Comments

There are no comments yet.