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