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